From 8207be7859547aed7aef254eaa0521f741f5a04f Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 10:27:46 -0700 Subject: [PATCH 01/14] Let a principal key hold only its public key A principal whose private key lives outside the hub still needs its public key on record, so the hub can verify what the principal signs. Such a row has no private key, and the hub refuses to sign with it. --- docs/AUTH.md | 11 +- .../0119_principal_key_public_only.sql | 1 + .../db/migrations/meta/0119_snapshot.json | 5007 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/src/principal-key-store.ts | 106 +- packages/db/src/schema/principal-keys.ts | 19 +- packages/hub-api/src/routes/workflows.test.ts | 2 + .../src/run-grant-materialization.test.ts | 2 + tests/db/principal-key-store.test.ts | 166 + tests/db/principal-store.test.ts | 2 + 10 files changed, 5303 insertions(+), 20 deletions(-) create mode 100644 packages/db/migrations/0119_principal_key_public_only.sql create mode 100644 packages/db/migrations/meta/0119_snapshot.json diff --git a/docs/AUTH.md b/docs/AUTH.md index 9d8ca045b..6a8082ed2 100644 --- a/docs/AUTH.md +++ b/docs/AUTH.md @@ -43,23 +43,28 @@ principal ### Signing keys -The `principal_key` table holds a principal's Ed25519 signing key, giving it a stable cryptographic identity. The public key is a stable handle a verifier can check a signature against; the hub custodies the private key. +The `principal_key` table holds a principal's Ed25519 signing key, giving it a stable cryptographic identity. The public key is a stable handle a verifier can check a signature against. For a hub-custodied key the hub also holds the private key; for a public-only key another party holds it. ``` principal_key id text PK -- pky_... principal_id text FK -> principal (ON DELETE CASCADE) public_key text NOT NULL UNIQUE -- hex-encoded 32-byte Ed25519 public key - private_key text NOT NULL -- hex-encoded 32-byte seed, sealed by the cipher + private_key text NULL -- hex-encoded 32-byte seed, sealed by the cipher; + -- NULL when another party holds the private key status text NOT NULL -- 'active' | 'retired' created_at timestamptz updated_at timestamptz UNIQUE(principal_id) WHERE status = 'active' -- one active key per principal ``` +A row with a NULL `private_key` is public-only: another party holds the private key, and the hub holds only the public key. The hub can check the principal's signatures against it, but `principalKeyStore.sign` refuses to sign with it. + +Only the active row signs or answers for a principal. Retiring a key sets its row to `retired`; the row stays, `sign` and `getPublicKey` ignore it, and another key can then become active. Recording a retired public-only key again for the same principal makes that row active again; recording the public key of a hub-custodied row is refused, so a key recorded as public-only never signs. + The private key seed is sealed at rest with the same AEAD cipher that protects credential secrets, bound to its row and column so a sealed key cannot be transplanted to another row. See [CREDENTIALS.md](./CREDENTIALS.md) for the encryption-at-rest mechanics. -Custody is an **attribution** model, not non-repudiation. A signature proves the action came from a holder of the principal's private key, which is enough to attribute the action to the principal within the system. It is **not** proof against the hub operator, who also holds the key and could sign as the principal. Non-repudiation would require the private key to live somewhere the hub cannot reach, which the hub-custodied model does not provide. +Hub custody is an **attribution** model, not non-repudiation. A signature with a hub-custodied key proves the action came from a holder of the principal's private key, which is enough to attribute the action to the principal within the system. It is **not** proof against the hub operator, who also holds the key and could sign as the principal. Non-repudiation would require the private key to live somewhere the hub cannot reach, which the hub-custodied model does not provide. Every principal is minted a signing key when it is created, in the same transaction as the principal row, so a principal never exists without a key. The hub seals the seed under `PRINCIPAL_KEY_ENCRYPTION_KEY` (32 bytes, hex) -- a separate key from `CREDENTIAL_ENCRYPTION_KEY`, so the two rotate independently -- and refuses to start without it. diff --git a/packages/db/migrations/0119_principal_key_public_only.sql b/packages/db/migrations/0119_principal_key_public_only.sql new file mode 100644 index 000000000..1a65fb11f --- /dev/null +++ b/packages/db/migrations/0119_principal_key_public_only.sql @@ -0,0 +1 @@ +ALTER TABLE "principal_key" ALTER COLUMN "private_key" DROP NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/meta/0119_snapshot.json b/packages/db/migrations/meta/0119_snapshot.json new file mode 100644 index 000000000..bc2c658f7 --- /dev/null +++ b/packages/db/migrations/meta/0119_snapshot.json @@ -0,0 +1,5007 @@ +{ + "id": "8141ae7e-e98a-4aec-ac5c-1d2301abd631", + "prevId": "080629bc-5cbc-4177-a00b-559c2e498385", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_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 + }, + "id_token": { + "name": "id_token", + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_trust": { + "name": "federation_trust", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_tenant_id": { + "name": "target_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federation_trust_tenant_id_tenant_id_fk": { + "name": "federation_trust_tenant_id_tenant_id_fk", + "tableFrom": "federation_trust", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_trust_target_tenant_id_tenant_id_fk": { + "name": "federation_trust_target_tenant_id_tenant_id_fk", + "tableFrom": "federation_trust", + "tableTo": "tenant", + "columnsFrom": ["target_tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federation_trust_tenant_id_target_tenant_id_unique": { + "name": "federation_trust_tenant_id_target_tenant_id_unique", + "nullsNotDistinct": false, + "columns": ["tenant_id", "target_tenant_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant": { + "name": "tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "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": { + "tenant_domain_lower_idx": { + "name": "tenant_domain_lower_idx", + "columns": [ + { + "expression": "lower(\"domain\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tenant_parent_id_tenant_id_fk": { + "name": "tenant_parent_id_tenant_id_fk", + "tableFrom": "tenant", + "tableTo": "tenant", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tenant_slug_unique": { + "name": "tenant_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": { + "tenant_slug_dns_label_check": { + "name": "tenant_slug_dns_label_check", + "value": "\"tenant\".\"slug\" ~ '^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$' and length(\"tenant\".\"slug\") <= 63" + } + }, + "isRLSEnabled": false + }, + "public.principal": { + "name": "principal", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref_id": { + "name": "ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "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": { + "principal_tenant_id_tenant_id_fk": { + "name": "principal_tenant_id_tenant_id_fk", + "tableFrom": "principal", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "principal_tenant_id_kind_ref_id_unique": { + "name": "principal_tenant_id_kind_ref_id_unique", + "nullsNotDistinct": false, + "columns": ["tenant_id", "kind", "ref_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_key": { + "name": "principal_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "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": { + "principal_key_one_active": { + "name": "principal_key_one_active", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"principal_key\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_key_principal_id_principal_id_fk": { + "name": "principal_key_principal_id_principal_id_fk", + "tableFrom": "principal_key", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "principal_key_public_key_unique": { + "name": "principal_key_public_key_unique", + "nullsNotDistinct": false, + "columns": ["public_key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_role": { + "name": "agent_role", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "agent_role_agent_id_workflow_definition_id_fk": { + "name": "agent_role_agent_id_workflow_definition_id_fk", + "tableFrom": "agent_role", + "tableTo": "workflow_definition", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_role_role_id_role_id_fk": { + "name": "agent_role_role_id_role_id_fk", + "tableFrom": "agent_role", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_role_agent_id_role_id_pk": { + "name": "agent_role_agent_id_role_id_pk", + "columns": ["agent_id", "role_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_role": { + "name": "principal_role", + "schema": "", + "columns": { + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "principal_role_principal_id_principal_id_fk": { + "name": "principal_role_principal_id_principal_id_fk", + "tableFrom": "principal_role", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "principal_role_role_id_role_id_fk": { + "name": "principal_role_role_id_role_id_fk", + "tableFrom": "principal_role", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "principal_role_principal_id_role_id_pk": { + "name": "principal_role_principal_id_role_id_pk", + "columns": ["principal_id", "role_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "role_tenant_id_tenant_id_fk": { + "name": "role_tenant_id_tenant_id_fk", + "tableFrom": "role", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grant": { + "name": "grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "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": { + "grant_invoker_allow_principal_idx": { + "name": "grant_invoker_allow_principal_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"grant\".\"origin\" = 'invoker' and \"grant\".\"effect\" = 'allow'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "grant_tenant_id_tenant_id_fk": { + "name": "grant_tenant_id_tenant_id_fk", + "tableFrom": "grant", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "grant_role_id_role_id_fk": { + "name": "grant_role_id_role_id_fk", + "tableFrom": "grant", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "grant_principal_id_principal_id_fk": { + "name": "grant_principal_id_principal_id_fk", + "tableFrom": "grant", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "grant_target_exactly_one": { + "name": "grant_target_exactly_one", + "value": "num_nonnulls(\"grant\".\"principal_id\", \"grant\".\"role_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.approval": { + "name": "approval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_address": { + "name": "agent_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_definition": { + "name": "tool_definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "tool_arguments": { + "name": "tool_arguments", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "timeout_at": { + "name": "timeout_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "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": { + "approval_tenant_status_idx": { + "name": "approval_tenant_status_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_anchor_run_idx": { + "name": "approval_anchor_run_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_run_idx": { + "name": "approval_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_tenant_id_tenant_id_fk": { + "name": "approval_tenant_id_tenant_id_fk", + "tableFrom": "approval", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "approval_anchor_run_id_workflow_run_id_fk": { + "name": "approval_anchor_run_id_workflow_run_id_fk", + "tableFrom": "approval", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "approval_run_id_workflow_run_id_fk": { + "name": "approval_run_id_workflow_run_id_fk", + "tableFrom": "approval", + "tableTo": "workflow_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "approval_correlation_id_unique": { + "name": "approval_correlation_id_unique", + "nullsNotDistinct": false, + "columns": ["correlation_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signal_correlation": { + "name": "signal_correlation", + "schema": "", + "columns": { + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_address": { + "name": "agent_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_name": { + "name": "signal_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_id": { + "name": "signal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "signal_correlation_tenant_id_tenant_id_fk": { + "name": "signal_correlation_tenant_id_tenant_id_fk", + "tableFrom": "signal_correlation", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "signal_correlation_anchor_run_id_workflow_run_id_fk": { + "name": "signal_correlation_anchor_run_id_workflow_run_id_fk", + "tableFrom": "signal_correlation", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "signal_correlation_run_id_workflow_run_id_fk": { + "name": "signal_correlation_run_id_workflow_run_id_fk", + "tableFrom": "signal_correlation", + "tableTo": "workflow_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider": { + "name": "provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin": { + "name": "plugin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_base_url": { + "name": "api_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_info_url": { + "name": "user_info_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "provider_tenant_id_tenant_id_fk": { + "name": "provider_tenant_id_tenant_id_fk", + "tableFrom": "provider", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_tenant_name": { + "name": "provider_tenant_name", + "nullsNotDistinct": false, + "columns": ["tenant_id", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "default_scopes": { + "name": "default_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "oauth_client_tenant_id_tenant_id_fk": { + "name": "oauth_client_tenant_id_tenant_id_fk", + "tableFrom": "oauth_client", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_client_provider_id_provider_id_fk": { + "name": "oauth_client_provider_id_provider_id_fk", + "tableFrom": "oauth_client", + "tableTo": "provider", + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_tenant_provider": { + "name": "oauth_client_tenant_provider", + "nullsNotDistinct": false, + "columns": ["tenant_id", "provider_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_version": { + "name": "secret_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "refresh_secret": { + "name": "refresh_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "credential_tenant_id_tenant_id_fk": { + "name": "credential_tenant_id_tenant_id_fk", + "tableFrom": "credential", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_principal_id_principal_id_fk": { + "name": "credential_principal_id_principal_id_fk", + "tableFrom": "credential", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "credential_provider_id_provider_id_fk": { + "name": "credential_provider_id_provider_id_fk", + "tableFrom": "credential", + "tableTo": "provider", + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_oauth_client_id_oauth_client_id_fk": { + "name": "credential_oauth_client_id_oauth_client_id_fk", + "tableFrom": "credential", + "tableTo": "oauth_client", + "columnsFrom": ["oauth_client_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_tenant_name": { + "name": "credential_tenant_name", + "nullsNotDistinct": false, + "columns": ["tenant_id", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset": { + "name": "asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_principal_id": { + "name": "creator_principal_id", + "type": "text", + "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": {}, + "foreignKeys": { + "asset_tenant_id_tenant_id_fk": { + "name": "asset_tenant_id_tenant_id_fk", + "tableFrom": "asset", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "asset_creator_principal_id_principal_id_fk": { + "name": "asset_creator_principal_id_principal_id_fk", + "tableFrom": "asset", + "tableTo": "principal", + "columnsFrom": ["creator_principal_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "asset_tenant_kind_name": { + "name": "asset_tenant_kind_name", + "nullsNotDistinct": false, + "columns": ["tenant_id", "kind", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transaction": { + "name": "transaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "wallet_id": { + "name": "wallet_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_id": { + "name": "sender_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "transaction_wallet_id_wallet_id_fk": { + "name": "transaction_wallet_id_wallet_id_fk", + "tableFrom": "transaction", + "tableTo": "wallet", + "columnsFrom": ["wallet_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transaction_run_id_workflow_run_id_fk": { + "name": "transaction_run_id_workflow_run_id_fk", + "tableFrom": "transaction", + "tableTo": "workflow_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wallet": { + "name": "wallet", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backend_type": { + "name": "backend_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "config": { + "name": "config", + "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": {}, + "foreignKeys": { + "wallet_tenant_id_tenant_id_fk": { + "name": "wallet_tenant_id_tenant_id_fk", + "tableFrom": "wallet", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.offering": { + "name": "offering", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing": { + "name": "pricing", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "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": {}, + "foreignKeys": { + "offering_agent_id_workflow_definition_id_fk": { + "name": "offering_agent_id_workflow_definition_id_fk", + "tableFrom": "offering", + "tableTo": "workflow_definition", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "offering_tenant_id_tenant_id_fk": { + "name": "offering_tenant_id_tenant_id_fk", + "tableFrom": "offering", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model": { + "name": "model", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_name": { + "name": "canonical_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_tenant_id_tenant_id_fk": { + "name": "model_tenant_id_tenant_id_fk", + "tableFrom": "model", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_tenant_canonical_name": { + "name": "model_tenant_canonical_name", + "nullsNotDistinct": false, + "columns": ["tenant_id", "canonical_name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_offering": { + "name": "model_offering", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deployment_tags": { + "name": "deployment_tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "capabilities": { + "name": "capabilities", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "quirks": { + "name": "quirks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_offering_tenant_id_tenant_id_fk": { + "name": "model_offering_tenant_id_tenant_id_fk", + "tableFrom": "model_offering", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_offering_model_id_model_id_fk": { + "name": "model_offering_model_id_model_id_fk", + "tableFrom": "model_offering", + "tableTo": "model", + "columnsFrom": ["model_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_offering_provider_id_model_provider_id_fk": { + "name": "model_offering_provider_id_model_provider_id_fk", + "tableFrom": "model_offering", + "tableTo": "model_provider", + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_offering_tenant_model_provider": { + "name": "model_offering_tenant_model_provider", + "nullsNotDistinct": false, + "columns": ["tenant_id", "model_id", "provider_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_pricing": { + "name": "model_pricing", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "offering_id": { + "name": "offering_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_token_price": { + "name": "input_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_token_price": { + "name": "output_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_read_token_price": { + "name": "cache_read_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_write_token_price": { + "name": "cache_write_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thinking_token_price": { + "name": "thinking_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_request_fee": { + "name": "per_request_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_image_fee": { + "name": "per_image_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_audio_fee": { + "name": "per_audio_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effective_from": { + "name": "effective_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "model_pricing_tenant_id_tenant_id_fk": { + "name": "model_pricing_tenant_id_tenant_id_fk", + "tableFrom": "model_pricing", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_pricing_offering_id_model_offering_id_fk": { + "name": "model_pricing_offering_id_model_offering_id_fk", + "tableFrom": "model_pricing", + "tableTo": "model_offering", + "columnsFrom": ["offering_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_pricing_offering_currency_effective_from": { + "name": "model_pricing_offering_currency_effective_from", + "nullsNotDistinct": false, + "columns": ["offering_id", "currency", "effective_from"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_provider": { + "name": "model_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin": { + "name": "plugin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wallet_id": { + "name": "wallet_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_provider_tenant_id_tenant_id_fk": { + "name": "model_provider_tenant_id_tenant_id_fk", + "tableFrom": "model_provider", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_provider_credential_id_credential_id_fk": { + "name": "model_provider_credential_id_credential_id_fk", + "tableFrom": "model_provider", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "model_provider_wallet_id_wallet_id_fk": { + "name": "model_provider_wallet_id_wallet_id_fk", + "tableFrom": "model_provider", + "tableTo": "wallet", + "columnsFrom": ["wallet_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_provider_tenant_name": { + "name": "model_provider_tenant_name", + "nullsNotDistinct": false, + "columns": ["tenant_id", "name"] + } + }, + "policies": {}, + "checkConstraints": { + "model_provider_auth_xor": { + "name": "model_provider_auth_xor", + "value": "(\"model_provider\".\"credential_id\" is not null) <> (\"model_provider\".\"wallet_id\" is not null)" + } + }, + "isRLSEnabled": false + }, + "public.sidecar": { + "name": "sidecar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reconnect_window_ended_at": { + "name": "reconnect_window_ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'online'" + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sidecar_token_hash_sha256_unique": { + "name": "sidecar_token_hash_sha256_unique", + "nullsNotDistinct": false, + "columns": ["token_hash_sha256"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sidecar_allocation": { + "name": "sidecar_allocation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ensure_accepted_generation": { + "name": "ensure_accepted_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconciliation_lease_id": { + "name": "reconciliation_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reconciliation_lease_expires_at": { + "name": "reconciliation_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "initialization_lease_id": { + "name": "initialization_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ensure_attempts": { + "name": "ensure_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "destroy_attempts": { + "name": "destroy_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deployment_cleanup_confirmed": { + "name": "deployment_cleanup_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_hold_released": { + "name": "provider_hold_released", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "connect_deadline": { + "name": "connect_deadline", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "max_disconnected_ms": { + "name": "max_disconnected_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "first_deploy_failed_at": { + "name": "first_deploy_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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": { + "sidecar_allocation_anchor_run_idx": { + "name": "sidecar_allocation_anchor_run_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sidecar_allocation_sidecar_idx": { + "name": "sidecar_allocation_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sidecar_allocation_reconciliation_idx": { + "name": "sidecar_allocation_reconciliation_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing', 'releasing') and \"sidecar_allocation\".\"next_attempt_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "sidecar_allocation_dispatchable_anchor_idx": { + "name": "sidecar_allocation_dispatchable_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sidecar_allocation_anchor_run_id_workflow_run_id_fk": { + "name": "sidecar_allocation_anchor_run_id_workflow_run_id_fk", + "tableFrom": "sidecar_allocation", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sidecar_allocation_tenant_id_tenant_id_fk": { + "name": "sidecar_allocation_tenant_id_tenant_id_fk", + "tableFrom": "sidecar_allocation", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "sidecar_allocation_sidecar_id_sidecar_id_fk": { + "name": "sidecar_allocation_sidecar_id_sidecar_id_fk", + "tableFrom": "sidecar_allocation", + "tableTo": "sidecar", + "columnsFrom": ["sidecar_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "sidecar_allocation_status_check": { + "name": "sidecar_allocation_status_check", + "value": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing', 'releasing', 'destroy_failed', 'released', 'failed')" + }, + "sidecar_allocation_generation_check": { + "name": "sidecar_allocation_generation_check", + "value": "\"sidecar_allocation\".\"generation\" >= 0" + }, + "sidecar_allocation_max_disconnected_check": { + "name": "sidecar_allocation_max_disconnected_check", + "value": "\"sidecar_allocation\".\"max_disconnected_ms\" > 0" + }, + "sidecar_allocation_accepted_generation_check": { + "name": "sidecar_allocation_accepted_generation_check", + "value": "\"sidecar_allocation\".\"ensure_accepted_generation\" is null or \"sidecar_allocation\".\"ensure_accepted_generation\" <= \"sidecar_allocation\".\"generation\"" + }, + "sidecar_allocation_initialization_check": { + "name": "sidecar_allocation_initialization_check", + "value": "\"sidecar_allocation\".\"initialization_lease_id\" is null or (\"sidecar_allocation\".\"status\" = 'allocated' and \"sidecar_allocation\".\"ensure_accepted_generation\" is not null and \"sidecar_allocation\".\"ensure_accepted_generation\" = \"sidecar_allocation\".\"generation\")" + } + }, + "isRLSEnabled": false + }, + "public.agent_session": { + "name": "agent_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_session_tenant_id_tenant_id_fk": { + "name": "agent_session_tenant_id_tenant_id_fk", + "tableFrom": "agent_session", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_session_agent_id_workflow_definition_id_fk": { + "name": "agent_session_agent_id_workflow_definition_id_fk", + "tableFrom": "agent_session", + "tableTo": "workflow_definition", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "agent_session_principal_id_principal_id_fk": { + "name": "agent_session_principal_id_principal_id_fk", + "tableFrom": "agent_session", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_asset": { + "name": "session_asset", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_pack_sha": { + "name": "asset_pack_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "materialized_at": { + "name": "materialized_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_asset_pack_sha_idx": { + "name": "session_asset_pack_sha_idx", + "columns": [ + { + "expression": "asset_pack_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "session_asset_instance_id_mount_path_pk": { + "name": "session_asset_instance_id_mount_path_pk", + "columns": ["instance_id", "mount_path"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inference_turn": { + "name": "inference_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inference_turn_instance_id_started_at_idx": { + "name": "inference_turn_instance_id_started_at_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inference_turn_session_id_agent_session_id_fk": { + "name": "inference_turn_session_id_agent_session_id_fk", + "tableFrom": "inference_turn", + "tableTo": "agent_session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inference_turn_tenant_id_tenant_id_fk": { + "name": "inference_turn_tenant_id_tenant_id_fk", + "tableFrom": "inference_turn", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_mail": { + "name": "session_mail", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "raw": { + "name": "raw", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_mail_instance_id_created_at_idx": { + "name": "session_mail_instance_id_created_at_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_mail_session_id_created_at_idx": { + "name": "session_mail_session_id_created_at_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_mail_session_id_agent_session_id_fk": { + "name": "session_mail_session_id_agent_session_id_fk", + "tableFrom": "session_mail", + "tableTo": "agent_session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_mail_tenant_id_tenant_id_fk": { + "name": "session_mail_tenant_id_tenant_id_fk", + "tableFrom": "session_mail", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.turn_part": { + "name": "turn_part", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "turn_part_turn_id_inference_turn_id_fk": { + "name": "turn_part_turn_id_inference_turn_id_fk", + "tableFrom": "turn_part", + "tableTo": "inference_turn", + "columnsFrom": ["turn_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "turn_part_session_id_agent_session_id_fk": { + "name": "turn_part_session_id_agent_session_id_fk", + "tableFrom": "turn_part", + "tableTo": "agent_session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_token": { + "name": "git_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref_pattern": { + "name": "ref_pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actions": { + "name": "actions", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "git_token_user_id_name_active_idx": { + "name": "git_token_user_id_name_active_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"git_token\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "git_token_tenant_id_tenant_id_fk": { + "name": "git_token_tenant_id_tenant_id_fk", + "tableFrom": "git_token", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "git_token_user_id_user_id_fk": { + "name": "git_token_user_id_user_id_fk", + "tableFrom": "git_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_token_principal_id_principal_id_fk": { + "name": "git_token_principal_id_principal_id_fk", + "tableFrom": "git_token", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_token_token_hash_sha256_unique": { + "name": "git_token_token_hash_sha256_unique", + "nullsNotDistinct": false, + "columns": ["token_hash_sha256"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.run_token": { + "name": "run_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "sealed_token": { + "name": "sealed_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "run_token_principal_consumer_idx": { + "name": "run_token_principal_consumer_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "run_token_principal_id_principal_id_fk": { + "name": "run_token_principal_id_principal_id_fk", + "tableFrom": "run_token", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "run_token_token_hash_sha256_unique": { + "name": "run_token_token_hash_sha256_unique", + "nullsNotDistinct": false, + "columns": ["token_hash_sha256"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_definition": { + "name": "workflow_definition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creator_principal_id": { + "name": "creator_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wire_hash": { + "name": "wire_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_requirements": { + "name": "grant_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "model_requirements": { + "name": "model_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_bindings": { + "name": "credential_bindings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lifecycle_policy": { + "name": "lifecycle_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'deployed'" + }, + "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": { + "workflow_definition_tenant_idx": { + "name": "workflow_definition_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_definition_asset_wire_hash_idx": { + "name": "workflow_definition_asset_wire_hash_idx", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "wire_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_definition_tenant_id_tenant_id_fk": { + "name": "workflow_definition_tenant_id_tenant_id_fk", + "tableFrom": "workflow_definition", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_definition_creator_principal_id_principal_id_fk": { + "name": "workflow_definition_creator_principal_id_principal_id_fk", + "tableFrom": "workflow_definition", + "tableTo": "principal", + "columnsFrom": ["creator_principal_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_definition_asset_id_asset_id_fk": { + "name": "workflow_definition_asset_id_asset_id_fk", + "tableFrom": "workflow_definition", + "tableTo": "asset", + "columnsFrom": ["asset_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_definition_version": { + "name": "workflow_definition_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "approved_wire_hash": { + "name": "approved_wire_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_snapshot": { + "name": "grant_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_definition_version_definition_idx": { + "name": "workflow_definition_version_definition_idx", + "columns": [ + { + "expression": "definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_definition_version_definition_id_workflow_definition_id_fk": { + "name": "workflow_definition_version_definition_id_workflow_definition_id_fk", + "tableFrom": "workflow_definition_version", + "tableTo": "workflow_definition", + "columnsFrom": ["definition_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workflow_definition_version_definition_version": { + "name": "workflow_definition_version_definition_version", + "nullsNotDistinct": false, + "columns": ["definition_id", "version"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run": { + "name": "workflow_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel_id": { + "name": "kernel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_preferences": { + "name": "model_preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_refs": { + "name": "credential_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lifecycle_policy": { + "name": "lifecycle_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "operator_cancellation_requested_at": { + "name": "operator_cancellation_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_deadline": { + "name": "cancellation_deadline", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity_release_at": { + "name": "capacity_release_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "infrastructure_failed_at": { + "name": "infrastructure_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "legacy_mail_accept": { + "name": "legacy_mail_accept", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_definition_idx": { + "name": "workflow_run_definition_idx", + "columns": [ + { + "expression": "definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_address_idx": { + "name": "workflow_run_address_idx", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_run\".\"address\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_anchor_idx": { + "name": "workflow_run_anchor_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run\".\"id\" = \"workflow_run\".\"anchor_run_id\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_live_anchor_idx": { + "name": "workflow_run_live_anchor_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run\".\"id\" = \"workflow_run\".\"anchor_run_id\" and \"workflow_run\".\"status\" in ('deployed', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_infrastructure_failed_idx": { + "name": "workflow_run_infrastructure_failed_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run\".\"infrastructure_failed_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_sidecar_idx": { + "name": "workflow_run_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run\".\"sidecar_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_run_definition_id_workflow_definition_id_fk": { + "name": "workflow_run_definition_id_workflow_definition_id_fk", + "tableFrom": "workflow_run", + "tableTo": "workflow_definition", + "columnsFrom": ["definition_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_tenant_id_tenant_id_fk": { + "name": "workflow_run_tenant_id_tenant_id_fk", + "tableFrom": "workflow_run", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_principal_id_principal_id_fk": { + "name": "workflow_run_principal_id_principal_id_fk", + "tableFrom": "workflow_run", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_run_sidecar_id_sidecar_id_fk": { + "name": "workflow_run_sidecar_id_sidecar_id_fk", + "tableFrom": "workflow_run", + "tableTo": "sidecar", + "columnsFrom": ["sidecar_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_run_legacy_mail_accept_check": { + "name": "workflow_run_legacy_mail_accept_check", + "value": "\"workflow_run\".\"legacy_mail_accept\" is null or \"workflow_run\".\"legacy_mail_accept\" in ('invoker-correspondent', 'correspondent')" + } + }, + "isRLSEnabled": false + }, + "public.workflow_run_dispatch": { + "name": "workflow_run_dispatch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'mail'" + }, + "sender_address": { + "name": "sender_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_message": { + "name": "raw_message", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "step_grants": { + "name": "step_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "acknowledged_generation": { + "name": "acknowledged_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "delivery_lease_id": { + "name": "delivery_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_lease_expires_at": { + "name": "delivery_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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()" + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_dispatch_anchor_message_idx": { + "name": "workflow_run_dispatch_anchor_message_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_dispatch_delivery_idx": { + "name": "workflow_run_dispatch_delivery_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run_dispatch\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_dispatch_anchor_status_idx": { + "name": "workflow_run_dispatch_anchor_status_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_dispatch_unresolved_anchor_idx": { + "name": "workflow_run_dispatch_unresolved_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run_dispatch\".\"status\" in ('pending', 'acknowledged') or (\"workflow_run_dispatch\".\"status\" = 'abandoned' and \"workflow_run_dispatch\".\"next_attempt_at\" is not null)", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_run_dispatch_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_dispatch_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_dispatch", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_run_dispatch_status_check": { + "name": "workflow_run_dispatch_status_check", + "value": "\"workflow_run_dispatch\".\"status\" in ('pending', 'acknowledged', 'abandoned', 'settled', 'failed')" + }, + "workflow_run_dispatch_kind_check": { + "name": "workflow_run_dispatch_kind_check", + "value": "\"workflow_run_dispatch\".\"kind\" in ('mail', 'signal')" + }, + "workflow_run_dispatch_attempt_count_check": { + "name": "workflow_run_dispatch_attempt_count_check", + "value": "\"workflow_run_dispatch\".\"attempt_count\" >= 0" + }, + "workflow_run_dispatch_acknowledged_generation_check": { + "name": "workflow_run_dispatch_acknowledged_generation_check", + "value": "\"workflow_run_dispatch\".\"acknowledged_generation\" is null or \"workflow_run_dispatch\".\"acknowledged_generation\" >= 0" + }, + "workflow_run_dispatch_acknowledged_state_check": { + "name": "workflow_run_dispatch_acknowledged_state_check", + "value": "\"workflow_run_dispatch\".\"status\" <> 'acknowledged' or \"workflow_run_dispatch\".\"acknowledged_generation\" is not null" + }, + "workflow_run_dispatch_pending_schedule_check": { + "name": "workflow_run_dispatch_pending_schedule_check", + "value": "\"workflow_run_dispatch\".\"status\" <> 'pending' or \"workflow_run_dispatch\".\"next_attempt_at\" is not null" + }, + "workflow_run_dispatch_mail_sender_check": { + "name": "workflow_run_dispatch_mail_sender_check", + "value": "\"workflow_run_dispatch\".\"kind\" <> 'mail' or \"workflow_run_dispatch\".\"status\" in ('abandoned', 'settled', 'failed') or \"workflow_run_dispatch\".\"sender_address\" is not null" + } + }, + "isRLSEnabled": false + }, + "public.workflow_pending_projection": { + "name": "workflow_pending_projection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "history_required": { + "name": "history_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "workflow_pending_projection_anchor_idx": { + "name": "workflow_pending_projection_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_pending_projection_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_pending_projection_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_pending_projection", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run_launch_spec": { + "name": "workflow_run_launch_spec", + "schema": "", + "columns": { + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_domain": { + "name": "deployment_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_authority_principal_id": { + "name": "source_authority_principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "frozen_approval_bundle": { + "name": "frozen_approval_bundle", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "definition_artifact_id": { + "name": "definition_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_snapshot": { + "name": "grant_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_offering_ids": { + "name": "source_offering_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "default_source_offering_id": { + "name": "default_source_offering_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_package_pins": { + "name": "tool_package_pins", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_launch_spec_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_launch_spec_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_launch_spec", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_launch_spec_source_authority_principal_id_principal_id_fk": { + "name": "workflow_run_launch_spec_source_authority_principal_id_principal_id_fk", + "tableFrom": "workflow_run_launch_spec", + "tableTo": "principal", + "columnsFrom": ["source_authority_principal_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workflow_run_launch_spec_definition_artifact_id_workflow_definition_artifact_id_fk": { + "name": "workflow_run_launch_spec_definition_artifact_id_workflow_definition_artifact_id_fk", + "tableFrom": "workflow_run_launch_spec", + "tableTo": "workflow_definition_artifact", + "columnsFrom": ["definition_artifact_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run_execution": { + "name": "workflow_run_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_execution_run_id_id_idx": { + "name": "workflow_run_execution_run_id_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_execution_status_idx": { + "name": "workflow_run_execution_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_run_execution_run_id_workflow_run_id_fk": { + "name": "workflow_run_execution_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_execution", + "tableTo": "workflow_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_probe": { + "name": "workflow_probe", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_asset_id": { + "name": "definition_asset_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entry": { + "name": "entry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pin": { + "name": "pin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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": { + "workflow_probe_active_idx": { + "name": "workflow_probe_active_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_probe\".\"status\" in ('pending', 'provisioning', 'probing', 'releasing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_probe_sidecar_idx": { + "name": "workflow_probe_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_probe_tenant_id_tenant_id_fk": { + "name": "workflow_probe_tenant_id_tenant_id_fk", + "tableFrom": "workflow_probe", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workflow_probe_definition_asset_id_asset_id_fk": { + "name": "workflow_probe_definition_asset_id_asset_id_fk", + "tableFrom": "workflow_probe", + "tableTo": "asset", + "columnsFrom": ["definition_asset_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workflow_probe_sidecar_id_sidecar_id_fk": { + "name": "workflow_probe_sidecar_id_sidecar_id_fk", + "tableFrom": "workflow_probe", + "tableTo": "sidecar", + "columnsFrom": ["sidecar_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_probe_status_check": { + "name": "workflow_probe_status_check", + "value": "\"workflow_probe\".\"status\" in ('pending', 'provisioning', 'probing', 'releasing', 'succeeded', 'failed')" + }, + "workflow_probe_succeeded_result_check": { + "name": "workflow_probe_succeeded_result_check", + "value": "\"workflow_probe\".\"status\" <> 'succeeded' or \"workflow_probe\".\"result\" is not null" + }, + "workflow_probe_generation_check": { + "name": "workflow_probe_generation_check", + "value": "\"workflow_probe\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_definition_artifact": { + "name": "workflow_definition_artifact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_definition_artifact_revision_idx": { + "name": "workflow_definition_artifact_revision_idx", + "columns": [ + { + "expression": "revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_definition_artifact_revision_id_workflow_definition_revision_id_fk": { + "name": "workflow_definition_artifact_revision_id_workflow_definition_revision_id_fk", + "tableFrom": "workflow_definition_artifact", + "tableTo": "workflow_definition_revision", + "columnsFrom": ["revision_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_definition_artifact_state_check": { + "name": "workflow_definition_artifact_state_check", + "value": "(\"workflow_definition_artifact\".\"result\" is null and \"workflow_definition_artifact\".\"lease_owner\" is not null and \"workflow_definition_artifact\".\"lease_expires_at\" is not null) or (\"workflow_definition_artifact\".\"result\" is not null and \"workflow_definition_artifact\".\"lease_owner\" is null and \"workflow_definition_artifact\".\"lease_expires_at\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.workflow_definition_revision": { + "name": "workflow_definition_revision", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_commit_sha": { + "name": "asset_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entry": { + "name": "entry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "closure": { + "name": "closure", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_definition_revision_tenant_id_tenant_id_fk": { + "name": "workflow_definition_revision_tenant_id_tenant_id_fk", + "tableFrom": "workflow_definition_revision", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workflow_definition_revision_asset_id_asset_id_fk": { + "name": "workflow_definition_revision_asset_id_asset_id_fk", + "tableFrom": "workflow_definition_revision", + "tableTo": "asset", + "columnsFrom": ["asset_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index e68972bd5..3a6521637 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -827,6 +827,13 @@ "when": 1793729632421, "tag": "0118_drop_launch_spec_deploy_content", "breakpoints": true + }, + { + "idx": 119, + "version": "7", + "when": 1793841949636, + "tag": "0119_principal_key_public_only", + "breakpoints": true } ] } diff --git a/packages/db/src/principal-key-store.ts b/packages/db/src/principal-key-store.ts index 94c3286a5..bca9968ae 100644 --- a/packages/db/src/principal-key-store.ts +++ b/packages/db/src/principal-key-store.ts @@ -23,16 +23,20 @@ export type CreatePrincipalKeyStoreDeps = { /** * Store for the `principal_key` table -- a principal's Ed25519 signing key. * - * The hub custodies the private key: it mints, seals, and signs with it on the - * principal's behalf. A signature therefore ATTRIBUTES an action to a principal - * but is NOT non-repudiable against the hub operator, who holds the key. See - * docs/AUTH.md. + * A key is either hub-custodied or public-only. For a hub-custodied key the hub + * mints, seals, and signs with the private key on the principal's behalf, so a + * signature ATTRIBUTES an action to a principal but is NOT non-repudiable + * against the hub operator, who holds the key. For a public-only key another + * party holds the private key: the hub records the public key it is given and + * refuses to sign. See docs/AUTH.md. * * The store owns the one-active-key-per-principal invariant end to end: `sign` - * and `getPublicKey` resolve the single active row, and `generate` inserts a new - * active key that the table's partial unique index rejects if the principal - * already has one. The private seed never leaves this module -- `sign` decrypts, - * signs, and discards it; no method returns private material. + * and `getPublicKey` resolve the single active row; `generate` and + * `recordPublicKey` insert a new active key, which the table's partial unique + * index rejects if the principal already has one; and `retireActive` retires + * the active key so another can take its place. The private seed never leaves + * this module -- `sign` decrypts, signs, and discards it; no method returns + * private material. */ export function createPrincipalKeyStore({ db, @@ -84,7 +88,8 @@ export function createPrincipalKeyStore({ /** * Sign a message with the principal's active key. Decrypts the sealed seed, * produces the raw 64-byte Ed25519 signature, and discards the seed. Throws - * when the principal has no active key. The seed stays a local that goes out + * when the principal has no active key, or when its active key is + * public-only. The seed stays a local that goes out * of scope; it is never returned or logged. */ async sign( @@ -98,6 +103,11 @@ export function createPrincipalKeyStore({ `principalKeyStore.sign: principal ${principalId} has no active key`, ); } + if (row.privateKey === null) { + throw new Error( + `principalKeyStore.sign: the active key of principal ${principalId} has no private key on the hub`, + ); + } const seedHex = await cipher.decrypt( row.privateKey, principalKeyAad(row.id, "private_key"), @@ -120,6 +130,84 @@ export function createPrincipalKeyStore({ } return row.publicKey; }, + + /** + * Make `publicKey` the principal's active key when another party holds + * the private key. The row holds no private key, so `sign` refuses it. + * + * Recording the public-only key that is already active is a no-op, and + * recording a public-only key the principal held before and retired makes + * that row active again. The call throws when the key belongs to another + * principal, when the key is hub-custodied, or when the principal already + * has a different active key: retire that key first. + * + * The caller serializes writers for this principal. The method reads the + * current rows and then writes, so it takes the transaction that holds the + * caller's lock rather than opening its own. + */ + async recordPublicKey( + principalId: string, + publicKey: string, + tx: DBExecutor, + ): Promise { + const [existing] = await tx + .select() + .from(principalKey) + .where(eq(principalKey.publicKey, publicKey)) + .limit(1); + if (existing !== undefined && existing.principalId !== principalId) { + throw new Error( + `principalKeyStore.recordPublicKey: key ${publicKey} belongs to principal ${existing.principalId}, not ${principalId}`, + ); + } + if (existing !== undefined && existing.privateKey !== null) { + throw new Error( + `principalKeyStore.recordPublicKey: key ${publicKey} of principal ${principalId} is hub-custodied, not public-only`, + ); + } + if (existing?.status === "active") return; + + const active = await loadActive(principalId, tx); + if (active !== undefined) { + throw new Error( + `principalKeyStore.recordPublicKey: principal ${principalId} already has active key ${active.publicKey}; retire it first`, + ); + } + const now = new Date(); + if (existing !== undefined) { + await tx + .update(principalKey) + .set({ status: "active", updatedAt: now }) + .where(eq(principalKey.id, existing.id)); + return; + } + await tx.insert(principalKey).values({ + id: generateId("principalKey"), + principalId, + publicKey, + privateKey: null, + status: "active", + createdAt: now, + updatedAt: now, + }); + }, + + /** + * Retire the principal's active key, so it no longer signs or answers for + * the principal and another key can become active. A principal with no + * active key is left as it is. + */ + async retireActive(principalId: string, tx?: DBExecutor): Promise { + await (tx ?? db) + .update(principalKey) + .set({ status: "retired", updatedAt: new Date() }) + .where( + and( + eq(principalKey.principalId, principalId), + eq(principalKey.status, "active"), + ), + ); + }, }; } diff --git a/packages/db/src/schema/principal-keys.ts b/packages/db/src/schema/principal-keys.ts index c32b4537a..0739b045d 100644 --- a/packages/db/src/schema/principal-keys.ts +++ b/packages/db/src/schema/principal-keys.ts @@ -4,15 +4,18 @@ import { pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core"; import { principal } from "./principals"; // A principal's Ed25519 signing key. `public_key` is the hex-encoded 32-byte -// public point; `private_key` holds the hex-encoded 32-byte seed sealed by the -// credential cipher (`enc:aead:...` ciphertext), so the seed is never at rest in -// plaintext under a configured key. The hub custodies the private key: a -// signature attributes an action to the principal, but is not non-repudiable -// against the hub operator. See docs/AUTH.md. +// public point. When the hub custodies the key, `private_key` holds the +// hex-encoded 32-byte seed sealed by the credential cipher (`enc:aead:...` +// ciphertext), so the seed is never at rest in plaintext under a configured +// key. A null `private_key` means another party holds the private key and the +// hub holds only the public key: the hub can verify the principal's signatures +// but cannot sign as it. A hub-custodied signature attributes an action to the +// principal, but is not non-repudiable against the hub operator. See +// docs/AUTH.md. // // The `status` axis expresses the one-active-key-per-principal invariant, which -// the partial unique index below enforces at the database. No path retires a -// key; every key a principal holds today is "active". +// the partial unique index below enforces at the database. A retired row stays +// for the record; only the active row signs or answers for the principal. export const principalKey = pgTable( "principal_key", { @@ -21,7 +24,7 @@ export const principalKey = pgTable( .notNull() .references(() => principal.id, { onDelete: "cascade" }), publicKey: text("public_key").notNull().unique(), - privateKey: text("private_key").notNull(), + privateKey: text("private_key"), status: text("status", { enum: ["active", "retired"] }).notNull(), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), diff --git a/packages/hub-api/src/routes/workflows.test.ts b/packages/hub-api/src/routes/workflows.test.ts index 2d27d5272..ab22350c0 100644 --- a/packages/hub-api/src/routes/workflows.test.ts +++ b/packages/hub-api/src/routes/workflows.test.ts @@ -661,6 +661,8 @@ function createMockPrincipalKeyStore(): PrincipalKeyStore { // signing a verifiable message is covered by the run-mail-send integration // test. sign: async () => new Uint8Array(64), + recordPublicKey: async () => undefined, + retireActive: async () => undefined, }; } diff --git a/packages/hub-api/src/run-grant-materialization.test.ts b/packages/hub-api/src/run-grant-materialization.test.ts index b762c6920..71e01f3e4 100644 --- a/packages/hub-api/src/run-grant-materialization.test.ts +++ b/packages/hub-api/src/run-grant-materialization.test.ts @@ -35,6 +35,8 @@ const stubPrincipalKeyStore: PrincipalKeyStore = { generate: async () => "pky_stub", sign: async () => new Uint8Array(64), getPublicKey: async () => "pky_stub_public", + recordPublicKey: async () => undefined, + retireActive: async () => undefined, }; // Minting is the run-token store's concern; this suite records which consumers diff --git a/tests/db/principal-key-store.test.ts b/tests/db/principal-key-store.test.ts index c37cd2b87..07d97bf79 100644 --- a/tests/db/principal-key-store.test.ts +++ b/tests/db/principal-key-store.test.ts @@ -27,6 +27,9 @@ import { seedPrincipal, seedTenants } from "@intx/test-harness/seed"; const TENANT = "tnt_pk"; const PRINCIPAL = "prn_pk"; +const OTHER_PRINCIPAL = "prn_pk_other"; +const KEY_A = "aa".repeat(32); +const KEY_B = "bb".repeat(32); // A real env-key cipher, not the noop cipher: the AEAD and its AAD binding must // actually run so a bug in principalKeyAad or the seal/unseal path fails a test // rather than passing silently through an identity cipher. @@ -45,6 +48,13 @@ describe.skipIf(!harnessDbEnvAvailable())( await h.close(); }); + function keyRows(principalId: string) { + return h.db + .select() + .from(principalKey) + .where(eq(principalKey.principalId, principalId)); + } + beforeEach(async () => { await h.reset(); await seedTenants(h.db, [{ id: TENANT }]); @@ -103,6 +113,162 @@ describe.skipIf(!harnessDbEnvAvailable())( ).rejects.toThrow(/no active key/); }); + test("recordPublicKey makes a public-only key active, and sign refuses it", async () => { + const store = createPrincipalKeyStore({ db: h.db, cipher }); + await h.db.transaction((tx) => + store.recordPublicKey(PRINCIPAL, KEY_A, tx), + ); + + expect(await store.getPublicKey(PRINCIPAL)).toBe(KEY_A); + const rows = await keyRows(PRINCIPAL); + expect(rows).toHaveLength(1); + expect(rows[0]?.privateKey).toBeNull(); + await expect( + store.sign(PRINCIPAL, new Uint8Array([1, 2, 3])), + ).rejects.toThrow(/has no private key on the hub/); + }); + + test("recordPublicKey of the active key is a no-op", async () => { + const store = createPrincipalKeyStore({ db: h.db, cipher }); + await h.db.transaction((tx) => + store.recordPublicKey(PRINCIPAL, KEY_A, tx), + ); + const [before] = await keyRows(PRINCIPAL); + + await h.db.transaction((tx) => + store.recordPublicKey(PRINCIPAL, KEY_A, tx), + ); + + expect(await keyRows(PRINCIPAL)).toEqual(before ? [before] : []); + }); + + test("recordPublicKey makes a retired key of the same principal active again", async () => { + const store = createPrincipalKeyStore({ db: h.db, cipher }); + await h.db.transaction((tx) => + store.recordPublicKey(PRINCIPAL, KEY_A, tx), + ); + await store.retireActive(PRINCIPAL); + const retiredAt = new Date("2020-01-01T00:00:00Z"); + await h.db + .update(principalKey) + .set({ updatedAt: retiredAt }) + .where(eq(principalKey.principalId, PRINCIPAL)); + const [retired] = await keyRows(PRINCIPAL); + if (retired === undefined) throw new Error("expected a key row"); + + await h.db.transaction((tx) => + store.recordPublicKey(PRINCIPAL, KEY_A, tx), + ); + + const rows = await keyRows(PRINCIPAL); + expect(rows).toHaveLength(1); + expect(rows[0]?.id).toBe(retired.id); + expect(rows[0]?.status).toBe("active"); + expect(rows[0]?.updatedAt.getTime()).toBeGreaterThan(retiredAt.getTime()); + expect(await store.getPublicKey(PRINCIPAL)).toBe(KEY_A); + }); + + test("recordPublicKey throws when the principal has a different active key", async () => { + const store = createPrincipalKeyStore({ db: h.db, cipher }); + await h.db.transaction((tx) => + store.recordPublicKey(PRINCIPAL, KEY_A, tx), + ); + + await expect( + h.db.transaction((tx) => store.recordPublicKey(PRINCIPAL, KEY_B, tx)), + ).rejects.toThrow(/already has active key .*; retire it first/); + expect(await store.getPublicKey(PRINCIPAL)).toBe(KEY_A); + }); + + test("recordPublicKey of a retired key throws when a different key is active", async () => { + const store = createPrincipalKeyStore({ db: h.db, cipher }); + await h.db.transaction((tx) => + store.recordPublicKey(PRINCIPAL, KEY_A, tx), + ); + await store.retireActive(PRINCIPAL); + await h.db.transaction((tx) => + store.recordPublicKey(PRINCIPAL, KEY_B, tx), + ); + + await expect( + h.db.transaction((tx) => store.recordPublicKey(PRINCIPAL, KEY_A, tx)), + ).rejects.toThrow(/already has active key .*; retire it first/); + expect(await store.getPublicKey(PRINCIPAL)).toBe(KEY_B); + }); + + test("recordPublicKey throws for a key that belongs to another principal", async () => { + const store = createPrincipalKeyStore({ db: h.db, cipher }); + await seedPrincipal(h.db, { id: OTHER_PRINCIPAL, tenantId: TENANT }); + await h.db.transaction((tx) => + store.recordPublicKey(OTHER_PRINCIPAL, KEY_A, tx), + ); + + await expect( + h.db.transaction((tx) => store.recordPublicKey(PRINCIPAL, KEY_A, tx)), + ).rejects.toThrow(/belongs to principal prn_pk_other/); + expect(await keyRows(PRINCIPAL)).toHaveLength(0); + }); + + test("recordPublicKey refuses the key of a hub-custodied row, active or retired", async () => { + const store = createPrincipalKeyStore({ db: h.db, cipher }); + const minted = await store.generate(PRINCIPAL); + + await expect( + h.db.transaction((tx) => store.recordPublicKey(PRINCIPAL, minted, tx)), + ).rejects.toThrow(/is hub-custodied, not public-only/); + + await store.retireActive(PRINCIPAL); + await expect( + h.db.transaction((tx) => store.recordPublicKey(PRINCIPAL, minted, tx)), + ).rejects.toThrow(/is hub-custodied, not public-only/); + await expect(store.getPublicKey(PRINCIPAL)).rejects.toThrow( + /no active key/, + ); + }); + + test("recordPublicKey in a rolled-back transaction leaves no row", async () => { + const store = createPrincipalKeyStore({ db: h.db, cipher }); + const rollback = new Error("roll back"); + + await expect( + h.db.transaction(async (tx) => { + await store.recordPublicKey(PRINCIPAL, KEY_A, tx); + throw rollback; + }), + ).rejects.toBe(rollback); + + expect(await keyRows(PRINCIPAL)).toHaveLength(0); + }); + + test("retireActive leaves a principal with no active key as it is", async () => { + const store = createPrincipalKeyStore({ db: h.db, cipher }); + await store.retireActive(PRINCIPAL); + expect(await keyRows(PRINCIPAL)).toHaveLength(0); + }); + + test("retireActive retires a hub-minted key so a new key can be minted", async () => { + const store = createPrincipalKeyStore({ db: h.db, cipher }); + const first = await store.generate(PRINCIPAL); + + await store.retireActive(PRINCIPAL); + await expect(store.getPublicKey(PRINCIPAL)).rejects.toThrow( + /no active key/, + ); + const second = await store.generate(PRINCIPAL); + + expect(second).not.toBe(first); + expect(await store.getPublicKey(PRINCIPAL)).toBe(second); + const statuses = (await keyRows(PRINCIPAL)) + .map((row) => [row.publicKey, row.status]) + .sort(); + expect(statuses).toEqual( + [ + [first, "retired"], + [second, "active"], + ].sort(), + ); + }); + test("parsePrincipalKeyRow validates the status enum", async () => { const store = createPrincipalKeyStore({ db: h.db, cipher }); await store.generate(PRINCIPAL); diff --git a/tests/db/principal-store.test.ts b/tests/db/principal-store.test.ts index e30db592d..ea93e680c 100644 --- a/tests/db/principal-store.test.ts +++ b/tests/db/principal-store.test.ts @@ -176,6 +176,8 @@ describe.skipIf(!harnessDbEnvAvailable())( }, sign: async () => new Uint8Array(64), getPublicKey: async () => "pky_unused", + recordPublicKey: async () => undefined, + retireActive: async () => undefined, }; const store = createPrincipalStore(h.db, throwingKeyStore); const now = new Date(); From f4720b25098bd0f673a4079d2d754620acd0c244 Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 10:54:05 -0700 Subject: [PATCH 02/14] Skip workflow principals in the principal key backfill --- bin/backfill-principal-keys.ts | 6 ++-- packages/db/src/backfill-principal-keys.ts | 31 ++++++++-------- tests/db/backfill-principal-keys.test.ts | 41 ++++++++++++++++------ 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/bin/backfill-principal-keys.ts b/bin/backfill-principal-keys.ts index 0750225fc..33abd5d6f 100644 --- a/bin/backfill-principal-keys.ts +++ b/bin/backfill-principal-keys.ts @@ -4,9 +4,9 @@ // per-principal-key feature. // // Run ONCE as the last step of the deploy that introduces per-principal keys, -// AFTER the key-minting hub is live. It is safe against a live hub: nothing -// reads principal keys yet, the hub only mints for newly-created principals, -// and the active-key unique index blocks a double key. Idempotent -- a +// AFTER the key-minting hub is live. It is safe against a live hub: the hub +// only mints for newly-created principals, and the active-key unique index +// blocks a double key. It keys only user principals. Idempotent -- a // principal that already holds an active key is skipped -- so it is safe to // re-run or resume after a partial failure. On a database where every principal // was created after the feature landed (e.g. local dev), it does nothing. diff --git a/packages/db/src/backfill-principal-keys.ts b/packages/db/src/backfill-principal-keys.ts index c6e2ef69a..cc42c15fc 100644 --- a/packages/db/src/backfill-principal-keys.ts +++ b/packages/db/src/backfill-principal-keys.ts @@ -3,24 +3,27 @@ // // Minting a key lives only in the principal-creation owner, so every principal // CREATED after that landed already has an active key. This one-shot pass keys -// the pre-existing population so the whole non-agent principal set matches the -// invariant "every non-agent principal has an active key". +// the pre-existing `user` population so it matches the invariant "every user +// principal has an active key". // -// Scope is every `user` and `workflow` principal REGARDLESS of status, not just -// active ones. A status flip (e.g. an invite moving from `invited` to `active`) -// reuses the same principal row and does not re-mint, so an active-only pass -// would leave a pre-existing `invited`/`suspended` principal keyless and turn it -// into a keyless ACTIVE principal the moment it is activated. `agent` is a -// legacy/inert kind that is never keyed at creation either, so it is skipped to -// keep the populations consistent. +// Scope is every `user` principal REGARDLESS of status, not just active ones. A +// status flip (e.g. an invite moving from `invited` to `active`) reuses the same +// principal row and does not re-mint, so an active-only pass would leave a +// pre-existing `invited`/`suspended` principal keyless and turn it into a +// keyless ACTIVE principal the moment it is activated. +// +// `workflow` principals are skipped. The pass counts a principal whose only key +// is retired as keyless, so on a workflow principal it would mint a +// hub-custodied key in place of the one that was retired. `agent` is a +// legacy/inert kind that is never keyed at creation either, so it is skipped +// too. // // Idempotent: a principal that already holds an active key is skipped, so the // pass is safe to re-run or resume after a partial failure. Each principal is // keyed in its own autocommit, so a partial failure leaves the earlier -// principals keyed. Safe to run against a LIVE hub -- nothing reads principal -// keys yet, the hub only mints for newly-created principals (disjoint from this -// pass's pre-existing set), and the `principal_key` active-key unique index -// blocks a double active key. +// principals keyed. Safe to run against a LIVE hub: the hub only mints for +// newly-created principals (disjoint from this pass's pre-existing set), and +// the `principal_key` active-key unique index blocks a double active key. import { and, eq, inArray } from "drizzle-orm"; @@ -29,7 +32,7 @@ import { principal } from "./schema/principals"; import { principalKey } from "./schema/principal-keys"; import type { PrincipalKeyStore } from "./principal-key-store"; -const BACKFILLED_KINDS = ["user", "workflow"] as const; +const BACKFILLED_KINDS = ["user"] as const; export type BackfillPrincipalKeysReport = { /** Principals that were keyless and received a fresh active key. */ diff --git a/tests/db/backfill-principal-keys.test.ts b/tests/db/backfill-principal-keys.test.ts index 0a98be5fd..17be00c26 100644 --- a/tests/db/backfill-principal-keys.test.ts +++ b/tests/db/backfill-principal-keys.test.ts @@ -21,12 +21,15 @@ import { seedPrincipal, seedTenants } from "@intx/test-harness/seed"; const TENANT = "tnt_bf"; const cipher = createTestCredentialCipher(); -// Every non-agent principal is keyless at seed time except `prn_keyed`, which -// is pre-keyed below. Covers both kinds and every status, since the backfill -// keys them all. -const KEYLESS_NON_AGENT = [ +// Every seeded principal is keyless at seed time except `prn_keyed`, which is +// pre-keyed below. The backfill keys user principals of every status. +const KEYLESS_USERS = [ { id: "prn_user_active", kind: "user" as const, status: "active" as const }, { id: "prn_user_invited", kind: "user" as const, status: "invited" as const }, +]; + +// The backfill skips workflow principals of every status. +const KEYLESS_WORKFLOWS = [ { id: "prn_wf_active", kind: "workflow" as const, status: "active" as const }, { id: "prn_wf_suspended", @@ -56,7 +59,7 @@ describe.skipIf(!harnessDbEnvAvailable())( beforeEach(async () => { await h.reset(); await seedTenants(h.db, [{ id: TENANT }]); - for (const p of KEYLESS_NON_AGENT) { + for (const p of [...KEYLESS_USERS, ...KEYLESS_WORKFLOWS]) { await seedPrincipal(h.db, { id: p.id, tenantId: TENANT, @@ -105,21 +108,26 @@ describe.skipIf(!harnessDbEnvAvailable())( return rows.length; } - test("keys every keyless non-agent principal, skips agent and already-keyed", async () => { + test("keys every keyless user principal, skips workflow, agent, and already-keyed", async () => { const store = newStore(); await store.generate("prn_keyed"); const before = await activeKeyRow("prn_keyed"); if (before === undefined) throw new Error("expected a pre-keyed row"); const report = await backfillPrincipalKeys(h.db, store); - expect(report).toEqual({ keysGenerated: 5, alreadyKeyed: 1 }); + expect(report).toEqual({ keysGenerated: 2, alreadyKeyed: 1 }); - // Every keyless non-agent principal now holds exactly one active key. - for (const p of KEYLESS_NON_AGENT) { + // Every keyless user principal now holds exactly one active key. + for (const p of KEYLESS_USERS) { expect(await keyCount(p.id)).toBe(1); expect((await activeKeyRow(p.id))?.status).toBe("active"); } + // The workflow principals were skipped -- no key minted. + for (const p of KEYLESS_WORKFLOWS) { + expect(await keyCount(p.id)).toBe(0); + } + // The agent-kind principal was skipped -- no key minted. expect(await keyCount("prn_agent")).toBe(0); @@ -136,7 +144,20 @@ describe.skipIf(!harnessDbEnvAvailable())( await backfillPrincipalKeys(h.db, store); const rerun = await backfillPrincipalKeys(h.db, store); - expect(rerun).toEqual({ keysGenerated: 0, alreadyKeyed: 6 }); + expect(rerun).toEqual({ keysGenerated: 0, alreadyKeyed: 3 }); + }); + + test("a workflow principal whose only key is retired gets no new key", async () => { + const store = newStore(); + await h.db.transaction((tx) => + store.recordPublicKey("prn_wf_active", "aa".repeat(32), tx), + ); + await store.retireActive("prn_wf_active"); + + await backfillPrincipalKeys(h.db, store); + + expect(await activeKeyRow("prn_wf_active")).toBeUndefined(); + expect(await keyCount("prn_wf_active")).toBe(1); }); }, ); From d01366e0793d0548d6e2a172ca5154622004d173 Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 11:32:33 -0700 Subject: [PATCH 03/14] Mark a run's first trigger by the run row's principal link The anchor lock already serializes first commits, so the principal link the commit writes onto the run row can mark it. A run principal's existence then says nothing about whether the run has committed, and the principal can be created independently of the first trigger. --- packages/db/src/workflow-run-store.ts | 33 +-- packages/hub-api/src/routes/approvals.test.ts | 16 +- packages/hub-api/src/routes/runs.test.ts | 23 +- packages/hub-api/src/routes/workflows.test.ts | 15 +- .../src/run-grant-materialization.test.ts | 25 +- .../hub-api/src/run-grant-materialization.ts | 248 +++++++++++------- packages/hub-api/src/workflow-run-trigger.ts | 6 +- tests/db/deployed-window.test.ts | 6 +- tests/db/mail-triggered-run-grants.test.ts | 137 +++++++++- tests/db/run-grants-loader.test.ts | 80 +++++- tests/db/run-tool-grant-mutation.test.ts | 18 +- tests/db/workflow-run-anchor.test.ts | 12 +- tests/db/workflow-run-terminal-flip.test.ts | 3 +- 13 files changed, 460 insertions(+), 162 deletions(-) diff --git a/packages/db/src/workflow-run-store.ts b/packages/db/src/workflow-run-store.ts index c8b5a7240..642ad03b0 100644 --- a/packages/db/src/workflow-run-store.ts +++ b/packages/db/src/workflow-run-store.ts @@ -53,30 +53,29 @@ export function createWorkflowRunStore(db: DBHandle) { }, /** - * Anchor an externally-triggered run onto its deploy-time anchor row. The - * anchor is created at deploy (born "deployed" with a null principal); the - * first trigger reconciles it here, attaching `principalId` and flipping - * "deployed" -> "running" in one UPDATE. The insert is a conflict-tolerant - * safety net: the anchor provably exists by the time a trigger runs, so it - * normally no-ops. The three-part guard makes the reconcile single-shot and - * safe: `principalId IS NULL` never overwrites a principal a concurrent - * winner already set, and `status = 'deployed'` never resurrects a run a - * concurrent teardown already settled terminal (the null-principal guard - * alone would). `principalId` must be non-null: only the externally- - * triggered path anchors through here, and it always mints one. + * Link an externally-triggered run row to its run principal at the first + * trigger. A deployment's anchor is created at deploy (born "deployed" + * with a null principal); this attaches `principalId` and flips "deployed" + * -> "running" in one UPDATE. A top-level run whose id is not the + * anchor's has no row yet, and the insert creates it already linked. The + * link marks the run's first trigger as committed. The guard makes the + * link single-shot: `principalId IS NULL` never overwrites a principal + * already linked, and `status = 'deployed'` never resurrects a run a + * teardown already settled terminal (the null-principal guard alone + * would). Returns whether this call inserted or linked the row. */ async anchorWithPrincipal( row: WorkflowRunInsert & { principalId: string }, tx?: DBExecutor, - ): Promise { + ): Promise { const executor = tx ?? db; const [inserted] = await executor .insert(workflowRun) .values(row) .onConflictDoNothing({ target: workflowRun.id }) - .returning(); - if (inserted !== undefined) return; - await executor + .returning({ id: workflowRun.id }); + if (inserted !== undefined) return true; + const linked = await executor .update(workflowRun) .set({ principalId: row.principalId, status: "running" }) .where( @@ -85,7 +84,9 @@ export function createWorkflowRunStore(db: DBHandle) { isNull(workflowRun.principalId), eq(workflowRun.status, "deployed"), ), - ); + ) + .returning({ id: workflowRun.id }); + return linked.length > 0; }, /** diff --git a/packages/hub-api/src/routes/approvals.test.ts b/packages/hub-api/src/routes/approvals.test.ts index 820bf78b7..f29f91f73 100644 --- a/packages/hub-api/src/routes/approvals.test.ts +++ b/packages/hub-api/src/routes/approvals.test.ts @@ -9,7 +9,7 @@ import { } from "@intx/types"; import type { GrantRule } from "@intx/types/authz"; import type { ApprovalStore, SignalCorrelationStore, DB } from "@intx/db"; -import { principal, sidecarAllocation, workflowRun } from "@intx/db/schema"; +import { sidecarAllocation, workflowRun } from "@intx/db/schema"; import type { WorkflowRunLifecycle } from "@intx/hub-sessions"; import { createApp } from "../app"; @@ -129,14 +129,14 @@ function createMockDB( }, select: () => ({ from: (table: unknown) => { - if (table === principal) { - // A standing resolution's setRunToolGrantEffect resolves the run - // principal here; this mock seeds none, so the mutation no-ops. - // Its correctness is covered by the setRunToolGrantEffect real-DB - // test. - return { where: () => ({ limit: () => Promise.resolve([]) }) }; - } return { + // A standing resolution's setRunToolGrantEffect resolves the run + // principal linked to the run row here; this mock seeds none, so + // the mutation no-ops. Its correctness is covered by the + // setRunToolGrantEffect real-DB test. + innerJoin: () => ({ + where: () => ({ limit: () => Promise.resolve([]) }), + }), where: () => ({ limit: () => ({ for: async (lock: string) => { diff --git a/packages/hub-api/src/routes/runs.test.ts b/packages/hub-api/src/routes/runs.test.ts index 22ffcc31d..194bb07a3 100644 --- a/packages/hub-api/src/routes/runs.test.ts +++ b/packages/hub-api/src/routes/runs.test.ts @@ -11,7 +11,7 @@ import type { SessionStatus } from "@intx/types"; import type { ConnectorThreadState } from "@intx/types/runtime"; import { createApp } from "../app"; -import { agentSession, grant, principal, workflowRun } from "@intx/db/schema"; +import { agentSession, grant, workflowRun } from "@intx/db/schema"; import { createAgentRepoStore, createSidecarEmitter, @@ -128,8 +128,9 @@ type MockDBOpts = { /** Captured rows passed to db.insert(sessionMail).values(...). */ inserts?: Record[]; /** The run's committed grants the authorization view reads: the run principal - * (a `principal` select) then its grant rows (a `grant` select). Omitted, the - * principal select returns empty and loadCommittedRunGrants yields null. */ + * linked to the run row (a joined `workflowRun` select) then its grant rows (a + * `grant` select). Omitted, the link select returns empty and + * loadCommittedRunGrants yields null. */ committedRunGrants?: { runPrincipalId: string; grantRows: unknown[] }; /** Rows the approvals-list route's `approvalStore.listByRunId` returns * (query.approval.findMany). Defaults to `[]`. */ @@ -154,7 +155,8 @@ function createMockDB(opts: MockDBOpts) { // Builder chain, distinguished by the target table `t`: // - workflowRun: `findRoutableById`'s run query (.where().limit() -> the - // seeded run, or empty when none is seeded). + // seeded run, or empty when none is seeded), and the committed-grants + // link lookup (.innerJoin().where().limit()). // - anything else (sessionMail): the priorMail query used by POST mail // (.where().orderBy().limit()). function selectChain() { @@ -163,17 +165,16 @@ function createMockDB(opts: MockDBOpts) { return { from: (t: unknown) => { if (t === workflowRun) { - return { - where: () => ({ limit: () => Promise.resolve(runRows) }), - }; - } - if (t === principal) { - // loadCommittedRunGrants: the run principal, `.where().limit(1)`. + // loadCommittedRunGrants: the run principal linked to the run row, + // `.innerJoin(principal).where().limit(1)`. const principalRows = opts.committedRunGrants ? [{ id: opts.committedRunGrants.runPrincipalId }] : []; return { - where: () => ({ limit: () => Promise.resolve(principalRows) }), + where: () => ({ limit: () => Promise.resolve(runRows) }), + innerJoin: () => ({ + where: () => ({ limit: () => Promise.resolve(principalRows) }), + }), }; } if (t === grant) { diff --git a/packages/hub-api/src/routes/workflows.test.ts b/packages/hub-api/src/routes/workflows.test.ts index ab22350c0..2e9ae0662 100644 --- a/packages/hub-api/src/routes/workflows.test.ts +++ b/packages/hub-api/src/routes/workflows.test.ts @@ -365,7 +365,7 @@ function createMockDB(opts: MockDBOpts) { // already-materialized guard select resolves through `.limit`, returning // no rows so the mock always exercises the first-commit insert path (a // redelivery no-op is covered against a real database). - const select = () => { + const select = (fields?: Record) => { const from = (table: unknown) => { // The anchor-run existence check selects the run alone; the trigger route // and the deploy readback additionally inner-join the definition to read @@ -374,6 +374,19 @@ function createMockDB(opts: MockDBOpts) { // plain case with the bare id, both keyed on the deployment. let joined = false; const selectedRows = (locked: boolean) => { + // The committed-grants lookup reads the run principal linked to the + // run row, selecting only its id. A run principal the commit inserted + // is linked by the same commit. + if ( + table === workflowRunTable && + joined && + fields !== undefined && + Object.keys(fields).join() === "id" + ) { + return inserts + .filter((record) => record.table === principalTable) + .map((record) => record.values); + } if (table === sidecarAllocationTable) { const status = opts.allocationStatus ?? "allocated"; return opts.allocationId === undefined diff --git a/packages/hub-api/src/run-grant-materialization.test.ts b/packages/hub-api/src/run-grant-materialization.test.ts index 71e01f3e4..b5bc88cb8 100644 --- a/packages/hub-api/src/run-grant-materialization.test.ts +++ b/packages/hub-api/src/run-grant-materialization.test.ts @@ -7,7 +7,6 @@ import { SELF_RUN_RESOURCE, type GrantWalkSnapshot } from "@intx/types"; import { grant as grantTable, type LegacyMailAcceptRule, - principal as principalTable, sidecarAllocation as sidecarAllocationTable, workflowRun as workflowRunTable, workflowRunLaunchSpec as workflowRunLaunchSpecTable, @@ -146,7 +145,20 @@ function mockDb(opts: { */ deployer?: { id: string; status: string } | null; }) { - function rows(table: unknown, joined: boolean): unknown[] { + function rows( + table: unknown, + joined: boolean, + fields: Record | undefined, + ): unknown[] { + // The committed-grants lookup reads the run principal linked to the run + // row, selecting only its id. + const linkLookup = + fields !== undefined && Object.keys(fields).join() === "id"; + if (table === workflowRunTable && joined && linkLookup) { + return opts.committedGrants === undefined + ? [] + : [{ id: opts.committedGrants.runPrincipalId }]; + } if (table === workflowRunTable && opts.deploymentRow) { return joined ? [ @@ -181,13 +193,10 @@ function mockDb(opts: { if (opts.snapshotReads) opts.snapshotReads.count += 1; return [{ snapshot: opts.grantSnapshot }]; } - if (table === principalTable && opts.committedGrants !== undefined) { - return [{ id: opts.committedGrants.runPrincipalId }]; - } if (table === grantTable) return opts.committedGrants?.stepGrants ?? []; return []; } - const select = () => ({ + const select = (fields?: Record) => ({ from: (table: unknown) => { let joined = false; const chain = { @@ -200,7 +209,7 @@ function mockDb(opts: { return chain; }, where: () => ({ - orderBy: () => Promise.resolve(rows(table, joined)), + orderBy: () => Promise.resolve(rows(table, joined, fields)), limit: () => { const selected = table === sidecarAllocationTable @@ -211,7 +220,7 @@ function mockDb(opts: { initializationLeaseId: opts.initializationLeaseId, }, ] - : rows(table, joined); + : rows(table, joined, fields); return Object.assign(Promise.resolve(selected), { for: () => Promise.resolve( diff --git a/packages/hub-api/src/run-grant-materialization.ts b/packages/hub-api/src/run-grant-materialization.ts index 0e3e739e1..1bc70f6fc 100644 --- a/packages/hub-api/src/run-grant-materialization.ts +++ b/packages/hub-api/src/run-grant-materialization.ts @@ -761,8 +761,7 @@ export type CommitRunGrantsArgs = { runTokenStore: RunTokenStore; /** * The consumers that get a hub run token, from the locked anchor's - * `credentialRefs.hubConsumers`. Minted only by the commit that creates - * the run principal. + * `credentialRefs.hubConsumers`. Minted only by the run's first commit. */ hubConsumers: readonly string[]; tenantId: string; @@ -853,28 +852,50 @@ export async function lockDispatchableAllocation( ); } -export async function loadCommittedRunGrantsFromExecutor( +/** + * The run principal a run's first trigger linked to its run row, or null + * before that trigger commits. Only the run's own workflow principal counts: + * the link must name the `kind = 'workflow'` principal whose `refId` is the + * run id. + */ +async function loadLinkedRunPrincipalId( executor: DBExecutor, tenantId: string, runId: string, -): Promise { - const [runPrincipal] = await executor +): Promise { + const [linked] = await executor .select({ id: principalTable.id }) - .from(principalTable) + .from(workflowRun) + .innerJoin(principalTable, eq(principalTable.id, workflowRun.principalId)) .where( and( + eq(workflowRun.id, runId), + eq(workflowRun.tenantId, tenantId), eq(principalTable.tenantId, tenantId), eq(principalTable.kind, "workflow"), eq(principalTable.refId, runId), ), ) .limit(1); - if (runPrincipal === undefined) return null; + return linked?.id ?? null; +} + +export async function loadCommittedRunGrantsFromExecutor( + executor: DBExecutor, + tenantId: string, + runId: string, +): Promise { + const runPrincipalId = await loadLinkedRunPrincipalId( + executor, + tenantId, + runId, + ); + if (runPrincipalId === null) return null; const rows = await executor .select() .from(grantTable) - .where(eq(grantTable.principalId, runPrincipal.id)) + .where(eq(grantTable.principalId, runPrincipalId)) .orderBy(asc(grantTable.id)); const validated = RunGrantsFrame.assert({ type: "run.grants", @@ -893,7 +914,7 @@ export async function loadCommittedRunGrantsFromExecutor( })), }); return { - runPrincipalId: runPrincipal.id, + runPrincipalId, stepGrants: validated.stepGrants, }; } @@ -962,8 +983,9 @@ export function approvalToolName( * the deploy already granted-with-a-checkpoint -- in the direction the operator * chose. Runs against the passed executor, so the caller mutates inside the * resolve transaction and a rolled-back resolve rolls back the grant change with - * it. A run with no principal (nothing to mutate) is a no-op. Returns the run - * principal's id when a row changed, and null otherwise. + * it. A run whose first trigger has not committed (nothing to mutate) is a + * no-op. Returns the run principal's id when a row changed, and null + * otherwise. */ export async function setRunToolGrantEffect( executor: DBExecutor, @@ -972,113 +994,144 @@ export async function setRunToolGrantEffect( toolName: string, effect: "allow" | "deny", ): Promise { - const [runPrincipal] = await executor - .select({ id: principalTable.id }) - .from(principalTable) - .where( - and( - eq(principalTable.tenantId, tenantId), - eq(principalTable.kind, "workflow"), - eq(principalTable.refId, runId), - ), - ) - .limit(1); - if (runPrincipal === undefined) return null; + const runPrincipalId = await loadLinkedRunPrincipalId( + executor, + tenantId, + runId, + ); + if (runPrincipalId === null) return null; const changed = await executor .update(grantTable) .set({ effect, updatedAt: new Date() }) .where( and( - eq(grantTable.principalId, runPrincipal.id), + eq(grantTable.principalId, runPrincipalId), eq(grantTable.resource, `${TOOL_GRANT_PREFIX}${toolName}`), eq(grantTable.effect, "ask"), ), ) .returning({ id: grantTable.id }); - return changed.length > 0 ? runPrincipal.id : null; + return changed.length > 0 ? runPrincipalId : null; } /** - * Idempotently reserve a run's principal, run row, and immutable grant rows - * in one transaction, keyed on the deployment's stable top-level run id. + * Idempotently commit a run's first trigger: its principal, run tokens, + * anchor link, and immutable grant rows, keyed on the deployment's stable + * top-level run id. * - * The transaction that wins the unique principal insert owns the grant - * inserts. A concurrent or later caller returns those exact persisted grants - * instead of sending its independently staged snapshot. This keeps the + * The caller holds the anchor row `FOR UPDATE` on an executable anchor, which + * serializes first commits. The anchor's link to its run principal marks the + * commit: a later caller finds the link and returns the exact persisted + * grants instead of its independently staged snapshot. This keeps the * database and Git authorization views identical when first deliveries race. * - * On the first commit the `runPrincipalId` is derived deterministically - * from `(tenantId, runId)` by the caller, so the principal insert and the - * grant rows that reference it agree on the id even across a retry. + * The `runPrincipalId` is derived deterministically from `(tenantId, runId)` + * by the caller, so the principal and the grant rows that reference it agree + * on the id even across a retry. A run principal that already exists without + * a link is reused only when it has that id, is active, and holds no grants, + * so a commit never links a different or deactivated principal or merges into + * an earlier snapshot. */ export async function commitRunGrants( args: CommitRunGrantsArgs, - tx?: DBExecutor, + tx: DBExecutor, ): Promise { const workflowRunStore = createWorkflowRunStore(args.db); const principalStore = createPrincipalStore(args.db, args.principalKeyStore); - const commit = async ( - executor: DBExecutor, - ): Promise => { - const existing = await loadCommittedRunGrantsFromExecutor( - executor, - args.tenantId, - args.runId, - ); - if (existing !== null) return existing.stepGrants; + const existing = await loadCommittedRunGrantsFromExecutor( + tx, + args.tenantId, + args.runId, + ); + if (existing !== null) return existing.stepGrants; - const insertedPrincipal = await principalStore.createIfAbsent( - { - id: args.runPrincipalId, - tenantId: args.tenantId, - kind: "workflow", - refId: args.runId, - status: "active", - createdAt: args.now, - updatedAt: args.now, - }, - executor, + const insertedPrincipal = await principalStore.createIfAbsent( + { + id: args.runPrincipalId, + tenantId: args.tenantId, + kind: "workflow", + refId: args.runId, + status: "active", + createdAt: args.now, + updatedAt: args.now, + }, + tx, + ); + if (insertedPrincipal === null) { + await assertReusableRunPrincipal(tx, args); + } + + await args.runTokenStore.mintIfAbsent( + { principalId: args.runPrincipalId, consumers: args.hubConsumers }, + tx, + ); + const linked = await workflowRunStore.anchorWithPrincipal( + { + id: args.runId, + anchorRunId: args.anchorRunId, + definitionId: args.definitionId, + tenantId: args.tenantId, + principalId: args.runPrincipalId, + status: "running", + }, + tx, + ); + if (!linked) { + const [anchor] = await tx + .select({ + status: workflowRun.status, + principalId: workflowRun.principalId, + }) + .from(workflowRun) + .where(eq(workflowRun.id, args.runId)) + .limit(1); + throw new Error( + `commitRunGrants: anchor ${args.runId} did not link principal ${args.runPrincipalId} (status ${anchor?.status ?? "absent"}, linked principal ${anchor?.principalId ?? "none"})`, ); - if (insertedPrincipal === null) { - const winner = await loadCommittedRunGrantsFromExecutor( - executor, - args.tenantId, - args.runId, - ); - if (winner === null) { - throw new Error( - `commitRunGrants: principal race for ${args.runId} did not expose the winning grant snapshot`, - ); - } - return winner.stepGrants; - } + } + for (const g of args.grantRows) { + await tx.insert(grantTable).values(g); + } + return [...args.grantRows] + .sort((left, right) => left.id.localeCompare(right.id)) + .map((grant) => runGrantToWire(grant)); +} - await args.runTokenStore.mintIfAbsent( - { principalId: args.runPrincipalId, consumers: args.hubConsumers }, - executor, +async function assertReusableRunPrincipal( + tx: DBExecutor, + args: CommitRunGrantsArgs, +): Promise { + const [existing] = await tx + .select({ id: principalTable.id, status: principalTable.status }) + .from(principalTable) + .where( + and( + eq(principalTable.tenantId, args.tenantId), + eq(principalTable.kind, "workflow"), + eq(principalTable.refId, args.runId), + ), + ) + .limit(1); + if (existing?.id !== args.runPrincipalId) { + throw new Error( + `commitRunGrants: run ${args.runId} has principal ${existing?.id ?? "none"}, not the derived ${args.runPrincipalId}`, ); - await workflowRunStore.anchorWithPrincipal( - { - id: args.runId, - anchorRunId: args.anchorRunId, - definitionId: args.definitionId, - tenantId: args.tenantId, - principalId: args.runPrincipalId, - status: "running", - }, - executor, + } + if (existing.status !== "active") { + throw new Error( + `commitRunGrants: run principal ${args.runPrincipalId} is ${existing.status}, not active`, + ); + } + const [grant] = await tx + .select({ id: grantTable.id }) + .from(grantTable) + .where(eq(grantTable.principalId, args.runPrincipalId)) + .limit(1); + if (grant !== undefined) { + throw new Error( + `commitRunGrants: run principal ${args.runPrincipalId} already holds grants before its first commit`, ); - for (const g of args.grantRows) { - await executor.insert(grantTable).values(g); - } - return [...args.grantRows] - .sort((left, right) => left.id.localeCompare(right.id)) - .map((grant) => runGrantToWire(grant)); - }; - if (tx !== undefined) { - return commit(tx); } - return args.db.transaction(commit); } export type MailTriggeredRunGrantsDeps = { @@ -1281,10 +1334,11 @@ export function createMailTriggeredRunGrantsMaterializer( // `evaluateMailAdmission` filters them to the `mail.accept` namespace, // picks one winning grant across the sender's coordinates, and // default-denies when none match. A `null` sender coordinate set is never - // admitted. The run principal does not exist before this commit, so no - // operator grant can target it yet: first-fire admission is governed by the - // deploy-approved accept surface alone, and an operator deny on the run - // principal binds from the next delivery on. + // admitted. The commit below refuses a run principal that already holds + // grants, so no operator grant on it takes part in the first fire: + // first-fire admission is governed by the deploy-approved accept surface + // alone, and an operator deny on the run principal binds from the next + // delivery on. // // The check runs before the grant requirements resolve, so a refused // sender is refused as `mail_admission_denied` and costs no grant reads. @@ -1399,9 +1453,9 @@ export function createMailTriggeredRunGrantsMaterializer( { tenantId, runPrincipalId, now }, ); if (grantRows === "not-ready") return grantRows; - // `now` stamps both the staged rows and the run principal's creation, - // so an invoker `allow` row expires `INVOKER_GRANT_TTL_MS` after the - // principal is created at the latest. + // `now` stamps the staged rows, and the run principal's creation when + // this commit creates it, so an invoker `allow` row expires + // `INVOKER_GRANT_TTL_MS` after the first commit at the latest. return commitRunGrants( { db: deps.db, diff --git a/packages/hub-api/src/workflow-run-trigger.ts b/packages/hub-api/src/workflow-run-trigger.ts index f80920fd5..951316f2d 100644 --- a/packages/hub-api/src/workflow-run-trigger.ts +++ b/packages/hub-api/src/workflow-run-trigger.ts @@ -491,9 +491,9 @@ export function createWorkflowRunTrigger(deps: TriggerWorkflowRunDeps) { { tenantId: tenant.id, runPrincipalId, now }, ); if (grantRows === "not-ready") return grantRows; - // `now` stamps both the staged rows and the run principal's creation, - // so an invoker `allow` row expires `INVOKER_GRANT_TTL_MS` after the - // principal is created at the latest. + // `now` stamps the staged rows, and the run principal's creation when + // this commit creates it, so an invoker `allow` row expires + // `INVOKER_GRANT_TTL_MS` after the first commit at the latest. return commitRunGrants( { ...grantArgs, diff --git a/tests/db/deployed-window.test.ts b/tests/db/deployed-window.test.ts index f9f91789b..23bd559f5 100644 --- a/tests/db/deployed-window.test.ts +++ b/tests/db/deployed-window.test.ts @@ -95,7 +95,7 @@ describe.skipIf(!harnessDbEnvAvailable())( status: "active", }); const store = createWorkflowRunStore(h.db); - await store.anchorWithPrincipal({ + const linked = await store.anchorWithPrincipal({ id: RUN_ID, anchorRunId: RUN_ID, tenantId: TENANT, @@ -103,6 +103,7 @@ describe.skipIf(!harnessDbEnvAvailable())( principalId: "prn-run", status: "running", }); + expect(linked).toBe(true); const [afterFirst] = await h.db .select() @@ -121,7 +122,7 @@ describe.skipIf(!harnessDbEnvAvailable())( refId: "run-other", status: "active", }); - await store.anchorWithPrincipal({ + const relinked = await store.anchorWithPrincipal({ id: RUN_ID, anchorRunId: RUN_ID, tenantId: TENANT, @@ -129,6 +130,7 @@ describe.skipIf(!harnessDbEnvAvailable())( principalId: "prn-run-2", status: "running", }); + expect(relinked).toBe(false); const [afterSecond] = await h.db .select() diff --git a/tests/db/mail-triggered-run-grants.test.ts b/tests/db/mail-triggered-run-grants.test.ts index 9938c8ca7..7915a229e 100644 --- a/tests/db/mail-triggered-run-grants.test.ts +++ b/tests/db/mail-triggered-run-grants.test.ts @@ -29,6 +29,7 @@ import { createCorrespondentGrantMinter, createMailTriggeredRunGrantsMaterializer, } from "@intx/hub-api"; +import { deriveRunPrincipalId } from "@intx/hub-common"; import { createRunCredentialsLoader } from "@intx/hub-sessions"; import { createTestDb, @@ -1243,9 +1244,10 @@ describe.skipIf(!harnessDbEnvAvailable())( grantRequirements: [], }); // The blocker plays the winner: under the anchor lock it commits the - // run principal with the winner's accept-grant, and it clears the - // anchor's key so credential resolution is not ready afterwards. The - // loser must be gated as a delivery to the live run, not parked. + // run principal with the winner's accept-grant and the run row linked + // to it, and it clears the anchor's key so credential resolution is + // not ready afterwards. The loser must be gated as a delivery to the + // live run, not parked. const locked = Promise.withResolvers(); const release = Promise.withResolvers(); const blocker = h.db.transaction(async (tx) => { @@ -1270,6 +1272,14 @@ describe.skipIf(!harnessDbEnvAvailable())( effect: "allow", origin: "creator", }); + await tx.insert(workflowRun).values({ + id: RUN_ID, + anchorRunId: DEPLOYMENT, + tenantId: TENANT, + definitionId: DEFINITION, + principalId: "prn_committed_run", + status: "running", + }); await tx .update(workflowRun) .set({ publicKey: null }) @@ -1309,6 +1319,127 @@ describe.skipIf(!harnessDbEnvAvailable())( }); }); + describe("a run principal that exists before the first commit", () => { + async function runGrantCount(principalId: string): Promise { + const rows = await h.db + .select({ id: grant.id }) + .from(grant) + .where(eq(grant.principalId, principalId)); + return rows.length; + } + + test("the first commit links and reuses an unlinked principal with the derived id", async () => { + await seedFrozenSnapshot(snapshot("secret:vault")); + const derived = await deriveRunPrincipalId(TENANT, RUN_ID); + await seedPrincipal(h.db, { + id: derived, + tenantId: TENANT, + kind: "workflow", + refId: RUN_ID, + status: "active", + }); + + const first = await materializeOnce(RUN_ID); + const second = await materializeOnce(RUN_ID); + + expect(first.outcome).toBe("materialized"); + expect(second).toEqual(first); + const [run] = await h.db + .select({ principalId: workflowRun.principalId }) + .from(workflowRun) + .where(eq(workflowRun.id, RUN_ID)); + expect(run?.principalId).toBe(derived); + expect(await runGrantCount(derived)).toBeGreaterThan(0); + const principals = await h.db + .select({ id: principal.id }) + .from(principal) + .where(eq(principal.refId, RUN_ID)); + expect(principals).toEqual([{ id: derived }]); + }); + + test("the first commit refuses a run principal whose id is not the derived one", async () => { + await seedFrozenSnapshot(snapshot("secret:vault")); + await seedPrincipal(h.db, { + id: "prn_not_derived", + tenantId: TENANT, + kind: "workflow", + refId: RUN_ID, + status: "active", + }); + + await expect(materializeOnce(RUN_ID)).rejects.toThrow( + /has principal prn_not_derived, not the derived/, + ); + expect(await runGrantCount("prn_not_derived")).toBe(0); + }); + + test("the first commit refuses an unlinked run principal that is not active", async () => { + await seedFrozenSnapshot(snapshot("secret:vault")); + const derived = await deriveRunPrincipalId(TENANT, RUN_ID); + await seedPrincipal(h.db, { + id: derived, + tenantId: TENANT, + kind: "workflow", + refId: RUN_ID, + status: "deactivated", + }); + + await expect(materializeOnce(RUN_ID)).rejects.toThrow( + /is deactivated, not active/, + ); + expect(await runGrantCount(derived)).toBe(0); + }); + + test("the first commit refuses an unlinked run principal that already holds grants", async () => { + await seedFrozenSnapshot(snapshot("secret:vault")); + const derived = await deriveRunPrincipalId(TENANT, RUN_ID); + await seedPrincipal(h.db, { + id: derived, + tenantId: TENANT, + kind: "workflow", + refId: RUN_ID, + status: "active", + }); + await seedGrant(h.db, { + id: "grt_before_commit", + tenantId: TENANT, + principalId: derived, + resource: "tool:write_file", + action: "use", + effect: "allow", + origin: "creator", + }); + + await expect(materializeOnce(RUN_ID)).rejects.toThrow( + /already holds grants before its first commit/, + ); + expect(await runGrantCount(derived)).toBe(1); + }); + + test("the first commit throws when the live run row cannot be linked", async () => { + // A live run row with no principal link that is not a "deployed" + // anchor: the guarded link updates nothing, which no first commit + // under the anchor lock can explain. + await seedFrozenSnapshot(snapshot("secret:vault")); + await h.db.insert(workflowRun).values({ + id: RUN_ID, + tenantId: TENANT, + anchorRunId: DEPLOYMENT, + definitionId: DEFINITION, + status: "running", + }); + + await expect(materializeOnce(RUN_ID)).rejects.toThrow( + /did not link principal .* \(status running, linked principal none\)/, + ); + const principals = await h.db + .select({ id: principal.id }) + .from(principal) + .where(eq(principal.refId, RUN_ID)); + expect(principals).toHaveLength(0); + }); + }); + describe("a delegator change while the first fire waits on the anchor lock", () => { // Hold the anchor lock, start the first fire and wait until it queues // on the lock, then apply `change` in the lock-holding transaction and diff --git a/tests/db/run-grants-loader.test.ts b/tests/db/run-grants-loader.test.ts index 5c29e4609..9e2e1c303 100644 --- a/tests/db/run-grants-loader.test.ts +++ b/tests/db/run-grants-loader.test.ts @@ -42,17 +42,18 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("returns the run principal's committed grants in id order", async () => { - await seedWorkflowRun(h.db, { - id: "run_a", - tenantId: "tnt", - status: "running", - }); await seedPrincipal(h.db, { id: "prn_run_a", tenantId: "tnt", kind: "workflow", refId: "run_a", }); + await seedWorkflowRun(h.db, { + id: "run_a", + tenantId: "tnt", + principalId: "prn_run_a", + status: "running", + }); for (const [id, effect] of [ ["grt_2", "allow"], ["grt_1", "ask"], @@ -85,5 +86,74 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(await load("run_missing")).toBeNull(); expect(await load("run_child")).toBeNull(); }); + + test("returns null for a run row linked to another run's principal", async () => { + await seedPrincipal(h.db, { + id: "prn_other", + tenantId: "tnt", + kind: "workflow", + refId: "run_other", + }); + await seedGrant(h.db, { + id: "grt_other", + tenantId: "tnt", + principalId: "prn_other", + resource: "tool:x", + action: "use", + effect: "allow", + origin: "creator", + }); + await seedWorkflowRun(h.db, { + id: "run_a", + tenantId: "tnt", + principalId: "prn_other", + status: "running", + }); + expect(await createRunGrantsLoader({ db: h.db })("run_a")).toBeNull(); + }); + + test("returns null for a run row linked to a principal that is not a workflow", async () => { + await seedPrincipal(h.db, { + id: "prn_user_a", + tenantId: "tnt", + kind: "user", + refId: "run_a", + }); + await seedGrant(h.db, { + id: "grt_user_a", + tenantId: "tnt", + principalId: "prn_user_a", + resource: "tool:x", + action: "use", + effect: "allow", + origin: "creator", + }); + await seedWorkflowRun(h.db, { + id: "run_a", + tenantId: "tnt", + principalId: "prn_user_a", + status: "running", + }); + expect(await createRunGrantsLoader({ db: h.db })("run_a")).toBeNull(); + }); + + test("returns null for a run whose principal is not linked yet", async () => { + // The run principal exists, but no first trigger has linked it to the + // run row, so the run has committed no grants. + await seedWorkflowRun(h.db, { + id: "run_unfired", + tenantId: "tnt", + status: "deployed", + }); + await seedPrincipal(h.db, { + id: "prn_run_unfired", + tenantId: "tnt", + kind: "workflow", + refId: "run_unfired", + }); + expect( + await createRunGrantsLoader({ db: h.db })("run_unfired"), + ).toBeNull(); + }); }, ); diff --git a/tests/db/run-tool-grant-mutation.test.ts b/tests/db/run-tool-grant-mutation.test.ts index 8efc0bf78..8249dcf02 100644 --- a/tests/db/run-tool-grant-mutation.test.ts +++ b/tests/db/run-tool-grant-mutation.test.ts @@ -27,7 +27,12 @@ import { harnessDbEnvAvailable, type TestDb, } from "@intx/test-harness/db-harness"; -import { seedGrant, seedPrincipal, seedTenants } from "@intx/test-harness/seed"; +import { + seedGrant, + seedPrincipal, + seedTenants, + seedWorkflowRun, +} from "@intx/test-harness/seed"; const TENANT = "tnt_grant_mut"; const RUN_ID = "run_grant_mut"; @@ -49,14 +54,21 @@ describe.skipIf(!harnessDbEnvAvailable())( beforeEach(async () => { await h.reset(); await seedTenants(h.db, [{ id: TENANT }]); - // The run's authorization principal: kind "workflow", refId the runId -- - // exactly what setRunToolGrantEffect resolves the run's grants by. + // The run's authorization principal: kind "workflow", refId the runId, + // linked from the run row the first trigger committed -- exactly what + // setRunToolGrantEffect resolves the run's grants by. await seedPrincipal(h.db, { id: RUN_PRINCIPAL, tenantId: TENANT, kind: "workflow", refId: RUN_ID, }); + await seedWorkflowRun(h.db, { + id: RUN_ID, + tenantId: TENANT, + anchorRunId: RUN_ID, + principalId: RUN_PRINCIPAL, + }); }); async function seedRunGrant( diff --git a/tests/db/workflow-run-anchor.test.ts b/tests/db/workflow-run-anchor.test.ts index 462dbe0c9..a401c864c 100644 --- a/tests/db/workflow-run-anchor.test.ts +++ b/tests/db/workflow-run-anchor.test.ts @@ -72,7 +72,7 @@ describe.skipIf(!harnessDbEnvAvailable())( test("inserts a fresh run row carrying the principal", async () => { const store = createWorkflowRunStore(h.db); - await store.anchorWithPrincipal({ + const linked = await store.anchorWithPrincipal({ id: "run-1", anchorRunId: DEPLOYMENT, tenantId: TENANT, @@ -80,6 +80,7 @@ describe.skipIf(!harnessDbEnvAvailable())( principalId: "prn-run", status: "running", }); + expect(linked).toBe(true); const [row] = await h.db .select() @@ -104,7 +105,7 @@ describe.skipIf(!harnessDbEnvAvailable())( }); const store = createWorkflowRunStore(h.db); - await store.anchorWithPrincipal({ + const linked = await store.anchorWithPrincipal({ id: "run-1", anchorRunId: DEPLOYMENT, tenantId: TENANT, @@ -112,6 +113,7 @@ describe.skipIf(!harnessDbEnvAvailable())( principalId: "prn-run", status: "running", }); + expect(linked).toBe(true); const rows = await h.db .select() @@ -140,7 +142,7 @@ describe.skipIf(!harnessDbEnvAvailable())( }); const store = createWorkflowRunStore(h.db); - await store.anchorWithPrincipal({ + const linked = await store.anchorWithPrincipal({ id: "run-1", anchorRunId: DEPLOYMENT, tenantId: TENANT, @@ -148,6 +150,7 @@ describe.skipIf(!harnessDbEnvAvailable())( principalId: "prn-run", status: "running", }); + expect(linked).toBe(false); const [row] = await h.db .select() @@ -176,7 +179,7 @@ describe.skipIf(!harnessDbEnvAvailable())( }); const store = createWorkflowRunStore(h.db); - await store.anchorWithPrincipal({ + const linked = await store.anchorWithPrincipal({ id: "run-1", anchorRunId: DEPLOYMENT, tenantId: TENANT, @@ -184,6 +187,7 @@ describe.skipIf(!harnessDbEnvAvailable())( principalId: "prn-run", status: "running", }); + expect(linked).toBe(false); const [row] = await h.db .select() diff --git a/tests/db/workflow-run-terminal-flip.test.ts b/tests/db/workflow-run-terminal-flip.test.ts index 96cebc50a..465417b0e 100644 --- a/tests/db/workflow-run-terminal-flip.test.ts +++ b/tests/db/workflow-run-terminal-flip.test.ts @@ -1586,7 +1586,7 @@ describe.skipIf(!harnessDbEnvAvailable())( // A later anchor attempt (the shape a first-fire trigger uses) must not // flip the terminal row back to running. - await store.anchorWithPrincipal({ + const linked = await store.anchorWithPrincipal({ id: "run-terminal-guard", anchorRunId: DEPLOYMENT, definitionId: seededDefinitionId, @@ -1594,6 +1594,7 @@ describe.skipIf(!harnessDbEnvAvailable())( principalId: "prn-terminal-guard", status: "running", }); + expect(linked).toBe(false); // A later createIfAbsent on the same id is a no-op: the row exists. const created = await store.createIfAbsent({ From c763777c1c81ddccc5eaeddf388c380b0644ab74 Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 12:13:25 -0700 Subject: [PATCH 04/14] Deactivate run principals by their run reference A stopped or failed deployment deactivates each run's own principal, found by its run reference rather than by the run row's link, so a run principal that its run has not linked yet is deactivated too. --- packages/db/src/index.ts | 6 +- packages/db/src/principal-store.ts | 33 +++++++ packages/db/src/sidecar-allocation-store.ts | 22 ++--- .../src/workflow-lifecycle-service.ts | 19 ++-- .../src/workflow-run-terminal-projection.ts | 30 ++---- tests/db/principal-store.test.ts | 96 ++++++++++++++++++- tests/db/workflow-run-terminal-flip.test.ts | 44 +++++++-- 7 files changed, 196 insertions(+), 54 deletions(-) diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 36f97528a..bf8147543 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -33,7 +33,11 @@ export { resolveDeploymentLifecyclePolicy, validateLifecyclePolicyEdit, } from "./workflow-lifecycle-policy"; -export { createPrincipalStore, type PrincipalStore } from "./principal-store"; +export { + createPrincipalStore, + deactivateRunPrincipals, + type PrincipalStore, +} from "./principal-store"; export { createPrincipalKeyStore, type CreatePrincipalKeyStoreDeps, diff --git a/packages/db/src/principal-store.ts b/packages/db/src/principal-store.ts index 4879eabf0..6f16adf8f 100644 --- a/packages/db/src/principal-store.ts +++ b/packages/db/src/principal-store.ts @@ -1,7 +1,10 @@ +import { and, eq, inArray, type SQL } from "drizzle-orm"; + import type { DB, DBExecutor } from "./client"; import { principal } from "./schema/principals"; import { parsePrincipalRow } from "./parse-row"; import type { PrincipalKeyStore } from "./principal-key-store"; +import { deleteRunTokensForPrincipals } from "./run-token-store"; type DBHandle = DB["db"]; @@ -88,3 +91,33 @@ export function createPrincipalStore( } export type PrincipalStore = ReturnType; + +/** + * Deactivate the run principals of the given runs and delete their run + * tokens, in the caller's transaction. A run's principal is the + * `kind = 'workflow'` principal whose `refId` is the run id; a run with none + * (an internal, workflow-spawned run) is skipped. + */ +export async function deactivateRunPrincipals( + tx: DBExecutor, + tenantId: string, + runIds: readonly string[], + now: Date | SQL, +): Promise { + if (runIds.length === 0) return; + const deactivated = await tx + .update(principal) + .set({ status: "deactivated", updatedAt: now }) + .where( + and( + eq(principal.tenantId, tenantId), + eq(principal.kind, "workflow"), + inArray(principal.refId, [...runIds]), + ), + ) + .returning({ id: principal.id }); + await deleteRunTokensForPrincipals( + tx, + deactivated.map((row) => row.id), + ); +} diff --git a/packages/db/src/sidecar-allocation-store.ts b/packages/db/src/sidecar-allocation-store.ts index 93540f82b..6a67d69d7 100644 --- a/packages/db/src/sidecar-allocation-store.ts +++ b/packages/db/src/sidecar-allocation-store.ts @@ -20,7 +20,7 @@ import { } from "@intx/types"; import type { DB, DBExecutor } from "./client"; -import { deleteRunTokensForPrincipals } from "./run-token-store"; +import { deactivateRunPrincipals } from "./principal-store"; import { createWorkflowPendingProjectionStore } from "./workflow-pending-projection-store"; import { assertSidecarHasRoom, @@ -38,7 +38,6 @@ import { canExecuteWorkflowRun } from "./workflow-lifecycle-policy"; import { isLiveWorkflowRunStatus, liveWorkflowRunStatuses, - principal, sidecar, sidecarAllocation, workflowRun, @@ -793,16 +792,15 @@ function buildSidecarAllocationStore( inArray(workflowRun.status, [...liveWorkflowRunStatuses]), ), ) - .returning({ id: workflowRun.id, principalId: workflowRun.principalId }); - const principalIds = failedRuns.flatMap(({ principalId }) => - principalId === null ? [] : [principalId], - ); - if (principalIds.length > 0) { - await tx - .update(principal) - .set({ status: "deactivated", updatedAt: now }) - .where(inArray(principal.id, principalIds)); - await deleteRunTokensForPrincipals(tx, principalIds); + .returning({ id: workflowRun.id, tenantId: workflowRun.tenantId }); + const [failedRun] = failedRuns; + if (failedRun !== undefined) { + await deactivateRunPrincipals( + tx, + failedRun.tenantId, + failedRuns.map(({ id }) => id), + now, + ); } return failedRuns.map(({ id }) => id); } diff --git a/packages/hub-sessions/src/workflow-lifecycle-service.ts b/packages/hub-sessions/src/workflow-lifecycle-service.ts index ce5c24b4d..a920e4228 100644 --- a/packages/hub-sessions/src/workflow-lifecycle-service.ts +++ b/packages/hub-sessions/src/workflow-lifecycle-service.ts @@ -16,7 +16,7 @@ import { import type { AnyPgColumn } from "drizzle-orm/pg-core"; import { - deleteRunTokensForPrincipals, + deactivateRunPrincipals, createSidecarAllocationStore, createWorkflowPendingProjectionStore, createWorkflowRunStore, @@ -33,7 +33,6 @@ import { import { liveWorkflowRunStatuses, isLiveWorkflowRunStatus, - principal, sidecarAllocation, workflowPendingProjection, workflowRun, @@ -617,17 +616,13 @@ export function createWorkflowLifecycleService({ inArray(workflowRun.status, [...liveWorkflowRunStatuses]), ), ) - .returning({ principalId: workflowRun.principalId }); - const principalIds = stopped.flatMap((row) => - row.principalId === null ? [] : [row.principalId], + .returning({ id: workflowRun.id }); + await deactivateRunPrincipals( + tx, + run.tenantId, + stopped.map((row) => row.id), + endedAt, ); - if (principalIds.length > 0) { - await tx - .update(principal) - .set({ status: "deactivated", updatedAt: endedAt }) - .where(inArray(principal.id, principalIds)); - await deleteRunTokensForPrincipals(tx, principalIds); - } await dispatches.abandonUnsettled( run.id, "workflow_cancelled", diff --git a/packages/hub-sessions/src/workflow-run-terminal-projection.ts b/packages/hub-sessions/src/workflow-run-terminal-projection.ts index bd580eb51..f3f90ed19 100644 --- a/packages/hub-sessions/src/workflow-run-terminal-projection.ts +++ b/packages/hub-sessions/src/workflow-run-terminal-projection.ts @@ -1,11 +1,11 @@ -import { and, eq } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { - deleteRunTokensForPrincipals, + deactivateRunPrincipals, type DBExecutor, type WorkflowRunStore, } from "@intx/db"; -import { principal, workflowRun } from "@intx/db/schema"; +import { workflowRun } from "@intx/db/schema"; import { getWorkflowRunEndedAt } from "./workflow-run-ended-at"; @@ -92,22 +92,12 @@ export async function projectTerminalRun( // deployment (the guard above), so no live row matched only because the run // is already terminal. Leave its settled status and `endedAt` alone. if (won === null) return "settled"; - // Deactivate the run's own principal, if it has one. Externally-triggered - // runs carry a principal; internal, workflow-spawned runs have - // `principalId = null` and inherit the deployment's grants, so there is - // nothing to deactivate. Deactivation is gated on winning the flip -- the - // single claim point -- not on the principal's own status. - if (won.principalId !== null) { - await tx - .update(principal) - .set({ status: "deactivated", updatedAt: now }) - // `won.principalId` is already this run's own principal and - // `principal.id` is the primary key, so the `refId` match only confirms - // the id we won belongs to this run. - .where( - and(eq(principal.id, won.principalId), eq(principal.refId, runId)), - ); - await deleteRunTokensForPrincipals(tx, [won.principalId]); - } + // Deactivate the run's own principal, found by its run reference, so a + // principal its run has not linked yet is deactivated too. Internal, + // workflow-spawned runs have no principal and inherit the deployment's + // grants, so there is nothing to deactivate. Deactivation is gated on + // winning the flip -- the single claim point -- not on the principal's own + // status. + await deactivateRunPrincipals(tx, anchor.tenantId, [runId], now); return "projected"; } diff --git a/tests/db/principal-store.test.ts b/tests/db/principal-store.test.ts index ea93e680c..9dc9dbfd0 100644 --- a/tests/db/principal-store.test.ts +++ b/tests/db/principal-store.test.ts @@ -11,18 +11,24 @@ import { and, eq } from "drizzle-orm"; import { createPrincipalStore, createPrincipalKeyStore, + deactivateRunPrincipals, type PrincipalKeyStore, } from "@intx/db"; -import { principal, principalKey } from "@intx/db/schema"; +import { principal, principalKey, runToken } from "@intx/db/schema"; import { createTestDb, harnessDbEnvAvailable, type TestDb, } from "@intx/test-harness/db-harness"; import { createTestCredentialCipher } from "@intx/test-harness/crypto"; -import { seedTenants } from "@intx/test-harness/seed"; +import { + seedPrincipal, + seedRunToken, + seedTenants, +} from "@intx/test-harness/seed"; const TENANT = "tnt_ps"; +const OTHER_TENANT = "tnt_ps_other"; const cipher = createTestCredentialCipher(); describe.skipIf(!harnessDbEnvAvailable())( @@ -210,3 +216,89 @@ describe.skipIf(!harnessDbEnvAvailable())( }); }, ); + +describe.skipIf(!harnessDbEnvAvailable())( + "deactivateRunPrincipals (real DB)", + () => { + let h: TestDb; + + beforeAll(async () => { + h = await createTestDb(); + }); + + afterAll(async () => { + await h.close(); + }); + + beforeEach(async () => { + await h.reset(); + await seedTenants(h.db, [{ id: TENANT }, { id: OTHER_TENANT }]); + }); + + async function statusOf(principalId: string): Promise { + const [row] = await h.db + .select({ status: principal.status }) + .from(principal) + .where(eq(principal.id, principalId)); + return row?.status; + } + + async function tokenCount(principalId: string): Promise { + const rows = await h.db + .select({ id: runToken.id }) + .from(runToken) + .where(eq(runToken.principalId, principalId)); + return rows.length; + } + + test("deactivates the run principals of the given runs and deletes their tokens", async () => { + await seedPrincipal(h.db, { + id: "prn_run_a", + tenantId: TENANT, + kind: "workflow", + refId: "run_a", + }); + await seedRunToken(h.db, { id: "rtk_run_a", principalId: "prn_run_a" }); + + // `run_child` has no principal of its own and is skipped. + await deactivateRunPrincipals( + h.db, + TENANT, + ["run_a", "run_child"], + new Date(), + ); + + expect(await statusOf("prn_run_a")).toBe("deactivated"); + expect(await tokenCount("prn_run_a")).toBe(0); + }); + + test("leaves other runs, other tenants, and other kinds alone", async () => { + await seedPrincipal(h.db, { + id: "prn_run_b", + tenantId: TENANT, + kind: "workflow", + refId: "run_b", + }); + await seedPrincipal(h.db, { + id: "prn_other_tenant_a", + tenantId: OTHER_TENANT, + kind: "workflow", + refId: "run_a", + }); + await seedPrincipal(h.db, { + id: "prn_user_a", + tenantId: TENANT, + kind: "user", + refId: "run_a", + }); + await seedRunToken(h.db, { id: "rtk_run_b", principalId: "prn_run_b" }); + + await deactivateRunPrincipals(h.db, TENANT, ["run_a"], new Date()); + + expect(await statusOf("prn_run_b")).toBe("active"); + expect(await tokenCount("prn_run_b")).toBe(1); + expect(await statusOf("prn_other_tenant_a")).toBe("active"); + expect(await statusOf("prn_user_a")).toBe("active"); + }); + }, +); diff --git a/tests/db/workflow-run-terminal-flip.test.ts b/tests/db/workflow-run-terminal-flip.test.ts index 465417b0e..e90f68f81 100644 --- a/tests/db/workflow-run-terminal-flip.test.ts +++ b/tests/db/workflow-run-terminal-flip.test.ts @@ -454,7 +454,7 @@ describe.skipIf(!harnessDbEnvAvailable())( test("deactivates only the run's own principal, not a bystander", async () => { // The run's own principal, plus an unrelated bystander principal on the // same tenant. The flip must deactivate only the run's own principal - // (matched by `won.principalId`), leaving the bystander active. (The + // (matched by its run reference), leaving the bystander active. (The // bystander cannot share the run's refId: the principal table's // (tenantId, kind, refId) uniqueness forbids two workflow principals with // the same refId, so scoping is proven through the id match.) @@ -493,15 +493,47 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(other?.status).toBe("active"); }); + test("deactivates a run principal that its run has not linked yet", async () => { + // The principal deploy creates for a run answers for it before the run's + // first trigger links it, so a run that ends unlinked loses it too. + await seedPrincipal(h.db, { + id: "prn-unlinked", + tenantId: TENANT, + kind: "workflow", + refId: "run-unlinked", + status: "active", + }); + await seedWorkflowRun(h.db, { + id: "run-unlinked", + anchorRunId: DEPLOYMENT, + tenantId: TENANT, + principalId: null, + status: "deployed", + }); + + await buildAndReceive("run-unlinked", "RunFailed"); + + const [run] = await h.db + .select() + .from(workflowRun) + .where(eq(workflowRun.id, "run-unlinked")); + expect(run?.status).toBe("failed"); + const [own] = await h.db + .select() + .from(principal) + .where(eq(principal.id, "prn-unlinked")); + expect(own?.status).toBe("deactivated"); + }); + test("an internal run with no principal is flipped and touches no principal", async () => { - // A second active principal on the tenant that shares the run's id as its - // refId, so a mis-scoped deactivation would visibly hit it. The internal - // run carries principalId = null, so nothing must be deactivated. + // An unrelated active workflow principal on the tenant, so a mis-scoped + // deactivation would visibly hit it. The internal run has no principal + // of its own, so nothing must be deactivated. await seedPrincipal(h.db, { id: "prn-bystander", tenantId: TENANT, kind: "workflow", - refId: "run-internal", + refId: "run-bystander", status: "active", }); await seedWorkflowRun(h.db, { @@ -520,8 +552,6 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(run?.status).toBe("completed"); expect(run?.endedAt).not.toBeNull(); - // The run had no principal of its own; the guarded flip won but the - // principal branch never ran, so the bystander stays active. const [bystander] = await h.db .select() .from(principal) From 999e51a561e7a7adc48d15145f178fde9835e2b3 Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 14:35:38 -0700 Subject: [PATCH 05/14] Create a deployment's run principal at deploy A run principal that exists from deploy gives the run one identity for its whole deployment, ready to hold a key the party running it supplies. Because the principal exists before the first trigger, an operator can act on it then: the first trigger keeps grants given to it, honors a deny among them, and refuses to start a suspended or deactivated one. The invoker grants a first trigger derives still last a day from the principal's creation, which is now the deploy, not the first trigger. --- apps/hub/src/server.ts | 1 - docs/API.md | 4 +- docs/AUTH.md | 14 +- docs/IMPLEMENTATION.md | 2 +- docs/INBOUND_MAIL_POLICY.md | 23 +- docs/WORKFLOW_AUTHORING.md | 6 +- packages/db/src/index.ts | 1 + packages/db/src/principal-store.ts | 76 +++--- packages/db/src/schema/workflow-run.ts | 14 +- packages/db/src/sender-key-resolver.test.ts | 7 +- packages/db/src/sender-key-resolver.ts | 21 +- .../hub-api/src/grant-materialization.test.ts | 25 +- packages/hub-api/src/grant-materialization.ts | 45 ++-- packages/hub-api/src/index.ts | 2 +- packages/hub-api/src/routes/grants.ts | 29 ++- packages/hub-api/src/routes/principals.ts | 2 +- packages/hub-api/src/routes/runs.ts | 2 +- packages/hub-api/src/routes/workflows.test.ts | 9 +- packages/hub-api/src/routes/workflows.ts | 2 +- packages/hub-api/src/run-delegated-grants.ts | 37 +-- .../src/run-grant-materialization.test.ts | 83 +++---- .../hub-api/src/run-grant-materialization.ts | 232 +++++++++++++----- .../hub-api/src/workflow-run-trigger.test.ts | 26 ++ packages/hub-api/src/workflow-run-trigger.ts | 22 +- .../hub-sessions/src/hub-session-lookups.ts | 6 +- .../hub-sessions/src/session-service.test.ts | 38 ++- packages/hub-sessions/src/session-service.ts | 131 ++++++---- .../src/workflow-allocation-service.ts | 16 +- .../hub-sessions/src/ws/sidecar-events.ts | 4 +- tests/db/anchor-before-frame.test.ts | 43 +++- tests/db/mail-accept-backfill.test.ts | 4 - tests/db/mail-triggered-run-grants.test.ts | 220 ++++++++++++++--- tests/db/principal-store.test.ts | 63 ++--- tests/db/sidecar-allocation-store.test.ts | 14 ++ tests/db/workflow-allocation-service.test.ts | 29 +++ tests/hub-api/grant-routes.test.ts | 24 +- tests/hub-api/trigger-delegator-race.test.ts | 90 ++++++- .../invoker-hub-access.test.ts | 30 ++- .../mail-federation-run-completes.test.ts | 36 ++- 39 files changed, 1005 insertions(+), 428 deletions(-) diff --git a/apps/hub/src/server.ts b/apps/hub/src/server.ts index 1b404a0ea..83bba83fd 100644 --- a/apps/hub/src/server.ts +++ b/apps/hub/src/server.ts @@ -306,7 +306,6 @@ export async function createHubServer({ materializeMailTriggeredRunGrants: createMailTriggeredRunGrantsMaterializer( { db, - principalKeyStore, runTokenStore, grantStoreFor: createGrantStore, // The registry createHubApp uses, so a conditioned operator diff --git a/docs/API.md b/docs/API.md index 86c4655cd..7e162a8b9 100644 --- a/docs/API.md +++ b/docs/API.md @@ -585,7 +585,7 @@ Body: SendMessage 202: unknown -- Trigger accepted for delivery 400: ErrorResponse -- Attachment validation error. Each variant carries a structured code (too_many_attachments, oversize_attachment, disallowed_mime_type, invalid_attachment_name, malformed_base64, oversize_total) with the offending index and limits. A malformed request body that fails SendMessage validation returns the generic error shape instead. 404: ErrorResponse -- Run not found -409: ErrorResponse -- Run address is not routable, its allocation is no longer active, the run is stopping or terminal, or (`deployer_unavailable`) the definition has invoker-sourced grant requirements and the deployer is not recorded or no longer active +409: ErrorResponse -- Run address is not routable, its allocation is no longer active, the run is stopping or terminal, (`deployer_unavailable`) the definition has invoker-sourced grant requirements and the deployer is not recorded or no longer active, or (`workflow_run_principal_inactive`) the first trigger finds the run principal suspended or deactivated 413: ErrorResponse -- Request body exceeds the maximum allowed size 503: ErrorResponse -- Run trigger substrate unavailable @@ -712,7 +712,7 @@ Body: SendMessage 202: unknown -- Trigger accepted for delivery 400: ErrorResponse -- Attachment validation error. Each variant carries a structured code (too_many_attachments, oversize_attachment, disallowed_mime_type, invalid_attachment_name, malformed_base64, oversize_total) with the offending index and limits. A malformed request body that fails SendMessage validation returns the generic error shape instead. 404: ErrorResponse -- Workflow deployment not found -409: ErrorResponse -- Deployment address is not routable, its allocation is no longer active, its top-level run is stopping or terminal, or (`deployer_unavailable`) the definition has invoker-sourced grant requirements and the deployer is not recorded or no longer active +409: ErrorResponse -- Deployment address is not routable, its allocation is no longer active, its top-level run is stopping or terminal, (`deployer_unavailable`) the definition has invoker-sourced grant requirements and the deployer is not recorded or no longer active, or (`workflow_run_principal_inactive`) the first trigger finds the run principal suspended or deactivated 413: ErrorResponse -- Request body exceeds the maximum allowed size 503: ErrorResponse -- Durable workflow dispatch unavailable diff --git a/docs/AUTH.md b/docs/AUTH.md index 6a8082ed2..2f38694c2 100644 --- a/docs/AUTH.md +++ b/docs/AUTH.md @@ -66,7 +66,7 @@ The private key seed is sealed at rest with the same AEAD cipher that protects c Hub custody is an **attribution** model, not non-repudiation. A signature with a hub-custodied key proves the action came from a holder of the principal's private key, which is enough to attribute the action to the principal within the system. It is **not** proof against the hub operator, who also holds the key and could sign as the principal. Non-repudiation would require the private key to live somewhere the hub cannot reach, which the hub-custodied model does not provide. -Every principal is minted a signing key when it is created, in the same transaction as the principal row, so a principal never exists without a key. The hub seals the seed under `PRINCIPAL_KEY_ENCRYPTION_KEY` (32 bytes, hex) -- a separate key from `CREDENTIAL_ENCRYPTION_KEY`, so the two rotate independently -- and refuses to start without it. +Every user principal is minted a signing key when it is created, in the same transaction as the principal row, so a user principal never exists without a key. A workflow run principal is different: deploy creates it with no hub-held key, and the run's first trigger links it to the run. The hub seals a hub-custodied key's seed under `PRINCIPAL_KEY_ENCRYPTION_KEY` (32 bytes, hex) -- a separate key from `CREDENTIAL_ENCRYPTION_KEY`, so the two rotate independently -- and refuses to start without it. ### Request resolution flow @@ -103,7 +103,7 @@ Request to /api/tenants/:tenantId/... with Authorization: Bearer itx_run_... A request with no run token, or with a git token, takes the session flow above unchanged. -A consumer opts into hub access by declaring the reserved credential handle `interchange.hub` (`HUB_CREDENTIAL_HANDLE`): a tool in `defineTool({ credentials })`, an action handler in `action({ credentials })`. Deploy records the declaring consumers on the deployment's anchor run, across the root and every nested body. When the hub creates the run principal, in the same transaction, it mints one token for each of them and stamps a `credential:hub:` / `use` grant scoped to that consumer by a `{ tool }` condition. The hub delivers the tokens on the run's `run.grants` barrier, as a credential delivery that binds the reserved handle to an `http` credential pinned to `HUB_API_ORIGIN`. Every barrier ahead of a trigger mail carries them, including a replay after a reconnect, and the reconnect credential reconcile sends them again. The reconcile never revokes them: a run with no tokens may not have minted them yet, and a deleted token already fails at the hub. The sidecar merges them into the supervisor's credential mirror before it writes the grants. It never writes them in the grants file: it keeps them in the mirror, which it seals in the run record (see _Restoring a run's authority in a new process_). A consumer resolves `interchange.hub` to a mediated credential and calls the hub through it, as with any other credential. Mediation is not containment: a consumer can read the header it sends, so a token that leaks works until its run principal is deactivated. +A consumer opts into hub access by declaring the reserved credential handle `interchange.hub` (`HUB_CREDENTIAL_HANDLE`): a tool in `defineTool({ credentials })`, an action handler in `action({ credentials })`. Deploy records the declaring consumers on the deployment's anchor run, across the root and every nested body. When the run's first trigger links the run principal to the run, in the same transaction, the hub mints one token for each of them and stamps a `credential:hub:` / `use` grant scoped to that consumer by a `{ tool }` condition. The hub delivers the tokens on the run's `run.grants` barrier, as a credential delivery that binds the reserved handle to an `http` credential pinned to `HUB_API_ORIGIN`. Every barrier ahead of a trigger mail carries them, including a replay after a reconnect, and the reconnect credential reconcile sends them again. The reconcile never revokes them: a run with no tokens may not have minted them yet, and a deleted token already fails at the hub. The sidecar merges them into the supervisor's credential mirror before it writes the grants. It never writes them in the grants file: it keeps them in the mirror, which it seals in the run record (see _Restoring a run's authority in a new process_). A consumer resolves `interchange.hub` to a mediated credential and calls the hub through it, as with any other credential. Mediation is not containment: a consumer can read the header it sends, so a token that leaks works until its run principal is deactivated. Every path that deactivates a run principal -- run termination, a stop or cancel, a terminal capacity failure, and `PATCH /principals/:principalId` -- deletes its tokens in the same transaction. @@ -156,7 +156,7 @@ Each requirement specifies: The `source` field declares where the delegated authority should come from: - `source: "creator"` — The definition author must delegate this. Resolved at launch against the creator's own grants (identified by `creatorPrincipalId` on the definition). The control plane validates that the creator currently holds the authority being delegated — a creator cannot delegate what they don't have. Materializes as a `grant` with `origin = 'creator'`; an `allow` row expires when the creator's grants stop allowing the requirement (see _Grant Revocation_). This is the setuid model: the definition author's authority travels with the definition. A definition sourced from a package-registry tarball is refused at freeze if it declares any creator requirement: the asset's `creatorPrincipalId` names whoever created the registry asset, not whoever published the tarball into it, and anyone holding `asset:` write can publish, so no publisher provenance exists to delegate from. A definition sourced from a `workflow` asset is not refused. A `workflow` asset is its creator's own codebase, and write on it makes the holder a contributor to that codebase, so it delegates the creator's authority: any principal that holds `asset:` write can change the code that a deploy runs with the creator's grants. Grant that write only to principals the creator trusts with that authority. -- `source: "invoker"` — The invoker must provide this. The invoker of record is the deployment's **deployer**: the principal that deployed it, recorded as the launch spec's `sourceAuthorityPrincipalId`. It is not whoever sends the trigger mail, so the trigger path cannot change whose authority a run delegates, and a run that deploys another workflow is that workflow's invoker. Resolved at launch against the deployer's delegatable grants, on the HTTP trigger and the mail-triggered path alike. Materializes as a `grant` with `origin = 'invoker'`. An `allow` row has an `expires_at` no later than 24 hours after the first trigger (`INVOKER_GRANT_TTL_MS`); a `deny` or `ask` row has none. A deployment with invoker-sourced requirements whose deployer is not recorded or no longer active cannot launch a run: the trigger is refused with 409 `deployer_unavailable`. A deployment that a run deployed can outlive that run, so a later first trigger is refused this way once the run has ended. A deployment without invoker-sourced requirements needs no deployer. The hub reads the deployer's grants at a run's first trigger. After that, the run's invoker rows follow the deployer through re-derivation (see _Grant Revocation_): a row lapses when the deployer loses the grant or is not active. The 24-hour expiry is an upper bound. Nothing renews it, so an invoker `allow` row on a long-lived run lapses 24 hours after its first trigger at the latest. Deploy does not check that the deployer can delegate the invoker requirements; the first trigger refuses a deployer that cannot. +- `source: "invoker"` — The invoker must provide this. The invoker of record is the deployment's **deployer**: the principal that deployed it, recorded as the launch spec's `sourceAuthorityPrincipalId`. It is not whoever sends the trigger mail, so the trigger path cannot change whose authority a run delegates, and a run that deploys another workflow is that workflow's invoker. Resolved at launch against the deployer's delegatable grants, on the HTTP trigger and the mail-triggered path alike. Materializes as a `grant` with `origin = 'invoker'`. An `allow` row has an `expires_at` no later than 24 hours after deploy (`INVOKER_GRANT_TTL_MS`); a `deny` or `ask` row has none. A deployment with invoker-sourced requirements whose deployer is not recorded or no longer active cannot launch a run: the trigger is refused with 409 `deployer_unavailable`. A deployment that a run deployed can outlive that run, so a later first trigger is refused this way once the run has ended. A deployment without invoker-sourced requirements needs no deployer. The hub reads the deployer's grants at a run's first trigger. After that, the run's invoker rows follow the deployer through re-derivation (see _Grant Revocation_): a row lapses when the deployer loses the grant or is not active. The 24-hour expiry is an upper bound. Nothing renews it, so an invoker `allow` row on a long-lived run lapses 24 hours after deploy at the latest. Deploy does not check that the deployer can delegate the invoker requirements; the first trigger refuses a deployer that cannot. A workflow's `mailAccept` `invoker` rule admits mail from the same principal; see INBOUND_MAIL_POLICY.md. @@ -374,13 +374,13 @@ Specificity still decides between grants: a broad conditioned `deny` loses to a Grant revocation is policy-driven with a default of fail-secure. -**Delegated grants follow their delegators**: A run's grant requirements are delegated from its creator (the creator of the definition's asset) or its invoker (the deployment's deployer). At the run's first trigger the hub writes one row on the run principal for each requirement the delegator allows. After that, the hub re-derives those rows whenever a delegator's grants change, so a running run holds what its delegators hold now: +**Delegated grants follow their delegators**: A run's grant requirements are delegated from its creator (the creator of the definition's asset) or its invoker (the deployment's deployer). At the run's first trigger the hub writes one row on the run principal for each requirement the delegator allows. The run principal exists from deploy, so an operator can grant to it before the first trigger; those rows stay, the first trigger sends them with the rows it writes, and an operator `mail.accept` deny among them refuses the first mail. An operator who suspends or deactivates the run principal before the first trigger makes the trigger fail with `workflow_run_principal_inactive`. After that, the hub re-derives those rows whenever a delegator's grants change, so a running run holds what its delegators hold now: - A requirement the delegator no longer allows loses the run's `allow` rows for it. This includes an `ask` that a standing approval changed to `allow`, because that `allow` exceeds what the delegator holds. The run's `deny` and `ask` rows for it stay as floors, with no expiry. -- A requirement the delegator allows again gets its row back. A regained invoker `allow` row ends no later than 24 hours after the first trigger, so once that time has passed the row does not come back. A regained `tool:` `ask` row gets back the latest `always` decision the run's approver made for that tool. +- A requirement the delegator allows again gets its row back. A regained invoker `allow` row ends no later than 24 hours after deploy, so once that time has passed the row does not come back. A regained `tool:` `ask` row gets back the latest `always` decision the run's approver made for that tool. - A row the delegator still allows stays, with its id and any standing decision, and takes the expiry its effect gets from the delegator's grants now (see _Row expiry_). -**Row expiry**: A requirement's `allow` row expires at the first expiry of a delegator grant after which the delegator's grants no longer allow the requirement, or never if no such expiry exists. When the delegator's `allow` grants for the requirement stand alone, that is the latest of their expiries. A specific `allow` that overrides a broader `deny` ends the row when the specific `allow` ends. A role grant with an expiry counts the same as a direct grant. An invoker `allow` row also ends 24 hours after the first trigger at the latest. A `deny` or `ask` row is a floor: it restricts the run, so it has no expiry. After the 24-hour limit an invoker requirement counts as lapsed: its `ask` row stays, and an `allow` row for it is deleted at the next re-derivation. A regained `tool:` row that a standing `always` approval makes `allow` gets the `allow` expiry. A standing (`always`) resolution that changes a requirement row's `ask` to `allow` or `deny` re-derives the run in the same request, so the row gets the expiry of its new effect at once. The hub's REST checks and the run's child both skip a row after its expiry; the child compares the expiry with its own clock. Re-derivation deletes a requirement's expired rows, so a delegator grant renewed after it expired gives the row back. A run that was live before the hub gave delegated rows these expiries keeps its earlier row expiries until its next re-derivation: a creator `allow` row with no expiry, and an invoker row, `allow` or floor, that ends 24 hours after the first trigger. +**Row expiry**: A requirement's `allow` row expires at the first expiry of a delegator grant after which the delegator's grants no longer allow the requirement, or never if no such expiry exists. When the delegator's `allow` grants for the requirement stand alone, that is the latest of their expiries. A specific `allow` that overrides a broader `deny` ends the row when the specific `allow` ends. A role grant with an expiry counts the same as a direct grant. An invoker `allow` row also ends 24 hours after deploy at the latest: the limit counts from the creation of the run principal, which deploy creates. A run whose first trigger comes more than 24 hours after deploy therefore gets no invoker `allow` rows. A `deny` or `ask` row is a floor: it restricts the run, so it has no expiry. After the 24-hour limit an invoker requirement counts as lapsed: its `ask` row stays, and an `allow` row for it is deleted at the next re-derivation. A regained `tool:` row that a standing `always` approval makes `allow` gets the `allow` expiry. A standing (`always`) resolution that changes a requirement row's `ask` to `allow` or `deny` re-derives the run in the same request, so the row gets the expiry of its new effect at once. The hub's REST checks and the run's child both skip a row after its expiry; the child compares the expiry with its own clock. Re-derivation deletes a requirement's expired rows, so a delegator grant renewed after it expired gives the row back. A run that was live before the hub gave delegated rows these expiries keeps its earlier row expiries until its next re-derivation: a creator `allow` row with no expiry, and an invoker row, `allow` or floor, that ends 24 hours after the first trigger. Only the rows that grant requirements produce depend on a delegator. The runtime `tool:` and `effect:` rows, the `mail.accept` rows and the credential-use rows come from the deploy approval, and a delegator change does not touch them. A requirement whose row has the same key as a runtime row is also left alone. @@ -402,7 +402,7 @@ The routes in the table, role delete and member invite take one tenant lock, so Points to know: -- An invoker `allow` row expires 24 hours after the run's first trigger at the latest (`INVOKER_GRANT_TTL_MS`), not when the run stops. Approving a requirement row's `ask` with `always` changes its effect to `allow`, and the row gets the `allow` expiry at once. +- An invoker `allow` row expires 24 hours after deploy at the latest (`INVOKER_GRANT_TTL_MS`), not when the run stops. Approving a requirement row's `ask` with `always` changes its effect to `allow`, and the row gets the `allow` expiry at once. - A creator that is suspended or deactivated still delegates; only the deployer must be active. A status change on a creator changes nothing. A deployer that is not active lapses the run's invoker rows. - A principal that deployed a workflow cannot be deleted (409). Deleting an asset's creator removes the creator link, and the run's creator rows lapse. - An operator `DELETE` of a run's requirement row is undone at the next re-derivation. An operator `PATCH` of its conditions changes its key, so the next re-derivation adds the original row next to it. An operator `PATCH` from `allow` to `deny` stays. It keeps the `allow` row's expiry until the next re-derivation removes it, and it ends at that expiry if no re-derivation comes first. To keep the `deny` past that expiry, set `expiresAt` to `null` in the same `PATCH`. diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 3e9fef1c2..7fda19ac8 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -433,7 +433,7 @@ Mail sent before initialization publishes its signing key is deferred under the Hub-originated triggers have a separate durable replay path in `workflow_run_dispatch`: -- Mail stores the immutable MIME bytes and the stable run's canonical step grants in the same transaction that reserves the run principal/grants. The exact generation receives `run.grants` then `mail.inbound` on one FIFO WebSocket. The hub loads the run's committed grants each time it sends or redelivers the trigger, and the `run.grants` frame carries them. So a decision that commits after the enqueue rides the trigger. A run whose principal is gone gets no grants. When the lookup is not wired or its load faults, the frame carries the grants the trigger last carried: the stored snapshot on a first send, or the last loaded grants on a redelivery. Later trigger occurrences reuse that snapshot as their fallback; a grants-only reservation with no `RunStarted` event is still eligible for its first delivery. +- Mail stores the immutable MIME bytes and the stable run's canonical step grants in the same transaction that commits the run's grants and links its run principal. The exact generation receives `run.grants` then `mail.inbound` on one FIFO WebSocket. The hub loads the run's committed grants each time it sends or redelivers the trigger, and the `run.grants` frame carries them. So a decision that commits after the enqueue rides the trigger. A run with no linked run principal gets no grants. When the lookup is not wired or its load faults, the frame carries the grants the trigger last carried: the stored snapshot on a first send, or the last loaded grants on a redelivery. Later trigger occurrences reuse that snapshot as their fallback; a grants-only reservation with no `RunStarted` event is still eligible for its first delivery. - A sidecar `mail.inbound.ack` proves only that the local inbox accepted the message. The Hub retains it until an accepted workflow-run Git pack shows either the matching `RunStarted` event or a claim-check entry under `addresses/*/consumed/`. An explicit supervisor rejection fails the dispatch instead of settling it. - Signals store a validated `signal.deliver` frame and are redelivered with the same `signalId`. Approval decisions for provisioned deployments enqueue that frame in the same transaction that resolves the approval. There is no sidecar signal ack; the Hub settles the row only after an accepted pack contains the corresponding `SignalReceived` event. - When a generation becomes ready, pending and acknowledged dispatches are requeued. Abandoned attempts remain stopped. Mail claim checks and signal ids make replay idempotent across a worker loss at any point in delivery. diff --git a/docs/INBOUND_MAIL_POLICY.md b/docs/INBOUND_MAIL_POLICY.md index 665569816..721d16d90 100644 --- a/docs/INBOUND_MAIL_POLICY.md +++ b/docs/INBOUND_MAIL_POLICY.md @@ -18,8 +18,9 @@ not receiving mail an author expected is the symptom this document exists for. The signature gate is the second of two gates. Before it, the hub admits or refuses the sender itself against the workflow's `mailAccept` rules. A workflow that declares no `mailAccept` admits a sender only through an -operator grant on the run principal, so it refuses the first mail that fires -it. See _The sender gate runs first_ below. +operator grant on the run principal, and such a grant cannot admit the first +mail that fires the run, so it refuses that mail. See _The sender gate runs +first_ below. ## Declaring a policy @@ -173,9 +174,11 @@ Each relation admits a sender that stands in that relation to the deployment: The default is deny. A workflow that declares no `mailAccept`, or declares no rule that matches the sender, refuses the mail unless an operator grant on the -run principal admits the sender. The first mail that fires a run has no run -principal yet, so no operator grant can admit it. An unknown sender is always -refused. The operator approves the rules at deploy time, as the `mail.accept:` +run principal admits the sender. The run principal exists from deploy, but an +operator grant on it cannot admit the first mail that fires the run: that mail +needs a declared rule. An operator `mail.accept` deny on the run principal +does refuse the first mail. An unknown sender is always refused. The +operator approves the rules at deploy time, as the `mail.accept:` strings of the capability walk. `defineWorkflow` rejects a grant requirement in the `mail.accept` namespace, so `mailAccept` is the only way to author a rule. @@ -261,11 +264,11 @@ spec of its deployment: `mail.accept:tenant:` grant for its own tenant. The backfill adds no invoker row and does not know the parties it mailed, so an existing run admits only senders in its own tenant. -- A deployment that has not fired has no run principal, so it gets only the - legacy rule and no tenant grant. After its first fire, a mail-trigger - deployment admits its invoker and the parties it mails. A sending deployment - admits only the parties it mails, so mail from the HTTP caller that started - the run is refused. Neither admits other senders in its own tenant, so each +- A deployment that had not fired had no run principal when the migration + ran, so it got only the legacy rule and no tenant grant. After its first + fire, a mail-trigger deployment admits its invoker and the parties it + mails. A sending deployment admits only the parties it mails, so mail from + the HTTP caller that started the run is refused. Neither admits other senders in its own tenant, so each admits fewer senders than a deployment that fired before the migration. - A known gap remains. A live run that mailed a party outside its tenant before the migration has no `correspondent` grant for that party, so the reply from diff --git a/docs/WORKFLOW_AUTHORING.md b/docs/WORKFLOW_AUTHORING.md index c4e267136..b2e21f43b 100644 --- a/docs/WORKFLOW_AUTHORING.md +++ b/docs/WORKFLOW_AUTHORING.md @@ -431,9 +431,9 @@ receives only mail the signature gate found nothing to complain about. Before that gate, the hub refuses hub-relayed mail from a sender that no `mailAccept` rule and no operator grant admits. A workflow declaring no `mailAccept` receives hub-relayed mail only when an operator grant on the run -principal admits the sender. That needs a run principal, so the first mail -that fires such a workflow is always refused. Every other message is dropped -before the workflow sees it. The HTTP trigger route is governed by its own +principal admits the sender. Such a grant cannot admit the first mail that +fires the run, so the first mail that fires such a workflow is always +refused. Every other message is dropped before the workflow sees it. The HTTP trigger route is governed by its own authorization, not by this gate. Mail cannot start a deployment that has no mail trigger: the hub refuses the first mail to it. The `mailAccept` rules of such a workflow take effect after its own trigger starts the run. diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index bf8147543..a1269c831 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -36,6 +36,7 @@ export { export { createPrincipalStore, deactivateRunPrincipals, + insertRunPrincipal, type PrincipalStore, } from "./principal-store"; export { diff --git a/packages/db/src/principal-store.ts b/packages/db/src/principal-store.ts index 6f16adf8f..1a92a0844 100644 --- a/packages/db/src/principal-store.ts +++ b/packages/db/src/principal-store.ts @@ -12,10 +12,11 @@ type PrincipalInsert = typeof principal.$inferInsert; type ParsedPrincipal = ReturnType; /** - * Store for the `principal` table -- the single owner of principal creation. - * Every principal insert in the system routes through this one factory, and - * every principal it creates is minted an active signing key in the same - * transaction, so a principal never exists without a key. + * Store for the `principal` table. This module is the single owner of + * principal creation: every principal insert in the system routes through + * this factory or `insertRunPrincipal`. Every principal the factory creates + * is minted an active signing key in the same transaction, so a user + * principal never exists without a key. A run principal gets no hub key. * * Each method accepts an optional transaction handle so the principal row and * its key join the transaction that co-writes the principal's roles and grants. @@ -53,45 +54,44 @@ export function createPrincipalStore( }; return tx === undefined ? db.transaction(run) : run(tx); }, - - /** - * Idempotent variant of `create` for the run path, where concurrent first - * deliveries race to reserve the same principal. On a win it inserts the - * principal, mints its key, and returns the parsed row; on a loss it returns - * `null` WITHOUT minting -- the winning transaction already minted the one - * active key -- so the caller can fall back to the winner's state. - * - * The conflict target is the natural key `(tenantId, kind, refId)`, NOT the - * surrogate `id`: user principals carry a random `generateId("principal")`, - * so their only real conflict key is the natural triple. The run path - * derives a deterministic id that agrees with the triple, so one target - * unifies both callers. Do not switch this to `principal.id`. - */ - async createIfAbsent( - row: PrincipalInsert, - tx?: DBExecutor, - ): Promise { - const run = async ( - executor: DBExecutor, - ): Promise => { - const [inserted] = await executor - .insert(principal) - .values(row) - .onConflictDoNothing({ - target: [principal.tenantId, principal.kind, principal.refId], - }) - .returning(); - if (inserted === undefined) return null; - await principalKeyStore.generate(inserted.id, executor); - return parsePrincipalRow(inserted); - }; - return tx === undefined ? db.transaction(run) : run(tx); - }, }; } export type PrincipalStore = ReturnType; +/** + * Insert the run principal of a run, in the caller's transaction, unless the + * run already has one. A run principal is the `kind = 'workflow'` principal + * whose `refId` is the run id. It gets no hub signing key: the party that + * runs the workflow holds its private key. + * + * The conflict target is the natural key `(tenantId, kind, refId)`, so a + * second insert for the same run inserts nothing. Returns whether this call + * inserted the row; a caller that expects a fresh run decides what an + * existing principal means. + */ +export async function insertRunPrincipal( + tx: DBExecutor, + row: { id: string; tenantId: string; runId: string; createdAt: Date }, +): Promise { + const inserted = await tx + .insert(principal) + .values({ + id: row.id, + tenantId: row.tenantId, + kind: "workflow", + refId: row.runId, + status: "active", + createdAt: row.createdAt, + updatedAt: row.createdAt, + }) + .onConflictDoNothing({ + target: [principal.tenantId, principal.kind, principal.refId], + }) + .returning({ id: principal.id }); + return inserted.length > 0; +} + /** * Deactivate the run principals of the given runs and delete their run * tokens, in the caller's transaction. A run's principal is the diff --git a/packages/db/src/schema/workflow-run.ts b/packages/db/src/schema/workflow-run.ts index 57bfd4744..62ad8ccce 100644 --- a/packages/db/src/schema/workflow-run.ts +++ b/packages/db/src/schema/workflow-run.ts @@ -87,12 +87,14 @@ export const workflowRun = pgTable( tenantId: text("tenant_id") .notNull() .references(() => tenant.id, { onDelete: "cascade" }), - // Nullable by design: an internal, workflow-spawned run has no principal of - // its own -- it inherits the deployment's grants at read time -- so null is - // the honest signal for "this run has no own principal." `onDelete: "set - // null"` is deliberate (unlike agent_instance's principal, which uses the - // implicit default): deleting a run's principal nulls the column rather than - // wedging the run row. + // The link to the run's own principal, written by its first grant commit. + // Null on a deployment anchor whose first trigger has not committed -- its + // run principal already exists from deploy, found by `refId` -- and on an + // internal, workflow-spawned run, which has no principal of its own and + // inherits the deployment's grants at read time. Find a run's principal by + // `refId`, not by this link. `onDelete: "set null"` is deliberate (unlike + // agent_instance's principal, which uses the implicit default): deleting a + // run's principal nulls the column rather than wedging the run row. principalId: text("principal_id").references(() => principal.id, { onDelete: "set null", }), diff --git a/packages/db/src/sender-key-resolver.test.ts b/packages/db/src/sender-key-resolver.test.ts index b13b93e23..ad017e3c2 100644 --- a/packages/db/src/sender-key-resolver.test.ts +++ b/packages/db/src/sender-key-resolver.test.ts @@ -54,9 +54,10 @@ describe("senderCoordinates", () => { }); test("a run sender with no principal fails closed to null", () => { - // An acked anchor holds a signing key before its first trigger mints its - // principal. It can sign, but has no principal to key admission on, so the - // coordinate set is null (the gate treats it as the unknown sender). + // An acked anchor holds a signing key before its first trigger links its + // run principal. It can sign, but has no linked principal to key admission + // on, so the coordinate set is null (the gate treats it as the unknown + // sender). const resolution: SenderKeyResolution = { source: "run", publicKey: "0xrunkey", diff --git a/packages/db/src/sender-key-resolver.ts b/packages/db/src/sender-key-resolver.ts index 48b9e4425..fcb03dc0b 100644 --- a/packages/db/src/sender-key-resolver.ts +++ b/packages/db/src/sender-key-resolver.ts @@ -32,9 +32,9 @@ const logger = getLogger(["db", "sender-key-resolver"]); * with no translation. * * A run's `principalId` is nullable: a deployment anchor holds a signing key - * (recorded at deploy ack) before its first trigger mints and reconciles its - * principal, so a run that CAN sign mail may not yet carry a principal. The key - * still resolves for signature checks; `senderCoordinates` returns `null` for + * (recorded at deploy ack) before its first trigger links its run principal + * to the anchor, so a run that CAN sign mail may not yet carry a principal. + * The key still resolves for signature checks; `senderCoordinates` returns `null` for * such a sender, which the admission gate treats as the `unknown` outcome. A * user sender always resolves to a concrete principal, so its `principalId` is * never null. @@ -158,8 +158,8 @@ export async function resolveSenderKey( .limit(1); if (row === undefined || row.publicKey === null) return null; // `principalId` may be null: an acked anchor holds a signing key before its - // first trigger mints and reconciles its principal, so it CAN sign yet has - // no principal. The key still resolves (the send path is unaffected); + // first trigger links its run principal, so it CAN sign yet has no linked + // principal. The key still resolves (the send path is unaffected); // `senderCoordinates` fails closed on the null principal for admission. // `tenantId` is a non-null column. `assetId` is null when the definition // has no asset. @@ -176,10 +176,11 @@ export async function resolveSenderKey( // `(tenant domain, user refId)` its address carries. const resolved = await resolveUserPrincipal(db, domain, localPart); if (resolved === null) return null; - // The principal exists; a principal with no active key violates the invariant - // that every principal is minted with one, so `getPublicKey` throws rather - // than defaulting. Do not soften that to null -- unlike the pre-ack run key - // above, a keyless principal is a real breakage that must surface. + // The principal exists; a user principal with no active key violates the + // invariant that every user principal is minted with one, so `getPublicKey` + // throws rather than defaulting. Do not soften that to null -- unlike the + // pre-ack run key above, a keyless principal is a real breakage that must + // surface. const publicKey = await principalKeyStore.getPublicKey( resolved.principalId, db, @@ -211,7 +212,7 @@ export type ResolvedSenderPrincipal = { * * Returns `null` when there is no durable identity to bind: an unresolvable * address, or a principal-less run (an acked anchor before its first trigger - * mints a principal, for which `senderCoordinates` returns `null`). A `null` + * links its principal, for which `senderCoordinates` returns `null`). A `null` * leaves the sender unadmitted rather than admitting a partial identity. The * strict resolver's throw on an ambiguous address or a keyless-principal * invariant break is preserved for the caller to catch, so a fault never diff --git a/packages/hub-api/src/grant-materialization.test.ts b/packages/hub-api/src/grant-materialization.test.ts index ef54386cc..3f193e357 100644 --- a/packages/hub-api/src/grant-materialization.test.ts +++ b/packages/hub-api/src/grant-materialization.test.ts @@ -29,10 +29,13 @@ function first(rows: readonly T[]): T { const NOW = new Date("2026-07-21T00:00:00.000Z"); +const INVOKER_EXPIRES_AT = new Date(NOW.getTime() + INVOKER_GRANT_TTL_MS); + const base = { tenantId: "t_1", targetPrincipalId: "p_target", now: NOW, + invokerExpiresAt: INVOKER_EXPIRES_AT, }; describe("resolveGrantMaterialization", () => { @@ -76,7 +79,27 @@ describe("resolveGrantMaterialization", () => { if (!res.ok) return; const r = first(res.grantRows); expect(r.origin).toBe("invoker"); - expect(r.expiresAt).toEqual(new Date(NOW.getTime() + INVOKER_GRANT_TTL_MS)); + expect(r.expiresAt).toEqual(INVOKER_EXPIRES_AT); + }); + + test("an invoker allow row whose expiry has passed is left out", async () => { + const res = await resolveGrantMaterialization({ + ...base, + invokerExpiresAt: NOW, + grantRequirements: [ + { resource: "wallet:w1", action: "spend", source: "invoker" }, + { resource: "wallet:w2", action: "spend", source: "creator" }, + ], + invokerGrants: [ + grant({ resource: "wallet:w1", action: "spend", origin: "role" }), + ], + creatorGrants: [ + grant({ resource: "wallet:w2", action: "spend", origin: "creator" }), + ], + }); + expect(res.ok).toBe(true); + if (!res.ok) return; + expect(res.grantRows.map((row) => row.resource)).toEqual(["wallet:w2"]); }); test("invoker-origin grants are NOT delegatable (filtered out before resolution)", async () => { diff --git a/packages/hub-api/src/grant-materialization.ts b/packages/hub-api/src/grant-materialization.ts index 38eb2e045..26127325d 100644 --- a/packages/hub-api/src/grant-materialization.ts +++ b/packages/hub-api/src/grant-materialization.ts @@ -61,17 +61,23 @@ export type ResolveGrantMaterializationArgs = { // The creator's collected grants. Used to authorize creator-sourced // requirements. Empty when the definition declares no creator requirements. creatorGrants: GrantRule[]; - // Timestamp stamped onto materialized rows and used to compute the invoker - // grant expiry. + // Timestamp stamped onto materialized rows. A row that has expired by `now` + // is not materialized. now: Date; + // The latest expiry of an invoker-sourced `allow` row. + invokerExpiresAt: Date; }; // A rejection carries the wire error shape and HTTP status the caller returns -// verbatim. `insufficient_grants` is a 403; `not_launchable` and -// `deployer_unavailable` are 409s. +// verbatim. `insufficient_grants` is a 403; `not_launchable`, +// `deployer_unavailable`, and `workflow_run_principal_inactive` are 409s. export type GrantMaterializationRejection = { status: 403 | 409; - code: "insufficient_grants" | "not_launchable" | "deployer_unavailable"; + code: + | "insufficient_grants" + | "not_launchable" + | "deployer_unavailable" + | "workflow_run_principal_inactive"; message: string; }; @@ -80,17 +86,14 @@ export type ResolveGrantMaterializationResult = | { ok: false; rejection: GrantMaterializationRejection }; // Invoker-delegated `allow` rows are short-lived: they expire a day after the -// run's first trigger at the latest, and nothing renews them. An invoker -// `deny` or `ask` row is a floor and does not expire. +// run principal is created, at deploy, at the latest, and nothing renews them. +// An invoker `deny` or `ask` row is a floor and does not expire. export const INVOKER_GRANT_TTL_MS = 24 * 60 * 60 * 1000; export type ResolveRequirementGrantContext = Omit< ResolveGrantMaterializationArgs, "grantRequirements" -> & { - // The latest expiry of an invoker-sourced `allow` row. - invokerExpiresAt: Date; -}; +>; /** * The expiry of a requirement's row with `effect`. An `allow` row ends at @@ -238,30 +241,20 @@ export async function resolveRequirementGrant( * * Decides each requirement with `resolveRequirementGrant` and stops at the * first rejection, so the caller writes every row or none. An invoker-sourced - * `allow` row expires `INVOKER_GRANT_TTL_MS` after `now` at the latest. + * `allow` row expires at `invokerExpiresAt` at the latest, and a row that has + * already expired by `now` is left out: it would grant nothing. * Nothing is written to the database; the caller inserts the returned rows. */ export async function resolveGrantMaterialization({ - tenantId, - targetPrincipalId, grantRequirements, - invokerGrants, - creatorGrants, - now, + ...ctx }: ResolveGrantMaterializationArgs): Promise { - const ctx: ResolveRequirementGrantContext = { - tenantId, - targetPrincipalId, - creatorGrants, - invokerGrants, - now, - invokerExpiresAt: new Date(now.getTime() + INVOKER_GRANT_TTL_MS), - }; - const grantRows: MaterializedGrantRow[] = []; for (const req of grantRequirements) { const resolved = await resolveRequirementGrant(req, ctx); if (!resolved.ok) return resolved; + const { expiresAt } = resolved.row; + if (expiresAt !== null && expiresAt <= ctx.now) continue; grantRows.push(resolved.row); } diff --git a/packages/hub-api/src/index.ts b/packages/hub-api/src/index.ts index ce43b4c82..4947150d4 100644 --- a/packages/hub-api/src/index.ts +++ b/packages/hub-api/src/index.ts @@ -30,13 +30,13 @@ export { createMailTriggeredRunGrantsMaterializer, createCorrespondentGrantMinter, createRunGrantsLoader, + lockTenantRunDelegatedGrants, setRunToolGrantEffect, type MailTriggeredRunGrantsDeps, type CorrespondentGrantMinterDeps, } from "./run-grant-materialization"; export { findDelegatedAnchorRunIds, - lockTenantRunDelegatedGrants, rederiveRunDelegatedGrants, sweepLapsedDeployerGrants, type RederiveRunDelegatedGrantsArgs, diff --git a/packages/hub-api/src/routes/grants.ts b/packages/hub-api/src/routes/grants.ts index e5d76c1e0..8f1e39956 100644 --- a/packages/hub-api/src/routes/grants.ts +++ b/packages/hub-api/src/routes/grants.ts @@ -143,19 +143,32 @@ const CONDITIONS_400_DESCRIPTION = "Validation error. A body that fails its schema gets the body validator's error envelope. A body whose `conditions` names an unregistered condition, holds a malformed condition body, or, on a workflow run's principal, holds a condition the run cannot evaluate gets an `ErrorResponse` whose message lists each problem as a JSON array."; // A grant on a workflow run's principal ships to the run, which evaluates it -// with narrower registries than the hub's. +// with narrower registries than the hub's. A run principal is the workflow +// principal whose `refId` names a run in the tenant; it exists from deploy, +// before its first trigger links it to the run. async function isRunPrincipal( db: DB["db"], tenantId: string, principalId: string, ): Promise { - const run = await db.query.workflowRun.findFirst({ - columns: { id: true }, - where: and( - eq(workflowRun.tenantId, tenantId), - eq(workflowRun.principalId, principalId), - ), - }); + const [run] = await db + .select({ id: workflowRun.id }) + .from(principal) + .innerJoin( + workflowRun, + and( + eq(workflowRun.id, principal.refId), + eq(workflowRun.tenantId, principal.tenantId), + ), + ) + .where( + and( + eq(principal.id, principalId), + eq(principal.tenantId, tenantId), + eq(principal.kind, "workflow"), + ), + ) + .limit(1); return run !== undefined; } diff --git a/packages/hub-api/src/routes/principals.ts b/packages/hub-api/src/routes/principals.ts index 5cda3e268..fe4fd3bb2 100644 --- a/packages/hub-api/src/routes/principals.ts +++ b/packages/hub-api/src/routes/principals.ts @@ -30,8 +30,8 @@ import { resolveWorkflowPrincipalLabels } from "./workflow-principal-name"; import { changeDelegatorGrants, findDelegatedAnchorRunIds, - lockTenantRunDelegatedGrants, } from "../run-delegated-grants"; +import { lockTenantRunDelegatedGrants } from "../run-grant-materialization"; import { parsePageParams, cursorCondition, diff --git a/packages/hub-api/src/routes/runs.ts b/packages/hub-api/src/routes/runs.ts index ac658974f..f577c7bb7 100644 --- a/packages/hub-api/src/routes/runs.ts +++ b/packages/hub-api/src/routes/runs.ts @@ -907,7 +907,7 @@ export function createRunRoutes({ ), 404: jsonResponse("Run not found", ErrorResponse), 409: jsonResponse( - "Run address is not routable, its allocation is no longer active, the run is stopping or terminal, or (`deployer_unavailable`) the definition has invoker-sourced grant requirements and the deployer is not recorded or no longer active", + "Run address is not routable, its allocation is no longer active, the run is stopping or terminal, (`deployer_unavailable`) the definition has invoker-sourced grant requirements and the deployer is not recorded or no longer active, or (`workflow_run_principal_inactive`) the first trigger finds the run principal suspended or deactivated", ErrorResponse, ), 413: jsonResponse( diff --git a/packages/hub-api/src/routes/workflows.test.ts b/packages/hub-api/src/routes/workflows.test.ts index 2e9ae0662..626296efb 100644 --- a/packages/hub-api/src/routes/workflows.test.ts +++ b/packages/hub-api/src/routes/workflows.test.ts @@ -535,6 +535,8 @@ function createMockDB(opts: MockDBOpts) { principal: { findFirst: async () => testPrincipal }, asset: { findFirst: async () => opts.assetRow }, }; + // The tenant re-derivation lock; the mock serializes nothing. + const execute = async () => []; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- drizzle PgDatabase type cannot be structurally satisfied in tests return { query, @@ -545,9 +547,10 @@ function createMockDB(opts: MockDBOpts) { query: typeof query; insert: typeof insert; select: typeof select; + execute: typeof execute; }) => Promise, ) => { - return fn({ query, insert, select }); + return fn({ query, insert, select, execute }); }, } as unknown as Parameters[0]["db"]; } @@ -659,8 +662,8 @@ function createMockSessionService(): SessionService { function createMockPrincipalKeyStore(): PrincipalKeyStore { return { - // The trigger mints the run principal's key while materializing grants; the - // return value is not read, so a canned hex public key suffices. + // The trigger generates no key; the store type requires `generate`, so a + // canned hex public key suffices. generate: async () => "ab".repeat(32), // The trigger resolves the triggering principal's public key to stamp on // the inbound frame (so the recipient can verify the mail). Return the same diff --git a/packages/hub-api/src/routes/workflows.ts b/packages/hub-api/src/routes/workflows.ts index fbaced06e..1ebef4b4f 100644 --- a/packages/hub-api/src/routes/workflows.ts +++ b/packages/hub-api/src/routes/workflows.ts @@ -820,7 +820,7 @@ export function createWorkflowRoutes({ ), 404: jsonResponse("Workflow deployment not found", ErrorResponse), 409: jsonResponse( - "Deployment address is not routable, its allocation is no longer active, its top-level run is stopping or terminal, or (`deployer_unavailable`) the definition has invoker-sourced grant requirements and the deployer is not recorded or no longer active", + "Deployment address is not routable, its allocation is no longer active, its top-level run is stopping or terminal, (`deployer_unavailable`) the definition has invoker-sourced grant requirements and the deployer is not recorded or no longer active, or (`workflow_run_principal_inactive`) the first trigger finds the run principal suspended or deactivated", ErrorResponse, ), 413: jsonResponse( diff --git a/packages/hub-api/src/run-delegated-grants.ts b/packages/hub-api/src/run-delegated-grants.ts index 3d2b2d715..cda8a450f 100644 --- a/packages/hub-api/src/run-delegated-grants.ts +++ b/packages/hub-api/src/run-delegated-grants.ts @@ -1,15 +1,4 @@ -import { - and, - asc, - eq, - exists, - gt, - inArray, - isNull, - ne, - or, - sql, -} from "drizzle-orm"; +import { and, asc, eq, exists, gt, inArray, isNull, ne, or } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; import { @@ -51,30 +40,12 @@ import { collectCreatorGrants, loadActiveDeployerId, loadAssetCreatorPrincipalId, + lockTenantRunDelegatedGrants, TOOL_GRANT_PREFIX, } from "./run-grant-materialization"; const log = getLogger(["hub-api", "run-delegated-grants"]); -/** - * Serialize re-derivations of run grants in one tenant for the rest of the - * transaction. - * - * A caller that changes a delegator's grants and then re-derives must take - * this lock before its own write. Otherwise it holds the written rows while - * it waits here, and a re-derivation that holds this lock can wait on those - * rows. The lock is transaction-scoped, so a second call in the same - * transaction does not block. - */ -export async function lockTenantRunDelegatedGrants( - tx: DBExecutor, - tenantId: string, -): Promise { - await tx.execute( - sql`select pg_advisory_xact_lock(hashtextextended(${`run-delegated-grants:${tenantId}`}, 0))`, - ); -} - /** * The tenant's live deployment anchors whose definition asset was created * by, or whose launch spec records as deployer, one of `principalIds`. A @@ -466,8 +437,8 @@ async function rederiveRun( creatorGrants, invokerGrants, now, - // The first trigger stamps the run principal and the invoker expiry from - // one `now`, so a re-added invoker row never outlives the first one. + // The first trigger's invoker expiry is anchored on the run principal's + // creation too, so a re-added invoker row never outlives the first one. invokerExpiresAt: new Date( run.principalCreatedAt.getTime() + INVOKER_GRANT_TTL_MS, ), diff --git a/packages/hub-api/src/run-grant-materialization.test.ts b/packages/hub-api/src/run-grant-materialization.test.ts index b5bc88cb8..a3d4870a2 100644 --- a/packages/hub-api/src/run-grant-materialization.test.ts +++ b/packages/hub-api/src/run-grant-materialization.test.ts @@ -6,6 +6,7 @@ import type { GrantRule } from "@intx/types/authz"; import { SELF_RUN_RESOURCE, type GrantWalkSnapshot } from "@intx/types"; import { grant as grantTable, + principal as principalTable, type LegacyMailAcceptRule, sidecarAllocation as sidecarAllocationTable, workflowRun as workflowRunTable, @@ -27,17 +28,6 @@ import { type MailAcceptGrantContext, } from "./run-grant-materialization"; -// This suite exercises grant materialization, not key minting. The key store is -// a no-op stub so the winning-reservation path does not try to insert into -// principal_key against the hand-rolled mock DB. -const stubPrincipalKeyStore: PrincipalKeyStore = { - generate: async () => "pky_stub", - sign: async () => new Uint8Array(64), - getPublicKey: async () => "pky_stub_public", - recordPublicKey: async () => undefined, - retireActive: async () => undefined, -}; - // Minting is the run-token store's concern; this suite records which consumers // the commit asks it to mint for. const mintedConsumers: string[][] = []; @@ -145,6 +135,12 @@ function mockDb(opts: { */ deployer?: { id: string; status: string } | null; }) { + const inserted: { table: unknown; values: Record }[] = []; + function insertedValues(table: unknown): Record[] { + return inserted + .filter((record) => record.table === table) + .map((record) => record.values); + } function rows( table: unknown, joined: boolean, @@ -154,9 +150,11 @@ function mockDb(opts: { // row, selecting only its id. const linkLookup = fields !== undefined && Object.keys(fields).join() === "id"; + // A run principal this mock's commit inserted is linked by the same + // commit. if (table === workflowRunTable && joined && linkLookup) { return opts.committedGrants === undefined - ? [] + ? insertedValues(principalTable) : [{ id: opts.committedGrants.runPrincipalId }]; } if (table === workflowRunTable && opts.deploymentRow) { @@ -193,7 +191,11 @@ function mockDb(opts: { if (opts.snapshotReads) opts.snapshotReads.count += 1; return [{ snapshot: opts.grantSnapshot }]; } - if (table === grantTable) return opts.committedGrants?.stepGrants ?? []; + if (table === grantTable) { + return opts.committedGrants === undefined + ? insertedValues(grantTable).map((row) => ({ ...row, roleId: null })) + : opts.committedGrants.stepGrants; + } return []; } const select = (fields?: Record) => ({ @@ -252,17 +254,21 @@ function mockDb(opts: { return chain; }, }); - const insert = (_table: unknown) => ({ - values: (values: unknown) => - Object.assign(Promise.resolve(), { + const insert = (table: unknown) => ({ + values: (values: Record) => { + inserted.push({ table, values }); + return Object.assign(Promise.resolve(), { onConflictDoNothing: () => ({ returning: () => Promise.resolve([values]), }), - }), + }); + }, }); const query = { asset: { findFirst: async () => opts.assetRow }, }; + // The tenant re-derivation lock; the mock serializes nothing. + const execute = async () => []; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- drizzle PgDatabase type cannot be structurally satisfied in tests return { query, @@ -273,8 +279,9 @@ function mockDb(opts: { query: typeof query; select: typeof select; insert: typeof insert; + execute: typeof execute; }) => Promise | unknown, - ) => fn({ query, select, insert }), + ) => fn({ query, select, insert, execute }), } as unknown as Parameters< typeof createMailTriggeredRunGrantsMaterializer >[0]["db"]; @@ -344,6 +351,7 @@ describe("stageRunGrantsFromSnapshot with the run's own resource", () => { runId: STAGED_RUN_ID, runPrincipalId: "prn_run", now: new Date("2026-01-01"), + invokerExpiresAt: new Date("2026-01-02"), invokerPrincipalId: null, assetId: ASSET_ID, legacyMailAccept: null, @@ -395,7 +403,6 @@ describe("createMailTriggeredRunGrantsMaterializer hub tokens", () => { hubConsumers: ["action:a"], }, }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([]), }); @@ -645,7 +652,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { assetRow, grantSnapshot: snapshot(), }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([creatorGrant(), deployerGrant()]), @@ -668,7 +674,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { snapshotReads: reads, topLevelRunStatus: "completed", }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([creatorGrant(), deployerGrant()]), @@ -698,7 +703,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { topLevelRunStatus: null, lockedRunStatus: "failed", }), - principalKeyStore: stubPrincipalKeyStore, // Both the creator and the deployer's invoker grant are held, so staging // succeeds and the run reaches the under-lock revalidation the test // exercises -- the lock, not an insufficient-authority reject, is what @@ -731,7 +735,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { snapshotReads: reads, anchorCancellationRequestedAt: new Date(), }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([creatorGrant(), deployerGrant()]), @@ -760,7 +763,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { topLevelRunStatus: null, lockedCancellationRequestedAt: new Date(), }), - principalKeyStore: stubPrincipalKeyStore, // Both the creator and the deployer's invoker grant are held, so staging // succeeds and the run reaches the under-lock revalidation the test // exercises -- the lock, not an insufficient-authority reject, is what @@ -786,7 +788,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { test("stages the tool grant, the creator requirement, and the invoker requirement bound to the deployer", async () => { const materialize = createMailTriggeredRunGrantsMaterializer({ db: mockDb({ deploymentRow, assetRow, grantSnapshot: snapshot() }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([creatorGrant(), deployerGrant()]), @@ -834,7 +835,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { assetRow, grantSnapshot: invokerOnlySnapshot(), }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([deployerGrant()]), }); @@ -868,7 +868,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { assetRow, grantSnapshot: invokerOnlySnapshot(), }), - principalKeyStore: stubPrincipalKeyStore, grantStoreFor: () => createInMemoryGrantStore([invokerGrant()]), }); const result = await materialize({ @@ -894,7 +893,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { assetRow, grantSnapshot: snapshot, }), - principalKeyStore: stubPrincipalKeyStore, grantStoreFor: () => createInMemoryGrantStore([deployerGrant()]), }); const result = await materialize({ @@ -920,7 +918,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { assetRow, grantSnapshot: invokerOnlySnapshot(), }), - principalKeyStore: stubPrincipalKeyStore, grantStoreFor: () => createInMemoryGrantStore([deployerGrant()]), }); await expect( @@ -948,7 +945,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { grantSnapshot: invokerOnlySnapshot(), deployer, }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([deployerGrant()]), }); @@ -973,7 +969,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { grantSnapshot: { ...snapshot(), grantRequirements: [] }, deployer: null, }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([]), }); @@ -1000,7 +995,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { ], }, }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([deployerGrant()]), }); @@ -1043,7 +1037,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { ], }, }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([deployerGrant()]), }); @@ -1076,7 +1069,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { grantSnapshot: invokerOnlySnapshot(), credentialRefs: { credentialIds: ["inference-only"], bindings: [] }, }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([deployerGrant()]), }); @@ -1101,7 +1093,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { grantSnapshot: invokerOnlySnapshot(), publicKey: null, }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([deployerGrant()]), }); @@ -1132,7 +1123,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { }, initializationLeaseId: "lease-1", }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([deployerGrant()]), }); @@ -1149,7 +1139,6 @@ describe("createMailTriggeredRunGrantsMaterializer staging", () => { // set. const materialize = createMailTriggeredRunGrantsMaterializer({ db: mockDb({ deploymentRow, assetRow, grantSnapshot: null }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([creatorGrant(), deployerGrant()]), @@ -1244,7 +1233,6 @@ describe("createMailTriggeredRunGrantsMaterializer admission gate", () => { assetRow, grantSnapshot: noAcceptSnapshot(), }), - principalKeyStore: stubPrincipalKeyStore, grantStoreFor: () => createInMemoryGrantStore([creatorGrant()]), }); await expect( @@ -1268,7 +1256,6 @@ describe("createMailTriggeredRunGrantsMaterializer admission gate", () => { assetRow, grantSnapshot: explicitPrincipalSnapshot(SENDER_PRINCIPAL_ID), }), - principalKeyStore: stubPrincipalKeyStore, grantStoreFor: () => createInMemoryGrantStore([]), }); const result = await materialize({ @@ -1289,7 +1276,6 @@ describe("createMailTriggeredRunGrantsMaterializer admission gate", () => { assetRow, grantSnapshot: explicitPrincipalSnapshot("prn_admitted"), }), - principalKeyStore: stubPrincipalKeyStore, grantStoreFor: () => createInMemoryGrantStore([]), }); await expect( @@ -1329,7 +1315,6 @@ describe("createMailTriggeredRunGrantsMaterializer admission gate", () => { assetRow, grantSnapshot: tenantAcceptSnapshot, }), - principalKeyStore: stubPrincipalKeyStore, grantStoreFor: () => createInMemoryGrantStore([]), }); await expect( @@ -1357,7 +1342,6 @@ describe("createMailTriggeredRunGrantsMaterializer admission gate", () => { stepGrants: [committedGrantRow], }, }), - principalKeyStore: stubPrincipalKeyStore, grantStoreFor: () => createInMemoryGrantStore([ runAcceptGrant(`mail.accept:principal:${SENDER_PRINCIPAL_ID}`), @@ -1389,7 +1373,6 @@ describe("createMailTriggeredRunGrantsMaterializer admission gate", () => { stepGrants: [committedGrantRow], }, }), - principalKeyStore: stubPrincipalKeyStore, // The run holds no accept-grant naming this sender. grantStoreFor: () => createInMemoryGrantStore([ @@ -1421,7 +1404,6 @@ describe("createMailTriggeredRunGrantsMaterializer admission gate", () => { stepGrants: [committedGrantRow], }, }), - principalKeyStore: stubPrincipalKeyStore, // A broad `*` grant matches `mail.accept:...` under pattern matching, but // the admission helper filters to the `mail.accept` namespace first, so it // never auto-admits. @@ -1469,7 +1451,6 @@ describe("createMailTriggeredRunGrantsMaterializer admission gate", () => { stepGrants: [committedGrantRow], }, }), - principalKeyStore: stubPrincipalKeyStore, grantStoreFor: () => createInMemoryGrantStore([ runAcceptGrant(`mail.accept:principal:${SENDER_PRINCIPAL_ID}`), @@ -1681,7 +1662,6 @@ describe("createMailTriggeredRunGrantsMaterializer committed grants", () => { snapshotReads: reads, committedGrants, }), - principalKeyStore: stubPrincipalKeyStore, runTokenStore: stubRunTokenStore, grantStoreFor: () => createInMemoryGrantStore([ @@ -1722,6 +1702,7 @@ describe("stageRunGrantsFromSnapshot for an HTTP launch", () => { runId: "run_http_launch", runPrincipalId: "prn_run", now: new Date("2026-01-01T00:00:00.000Z"), + invokerExpiresAt: new Date("2026-01-02T00:00:00.000Z"), invokerPrincipalId: DEPLOYER_PRINCIPAL_ID, assetId: ASSET_ID, legacyMailAccept: null, @@ -1743,6 +1724,16 @@ describe("stageRunGrantsFromSnapshot for an HTTP launch", () => { }); describe("createCorrespondentGrantMinter", () => { + // The minter resolves correspondents through the key store; these cases + // mint nothing, so a no-op stub suffices. + const stubPrincipalKeyStore: PrincipalKeyStore = { + generate: async () => "pky_stub", + sign: async () => new Uint8Array(64), + getPublicKey: async () => "pky_stub_public", + recordPublicKey: async () => undefined, + retireActive: async () => undefined, + }; + // A DB stand-in for the minter's unlocked rule check. It serves the sending // anchor and the frozen snapshot of its definition, and counts the // transactions the minter opens. The locked path itself is covered by the DB diff --git a/packages/hub-api/src/run-grant-materialization.ts b/packages/hub-api/src/run-grant-materialization.ts index 1bc70f6fc..fa683fad9 100644 --- a/packages/hub-api/src/run-grant-materialization.ts +++ b/packages/hub-api/src/run-grant-materialization.ts @@ -11,7 +11,8 @@ // resolved the id and the consumer. A first commit waits until that // resolution is recorded. This // module stages those rows and commits them idempotently on the run id, -// minting the run principal and anchoring the run row in one transaction. +// granting them to the run principal and linking it to the run row in one +// transaction. // // Delivery (`run.grants` frame, trigger mail / inbound mail forwarding) is // NOT owned here: the two call sites order those differently for their @@ -42,8 +43,8 @@ import type { RunTokenStore, } from "@intx/db"; import { - createPrincipalStore, createWorkflowRunStore, + insertRunPrincipal, workflowRunExecutability, loadDeploymentGrantSnapshot, resolveSenderPrincipal, @@ -84,6 +85,7 @@ import { } from "@intx/authz"; import { + INVOKER_GRANT_TTL_MS, makeGrantRow, resolveGrantMaterialization, type GrantMaterializationRejection, @@ -467,6 +469,8 @@ export type StageRunGrantsFromSnapshotArgs = { runId: string; runPrincipalId: string; now: Date; + /** The latest expiry of an invoker-delegated `allow` row. */ + invokerExpiresAt: Date; /** * The deployment's active deployer, its invoker of record. Resolves the * snapshot's `mail.accept:invoker` marker to `mail.accept:principal:`. @@ -559,6 +563,7 @@ export async function stageRunGrantsFromSnapshot( invokerGrants: args.invokerGrants, creatorGrants: args.creatorGrants, now: args.now, + invokerExpiresAt: args.invokerExpiresAt, }); if (!materialization.ok) { return { ok: false, rejection: materialization.rejection }; @@ -701,6 +706,8 @@ export type StageFirstTriggerRunGrantsArgs = { legacyMailAccept: LegacyMailAcceptRule | null; credentialBindings: readonly { credentialId: string; consumer: string }[]; now: Date; + /** The latest expiry of an invoker-delegated `allow` row. */ + invokerExpiresAt: Date; }; /** @@ -741,6 +748,7 @@ export async function stageFirstTriggerRunGrants( runId: args.runId, runPrincipalId: args.runPrincipalId, now: args.now, + invokerExpiresAt: args.invokerExpiresAt, invokerPrincipalId: await loadActiveDeployerId( executor, args.tenantId, @@ -757,7 +765,6 @@ export async function stageFirstTriggerRunGrants( export type CommitRunGrantsArgs = { db: DB["db"]; - principalKeyStore: PrincipalKeyStore; runTokenStore: RunTokenStore; /** * The consumers that get a hub run token, from the locked anchor's @@ -1027,17 +1034,19 @@ export async function setRunToolGrantEffect( * * The `runPrincipalId` is derived deterministically from `(tenantId, runId)` * by the caller, so the principal and the grant rows that reference it agree - * on the id even across a retry. A run principal that already exists without - * a link is reused only when it has that id, is active, and holds no grants, - * so a commit never links a different or deactivated principal or merges into - * an earlier snapshot. + * on the id even across a retry. A deployment's run principal exists from + * deploy; the commit creates one, with no hub key, only for a run that has + * none. An existing run principal must have that id and be active, so a + * commit never links a different or deactivated principal. Grants an + * operator gave the principal before its first trigger stay, and the commit + * returns all of the principal's rows, which is what every later read + * returns. */ export async function commitRunGrants( args: CommitRunGrantsArgs, tx: DBExecutor, ): Promise { const workflowRunStore = createWorkflowRunStore(args.db); - const principalStore = createPrincipalStore(args.db, args.principalKeyStore); const existing = await loadCommittedRunGrantsFromExecutor( tx, args.tenantId, @@ -1045,19 +1054,13 @@ export async function commitRunGrants( ); if (existing !== null) return existing.stepGrants; - const insertedPrincipal = await principalStore.createIfAbsent( - { - id: args.runPrincipalId, - tenantId: args.tenantId, - kind: "workflow", - refId: args.runId, - status: "active", - createdAt: args.now, - updatedAt: args.now, - }, - tx, - ); - if (insertedPrincipal === null) { + const insertedPrincipal = await insertRunPrincipal(tx, { + id: args.runPrincipalId, + tenantId: args.tenantId, + runId: args.runId, + createdAt: args.now, + }); + if (!insertedPrincipal) { await assertReusableRunPrincipal(tx, args); } @@ -1092,9 +1095,106 @@ export async function commitRunGrants( for (const g of args.grantRows) { await tx.insert(grantTable).values(g); } - return [...args.grantRows] - .sort((left, right) => left.id.localeCompare(right.id)) - .map((grant) => runGrantToWire(grant)); + const committed = await loadCommittedRunGrantsFromExecutor( + tx, + args.tenantId, + args.runId, + ); + if (committed === null) { + throw new Error( + `commitRunGrants: run ${args.runId} has no committed grants after linking principal ${args.runPrincipalId}`, + ); + } + return committed.stepGrants; +} + +/** + * Serialize re-derivations of run grants in one tenant for the rest of the + * transaction. + * + * A caller that changes a delegator's grants and then re-derives must take + * this lock before its own write. Otherwise it holds the written rows while + * it waits here, and a re-derivation that holds this lock can wait on those + * rows. A first trigger takes it before its other locks, so a grant change + * on the run principal commits before the first commit reads the + * principal's rows, or waits until that commit is done. The scope is the + * tenant, not the run principal: a grant change does not lock the principal + * row, and a role change finds the principals it affects only after it + * writes, so no narrower lock orders both. The lock is transaction-scoped, so + * a second call in the same transaction does not block. + */ +export async function lockTenantRunDelegatedGrants( + tx: DBExecutor, + tenantId: string, +): Promise { + await tx.execute( + sql`select pg_advisory_xact_lock(hashtextextended(${`run-delegated-grants:${tenantId}`}, 0))`, + ); +} + +export type RunPrincipalBeforeFirstCommit = { + id: string; + status: string; + createdAt: Date; +}; + +/** + * A run's principal before its first commit, read in the caller's locked + * transaction, or null when the run has none yet. A deployment's run + * principal exists from deploy, so a first trigger reads its status, which + * an operator can have changed, and its creation time, which anchors the + * expiry of invoker-delegated `allow` rows. + */ +export async function loadRunPrincipalBeforeFirstCommit( + tx: DBExecutor, + tenantId: string, + runId: string, +): Promise { + const [row] = await tx + .select({ + id: principalTable.id, + status: principalTable.status, + createdAt: principalTable.createdAt, + }) + .from(principalTable) + .where( + and( + eq(principalTable.tenantId, tenantId), + eq(principalTable.kind, "workflow"), + eq(principalTable.refId, runId), + ), + ) + .limit(1); + return row ?? null; +} + +/** + * The refusal of a first trigger whose run principal an operator suspended or + * deactivated before the run fired, or null when the run may fire. + */ +export function inactiveRunPrincipalRejection( + runId: string, + runPrincipal: RunPrincipalBeforeFirstCommit | null, +): GrantMaterializationRejection | null { + if (runPrincipal === null || runPrincipal.status === "active") return null; + return { + status: 409, + code: "workflow_run_principal_inactive", + message: `Workflow run ${runId} has a ${runPrincipal.status} principal and cannot start`, + }; +} + +/** + * The latest expiry of a run's invoker-delegated `allow` rows: + * `INVOKER_GRANT_TTL_MS` after the run principal is created, which is the + * deployment's deploy time. A run with no principal yet creates it at `now`. + */ +export function runInvokerExpiresAt( + runPrincipal: RunPrincipalBeforeFirstCommit | null, + now: Date, +): Date { + const createdAt = runPrincipal?.createdAt ?? now; + return new Date(createdAt.getTime() + INVOKER_GRANT_TTL_MS); } async function assertReusableRunPrincipal( @@ -1117,26 +1217,17 @@ async function assertReusableRunPrincipal( `commitRunGrants: run ${args.runId} has principal ${existing?.id ?? "none"}, not the derived ${args.runPrincipalId}`, ); } + // The caller refuses an inactive run principal before it stages, under the + // same lock, so reaching here with one is an invariant break. if (existing.status !== "active") { throw new Error( `commitRunGrants: run principal ${args.runPrincipalId} is ${existing.status}, not active`, ); } - const [grant] = await tx - .select({ id: grantTable.id }) - .from(grantTable) - .where(eq(grantTable.principalId, args.runPrincipalId)) - .limit(1); - if (grant !== undefined) { - throw new Error( - `commitRunGrants: run principal ${args.runPrincipalId} already holds grants before its first commit`, - ); - } } export type MailTriggeredRunGrantsDeps = { db: DB["db"]; - principalKeyStore: PrincipalKeyStore; runTokenStore: RunTokenStore; grantStoreFor: GrantStoreFactory; // The hub's condition registry, passed to the admission evaluator so a @@ -1334,11 +1425,9 @@ export function createMailTriggeredRunGrantsMaterializer( // `evaluateMailAdmission` filters them to the `mail.accept` namespace, // picks one winning grant across the sender's coordinates, and // default-denies when none match. A `null` sender coordinate set is never - // admitted. The commit below refuses a run principal that already holds - // grants, so no operator grant on it takes part in the first fire: - // first-fire admission is governed by the deploy-approved accept surface - // alone, and an operator deny on the run principal binds from the next - // delivery on. + // admitted. This check reads only the deploy-approved accept surface; the + // check under the lock below also reads the grants an operator gave the + // run principal before its first trigger. // // The check runs before the grant requirements resolve, so a refused // sender is refused as `mail_admission_denied` and costs no grant reads. @@ -1375,6 +1464,7 @@ export function createMailTriggeredRunGrantsMaterializer( if (!admission.admit) return admissionDenied(); const reserved = await deps.db.transaction(async (tx) => { + await lockTenantRunDelegatedGrants(tx, tenantId); const anchorState = await lockWorkflowRunState( tx, anchorRunId, @@ -1396,6 +1486,21 @@ export function createMailTriggeredRunGrantsMaterializer( ); if (raced !== null) return { committedByAnotherSender: raced }; + const runPrincipal = await loadRunPrincipalBeforeFirstCommit( + tx, + tenantId, + runId, + ); + const inactive = inactiveRunPrincipalRejection(runId, runPrincipal); + if (inactive !== null) { + return { + rejected: { + outcome: "rejected", + ...inactive, + } satisfies MailTriggeredRunGrantsResult, + }; + } + // The definition's FULL declared requirements are resolved -- both // creator- and invoker-sourced -- so an invoker requirement the deployer // cannot satisfy fails the run closed instead of launching @@ -1418,6 +1523,7 @@ export function createMailTriggeredRunGrantsMaterializer( legacyMailAccept: anchor.legacyMailAccept, credentialBindings: credentialUseBindings(anchor.credentialRefs), now, + invokerExpiresAt: runInvokerExpiresAt(runPrincipal, now), }, ); if (!staged.ok) { @@ -1431,15 +1537,30 @@ export function createMailTriggeredRunGrantsMaterializer( }; } // The provisional admission read the deployer before the lock, and the - // deployer can stop being active before these reads. Admit against the - // staged rows, so a sender is only admitted by the `mail.accept` rows - // the run commits. + // deployer can stop being active before these reads. Admit only a + // sender the staged rows the run commits admit: a declared rule starts + // a run, an operator grant does not. Then admit again with the grants + // an operator gave the run principal before its first trigger added, + // so an operator `mail.accept` deny binds from the first fire. + const stagedRules = staged.grantRows.map(materializedRowToGrantRule); + const registry = deps.registry ? { registry: deps.registry } : {}; const stagedAdmission = await evaluateMailAdmission({ senderCoordinates, - recipientGrants: staged.grantRows.map(materializedRowToGrantRule), - ...(deps.registry ? { registry: deps.registry } : {}), + recipientGrants: stagedRules, + ...registry, }); if (!stagedAdmission.admit) return { rejected: admissionDenied() }; + if (runPrincipal !== null) { + const operatorGrants = await deps + .grantStoreFor(tx) + .collectGrants(runPrincipal.id, tenantId); + const operatorAdmission = await evaluateMailAdmission({ + senderCoordinates, + recipientGrants: [...operatorGrants, ...stagedRules], + ...registry, + }); + if (!operatorAdmission.admit) return { rejected: admissionDenied() }; + } const resolution = await lockAnchorCredentialResolution(tx, anchorRunId); if (resolution === null) { @@ -1453,13 +1574,9 @@ export function createMailTriggeredRunGrantsMaterializer( { tenantId, runPrincipalId, now }, ); if (grantRows === "not-ready") return grantRows; - // `now` stamps the staged rows, and the run principal's creation when - // this commit creates it, so an invoker `allow` row expires - // `INVOKER_GRANT_TTL_MS` after the first commit at the latest. return commitRunGrants( { db: deps.db, - principalKeyStore: deps.principalKeyStore, runTokenStore: deps.runTokenStore, hubConsumers: hubConsumersOf(resolution.credentialRefs), tenantId, @@ -1492,14 +1609,15 @@ const correspondentLogger = getLogger(["hub-api", "correspondent-grant"]); * Resolve the principal a correspondent's reply will present. A party with a * durable identity resolves through the same `resolveSenderPrincipal` the reply * resolves through. A local deployment that `resolveSenderPrincipal` cannot - * resolve -- one that has not fired yet (the first fire sets its principal), or - * one whose signing key is cleared while its sidecar is replaced -- resolves - * from its anchor row instead: the committed principal when there is one, else - * the id the first fire derives, `deriveRunPrincipalId(tenantId, runId)` with - * the runId taken from the deployment address, exactly as the mail seam and the - * trigger route derive it. A committed principal is that same derived id, so a - * first fire that commits concurrently cannot make the two disagree. The anchor - * is matched on its exact stored address, as the mail materializer matches it. + * resolve -- one that has not fired yet (the first fire links its principal to + * the anchor), or one whose signing key is cleared while its sidecar is + * replaced -- resolves from its anchor row instead: the linked principal when + * there is one, else `deriveRunPrincipalId(tenantId, runId)` with the runId + * taken from the deployment address. Deploy creates the run principal with + * that same derived id, and the mail seam and the trigger route derive it the + * same way, so a first fire that commits concurrently cannot make the two + * disagree. The anchor is matched on its exact stored address, as the mail + * materializer matches it. * * Only an executable anchor qualifies: a stopping or terminal deployment never * fires or replies, so a row for it would be dead weight. An anchor without a diff --git a/packages/hub-api/src/workflow-run-trigger.test.ts b/packages/hub-api/src/workflow-run-trigger.test.ts index 3bacd47f8..84f65c724 100644 --- a/packages/hub-api/src/workflow-run-trigger.test.ts +++ b/packages/hub-api/src/workflow-run-trigger.test.ts @@ -247,6 +247,8 @@ describe("triggerWorkflowRun first-trigger staging", () => { const tx = { select: queuedSelect(opts.txRows), query: { asset: { findFirst: async () => asset } }, + // The tenant re-derivation lock; the mock serializes nothing. + execute: async () => [], }; const db = { select: queuedSelect( @@ -362,6 +364,30 @@ describe("triggerWorkflowRun first-trigger staging", () => { expect(collected).toEqual([CREATOR_ID]); }); + test("refuses a first trigger whose run principal an operator suspended", async () => { + const { trigger } = makeTrigger({ + txRows: [ + // The anchor lock. + [{ status: "running", expiresAt: null, cancellationRequestedAt: null }], + // No run grants committed by a concurrent trigger. + [], + // The run principal deploy created, since suspended. + [{ id: "prn_run", status: "suspended", createdAt: new Date() }], + ], + grantStoreFor: () => { + throw new Error("a refused first trigger must not stage"); + }, + }); + + const result = await trigger(triggerArgs); + + expect(result).toMatchObject({ + ok: false, + status: 409, + body: { error: { code: "workflow_run_principal_inactive" } }, + }); + }); + test("a trigger that lost the first-fire race sends the winner's grants without staging", async () => { const winnerGrant = { id: "grt_winner", diff --git a/packages/hub-api/src/workflow-run-trigger.ts b/packages/hub-api/src/workflow-run-trigger.ts index 951316f2d..fe1289f73 100644 --- a/packages/hub-api/src/workflow-run-trigger.ts +++ b/packages/hub-api/src/workflow-run-trigger.ts @@ -68,7 +68,11 @@ import { lockDispatchableAllocation, lockWorkflowRunState, credentialUseBindings, + inactiveRunPrincipalRejection, loadCommittedRunGrantsFromExecutor, + loadRunPrincipalBeforeFirstCommit, + lockTenantRunDelegatedGrants, + runInvokerExpiresAt, stageFirstTriggerRunGrants, } from "./run-grant-materialization"; import type { GrantMaterializationRejection } from "./grant-materialization"; @@ -437,7 +441,6 @@ export function createWorkflowRunTrigger(deps: TriggerWorkflowRunDeps) { > => { const grantArgs = { db, - principalKeyStore, runTokenStore, tenantId: tenant.id, anchorRunId, @@ -460,6 +463,13 @@ export function createWorkflowRunTrigger(deps: TriggerWorkflowRunDeps) { runId, ); if (raced !== null) return raced.stepGrants; + const runPrincipal = await loadRunPrincipalBeforeFirstCommit( + tx, + tenant.id, + runId, + ); + const inactive = inactiveRunPrincipalRejection(runId, runPrincipal); + if (inactive !== null) return { rejected: inactive }; // Invoker-sourced requirements resolve against the deployment's // deployer, not this caller, so the trigger path cannot change whose // authority a run delegates. The deployer and the asset behind the @@ -477,6 +487,7 @@ export function createWorkflowRunTrigger(deps: TriggerWorkflowRunDeps) { legacyMailAccept: anchor.legacyMailAccept, credentialBindings: credentialUseBindings(anchor.credentialRefs), now, + invokerExpiresAt: runInvokerExpiresAt(runPrincipal, now), }); if (!staged.ok) return { rejected: staged.rejection }; const resolution = await lockAnchorCredentialResolution(tx, anchorRunId); @@ -491,9 +502,6 @@ export function createWorkflowRunTrigger(deps: TriggerWorkflowRunDeps) { { tenantId: tenant.id, runPrincipalId, now }, ); if (grantRows === "not-ready") return grantRows; - // `now` stamps the staged rows, and the run principal's creation when - // this commit creates it, so an invoker `allow` row expires - // `INVOKER_GRANT_TTL_MS` after the first commit at the latest. return commitRunGrants( { ...grantArgs, @@ -520,6 +528,9 @@ export function createWorkflowRunTrigger(deps: TriggerWorkflowRunDeps) { }; } const committed = await db.transaction(async (tx) => { + if (firstTriggerSnapshot !== null) { + await lockTenantRunDelegatedGrants(tx, tenant.id); + } if ( !(await lockDispatchableAllocation(tx, allocationId, anchorRunId)) ) { @@ -600,6 +611,9 @@ export function createWorkflowRunTrigger(deps: TriggerWorkflowRunDeps) { } const reserved = await db.transaction(async (tx) => { + if (firstTriggerSnapshot !== null) { + await lockTenantRunDelegatedGrants(tx, tenant.id); + } const anchorState = await lockWorkflowRunState( tx, anchorRunId, diff --git a/packages/hub-sessions/src/hub-session-lookups.ts b/packages/hub-sessions/src/hub-session-lookups.ts index 50e78daa9..5d1996c62 100644 --- a/packages/hub-sessions/src/hub-session-lookups.ts +++ b/packages/hub-sessions/src/hub-session-lookups.ts @@ -331,7 +331,7 @@ export function createHubSessionLookups( // Lazily anchor the run before its correlation and approval reference // it. A workflow-spawned internal run never crosses the external - // trigger route that mints a run principal, so its run row would + // trigger route that links a run principal, so its run row would // otherwise not exist; ensure it here so the co-written rows have a // referent. The principal is null: an internal run inherits its // deployment's grants and has no principal of its own. The insert is @@ -742,8 +742,8 @@ export async function resolveRunIdForSession( tenantId: string, ): Promise { // No `endedAt` filter: a stopped run's mail must stay fetchable, so the - // session resolves whether or not it has ended. The run principal is minted - // per launch and shared 1:1 by the run and its session, so at most one row + // session resolves whether or not it has ended. The run principal is created + // per deployment and shared 1:1 by the run and its session, so at most one row // matches; order deterministically anyway so a hypothetical second row cannot // make the pick flap, mirroring `resolveRunSessionId`. const row = await db diff --git a/packages/hub-sessions/src/session-service.test.ts b/packages/hub-sessions/src/session-service.test.ts index d870c562e..f0ea592c0 100644 --- a/packages/hub-sessions/src/session-service.test.ts +++ b/packages/hub-sessions/src/session-service.test.ts @@ -13,7 +13,10 @@ import { type AgentDeployFrame, WorkflowProjectionDefinition, } from "@intx/types/sidecar"; -import { sidecarAllocation } from "@intx/db/schema"; +import { + sidecarAllocation, + workflowRun as workflowRunTable, +} from "@intx/db/schema"; import type { ToolPackageManifest } from "@intx/types/tool-packages"; import type { CredentialDelivery } from "@intx/types/credential-delivery"; import type { DB, SidecarAllocation } from "@intx/db"; @@ -519,10 +522,16 @@ describe("deployCodeSourcedWorkflow", () => { }), }, }, - insert: () => ({ + // The anchor insert is captured; the run principal insert that joins it + // reports a fresh row. + insert: (table: unknown) => ({ values: (row: Record) => { - capturedAnchorRow = row; - return Promise.resolve(); + if (table === workflowRunTable) capturedAnchorRow = row; + return Object.assign(Promise.resolve(), { + onConflictDoNothing: () => ({ + returning: () => Promise.resolve([row]), + }), + }); }, }), update: () => ({ @@ -535,6 +544,7 @@ describe("deployCodeSourcedWorkflow", () => { }; }, }), + transaction: (run: (tx: unknown) => Promise) => run(CAPTURING_DB), } as unknown as DB["db"]; async function capturePreparedDeployError( @@ -1991,7 +2001,15 @@ describe("deployCodeSourcedWorkflow", () => { }), }, }, - insert: () => ({ values: () => Promise.resolve() }), + insert: () => ({ + values: (row: Record) => + Object.assign(Promise.resolve(), { + onConflictDoNothing: () => ({ + returning: () => Promise.resolve([row]), + }), + }), + }), + transaction: (run: (tx: unknown) => Promise) => run(COUNTING_DB), update: () => ({ set: () => ({ where: () => ({ @@ -2169,7 +2187,15 @@ describe("deployCodeSourcedWorkflow", () => { }), }, }, - insert: () => ({ values: () => Promise.resolve() }), + insert: () => ({ + values: (row: Record) => + Object.assign(Promise.resolve(), { + onConflictDoNothing: () => ({ + returning: () => Promise.resolve([row]), + }), + }), + }), + transaction: (run: (tx: unknown) => Promise) => run(COUNTING_DB), update: () => ({ set: () => ({ where: () => ({ diff --git a/packages/hub-sessions/src/session-service.ts b/packages/hub-sessions/src/session-service.ts index 7429a1299..50422c5bd 100644 --- a/packages/hub-sessions/src/session-service.ts +++ b/packages/hub-sessions/src/session-service.ts @@ -5,11 +5,15 @@ import { buildCredentialDelivery, createSidecarAllocationStore, createWorkflowDefinitionArtifactStore, + deactivateRunPrincipals, + insertRunPrincipal, resolveInferenceMaterials, type DB, type SidecarAllocation, } from "@intx/db"; +import { deriveRunPrincipalId } from "@intx/hub-common"; import { + principal as principalTable, workflowDefinition as workflowDefinitionTable, workflowRun as workflowRunTable, type WorkflowRunCredentialRefs, @@ -877,21 +881,41 @@ export async function deployCodeSourcedWorkflow( const { definitionId, sendArgs, hubConsumers } = await prepareSourceRefDeploy(args); - // INSERT the anchor before the frame. A collision or DB error here spawned - // nothing (no frame went out), so it is a clean, non-leaking failure. + // INSERT the anchor and its run principal before the frame. A collision or + // DB error here spawned nothing (no frame went out), so it is a clean, + // non-leaking failure. try { - await args.db.insert(workflowRunTable).values({ - id: args.anchorRunId, - tenantId: args.tenantId, - anchorRunId: args.anchorRunId, - definitionId, - address: args.agentAddress, - publicKey: null, - status: "deployed", - createdAt: new Date(), - // Persist the non-secret shape of the delivery so the reconnect resync - // can re-resolve current materials for these ids. Secrets never land here. - ...credentialRefsField(sendArgs.credentials, hubConsumers), + const runPrincipalId = await deriveRunPrincipalId( + args.tenantId, + args.anchorRunId, + ); + await args.db.transaction(async (tx) => { + const createdAt = new Date(); + await tx.insert(workflowRunTable).values({ + id: args.anchorRunId, + tenantId: args.tenantId, + anchorRunId: args.anchorRunId, + definitionId, + address: args.agentAddress, + publicKey: null, + status: "deployed", + createdAt, + // Persist the non-secret shape of the delivery so the reconnect resync + // can re-resolve current materials for these ids. Secrets never land + // here. + ...credentialRefsField(sendArgs.credentials, hubConsumers), + }); + const runPrincipalCreated = await insertRunPrincipal(tx, { + id: runPrincipalId, + tenantId: args.tenantId, + runId: args.anchorRunId, + createdAt, + }); + if (!runPrincipalCreated) { + throw new Error( + `Deployment ${args.anchorRunId} already has a run principal`, + ); + } }); } catch (cause) { throw new SessionLaunchError(cause, false); @@ -907,19 +931,34 @@ export async function deployCodeSourcedWorkflow( // fully roll the anchor back. The guard (`deployed`, null key) is a // tripwire on the `frameSent: false` contract -- a 0-row delete means the // row advanced or vanished, so the contract lied and a child may be live; - // surface that loudly and refuse to claim it is safe to roll back. - const deleted = await args.db - .delete(workflowRunTable) - .where( - and( - eq(workflowRunTable.id, args.anchorRunId), - eq(workflowRunTable.anchorRunId, args.anchorRunId), - eq(workflowRunTable.tenantId, args.tenantId), - eq(workflowRunTable.status, "deployed"), - isNull(workflowRunTable.publicKey), - ), - ) - .returning({ id: workflowRunTable.id }); + // surface that loudly and refuse to claim it is safe to roll back. The + // run principal goes with the anchor. + const deleted = await args.db.transaction(async (tx) => { + const rows = await tx + .delete(workflowRunTable) + .where( + and( + eq(workflowRunTable.id, args.anchorRunId), + eq(workflowRunTable.anchorRunId, args.anchorRunId), + eq(workflowRunTable.tenantId, args.tenantId), + eq(workflowRunTable.status, "deployed"), + isNull(workflowRunTable.publicKey), + ), + ) + .returning({ id: workflowRunTable.id }); + if (rows.length > 0) { + await tx + .delete(principalTable) + .where( + and( + eq(principalTable.tenantId, args.tenantId), + eq(principalTable.kind, "workflow"), + eq(principalTable.refId, args.anchorRunId), + ), + ); + } + return rows; + }); if (deleted.length === 0) { logger.error`anchor-before-frame rollback found no deployed/null-key row for ${args.anchorRunId} after a frameSent:false failure; the never-sent contract was violated and a child may be live`; throw new SessionLaunchError(cause, true); @@ -930,20 +969,30 @@ export async function deployCodeSourcedWorkflow( // a sent-but-unacked frame or unexpected error leaves it possibly live. Fence the // anchor `deployed` -> `failed` (guarded so a self-flip to "running" by a // trigger that already landed is left alone). Do NOT delete: a live child - // needs the anchor to bootstrap. - const flipped = await args.db - .update(workflowRunTable) - .set({ status: "failed" }) - .where( - and( - eq(workflowRunTable.id, args.anchorRunId), - eq(workflowRunTable.anchorRunId, args.anchorRunId), - eq(workflowRunTable.tenantId, args.tenantId), - eq(workflowRunTable.status, "deployed"), - isNull(workflowRunTable.publicKey), - ), - ) - .returning({ id: workflowRunTable.id }); + // needs the anchor to bootstrap. The failed run's principal is deactivated + // with it. + const flipped = await args.db.transaction(async (tx) => { + const rows = await tx + .update(workflowRunTable) + .set({ status: "failed" }) + .where( + and( + eq(workflowRunTable.id, args.anchorRunId), + eq(workflowRunTable.anchorRunId, args.anchorRunId), + eq(workflowRunTable.tenantId, args.tenantId), + eq(workflowRunTable.status, "deployed"), + isNull(workflowRunTable.publicKey), + ), + ) + .returning({ id: workflowRunTable.id }); + await deactivateRunPrincipals( + tx, + args.tenantId, + rows.map((row) => row.id), + new Date(), + ); + return rows; + }); if (flipped.length === 0) { // The anchor already advanced past deployed -- a trigger flipped it to // "running", so the deploy actually succeeded and the run is progressing diff --git a/packages/hub-sessions/src/workflow-allocation-service.ts b/packages/hub-sessions/src/workflow-allocation-service.ts index 0a6ef7af6..6c6aec1d4 100644 --- a/packages/hub-sessions/src/workflow-allocation-service.ts +++ b/packages/hub-sessions/src/workflow-allocation-service.ts @@ -5,6 +5,7 @@ import { createSidecarAllocationStore, createWorkflowProbeStore, createWorkflowRunLaunchSpecStore, + insertRunPrincipal, resolveTenantSidecarCapabilityPolicies, resolveDeploymentLifecyclePolicy, canExecuteWorkflowRun, @@ -17,7 +18,7 @@ import { } from "@intx/db"; import { grant, workflowRun } from "@intx/db/schema"; import { eq } from "drizzle-orm"; -import { generateId } from "@intx/hub-common"; +import { deriveRunPrincipalId, generateId } from "@intx/hub-common"; import { getLogger } from "@intx/log"; import { hexEncode, @@ -471,6 +472,19 @@ export function createWorkflowAllocationService({ expiresAt: lifecycleDeadline(createdAt, lifecycle.maxLifetime), createdAt, }); + // The deployment's run principal exists from deploy. It holds no hub + // key, and the first trigger links it to the anchor. + const runPrincipalCreated = await insertRunPrincipal(tx, { + id: await deriveRunPrincipalId(request.tenantId, request.anchorRunId), + tenantId: request.tenantId, + runId: request.anchorRunId, + createdAt, + }); + if (!runPrincipalCreated) { + throw new Error( + `Deployment ${request.anchorRunId} already has a run principal`, + ); + } const maxDisconnectedMs = lifecycleDurationMs(lifecycle.maxDisconnected); await tx.insert(grant).values({ id: generateId("grant"), diff --git a/packages/hub-sessions/src/ws/sidecar-events.ts b/packages/hub-sessions/src/ws/sidecar-events.ts index 6111c55bf..bb079a060 100644 --- a/packages/hub-sessions/src/ws/sidecar-events.ts +++ b/packages/hub-sessions/src/ws/sidecar-events.ts @@ -345,8 +345,8 @@ export type SidecarLookups = { * degrade-to-null path. * * Returns `null` when the sender does not resolve to a concrete principal -- - * an address matching no principal, or a run whose deploy is acked but whose - * principal is not yet minted (a principal-less signer). THROWS on a genuine + * an address matching no principal, or an initialized run whose first + * trigger has not yet linked its run principal. THROWS on a genuine * fault (an ambiguous address, a data-integrity break). Both a `null` and a * throw leave the sender with no admission coordinates at the seam, so the * gate never admits it. Wraps the strict `resolveSenderKey` in `@intx/db` diff --git a/tests/db/anchor-before-frame.test.ts b/tests/db/anchor-before-frame.test.ts index e7dab4635..f8dd2c034 100644 --- a/tests/db/anchor-before-frame.test.ts +++ b/tests/db/anchor-before-frame.test.ts @@ -38,6 +38,7 @@ import { DeploymentRejectedError } from "@intx/types/sidecar"; import { createNoopCredentialCipher, generateKeyPair } from "@intx/crypto"; import { createSidecarAllocationStore } from "@intx/db"; import { + principal, sidecarAllocation, workflowDefinition, workflowRun, @@ -58,7 +59,11 @@ import { harnessDbEnvAvailable, type TestDb, } from "@intx/test-harness/db-harness"; -import { seedTenants, seedWorkflowRun } from "@intx/test-harness/seed"; +import { + seedPrincipal, + seedTenants, + seedWorkflowRun, +} from "@intx/test-harness/seed"; import type { HarnessConfig, InferenceSource, @@ -275,6 +280,15 @@ describe.skipIf(!harnessDbEnvAvailable())( }); } + async function readRunPrincipalStatus(): Promise { + const [row] = await h.db + .select({ status: principal.status }) + .from(principal) + .where(eq(principal.refId, ANCHOR_RUN_ID)) + .limit(1); + return row?.status; + } + async function readAnchor(): Promise< { status: string; publicKey: string | null } | undefined > { @@ -386,8 +400,10 @@ describe.skipIf(!harnessDbEnvAvailable())( }), ); await expectLeaked(deployWith(router), false); - // Proven never on the wire: the anchor is fully rolled back. + // Proven never on the wire: the anchor and its run principal are fully + // rolled back. expect(await readAnchor()).toBeUndefined(); + expect(await readRunPrincipalStatus()).toBeUndefined(); }); test("a sent-but-unacked frame (frameSent:true) fences the anchor and reports a leak", async () => { @@ -395,10 +411,12 @@ describe.skipIf(!harnessDbEnvAvailable())( Object.assign(new Error("ack timeout"), { frameSent: true }), ); await expectLeaked(deployWith(router), true); - // A child may be live: the anchor is fenced to failed, not deleted. + // A child may be live: the anchor is fenced to failed, not deleted, and + // its run principal is deactivated. const row = await readAnchor(); expect(row?.status).toBe("failed"); expect(row?.publicKey).toBeNull(); + expect(await readRunPrincipalStatus()).toBe("deactivated"); }); test("an untagged emit error is treated as possibly-live: fence and report a leak", async () => { @@ -542,6 +560,25 @@ describe.skipIf(!harnessDbEnvAvailable())( expect((await readAnchor())?.status).toBe("running"); }); + test("an existing run principal fails the deploy closed before the frame with no leak", async () => { + // A principal already answers for this run, so deploy does not adopt + // it: the anchor insert rolls back and the frame never goes out. + await seedPrincipal(h.db, { + id: "prn_stale_run", + tenantId: TENANT_ID, + kind: "workflow", + refId: ANCHOR_RUN_ID, + status: "active", + }); + let sendCalled = false; + const router = throwingRouter(new Error("unused"), async () => { + sendCalled = true; + }); + await expectLeaked(deployWith(router), false); + expect(sendCalled).toBe(false); + expect(await readAnchor()).toBeUndefined(); + }); + test("a frameSent:false failure whose anchor already vanished refuses to claim safe", async () => { // The guarded delete finds 0 rows: the frameSent:false contract lied, so // a child may be live and the failure must NOT be reported as safe. diff --git a/tests/db/mail-accept-backfill.test.ts b/tests/db/mail-accept-backfill.test.ts index ffec9ee25..61ae22782 100644 --- a/tests/db/mail-accept-backfill.test.ts +++ b/tests/db/mail-accept-backfill.test.ts @@ -376,10 +376,6 @@ describe.skipIf(!harnessDbEnvAvailable())( cipher: createTestCredentialCipher(), }), db: h.db, - principalKeyStore: createPrincipalKeyStore({ - db: h.db, - cipher: createTestCredentialCipher(), - }), grantStoreFor: createGrantStore, }); return materializer({ agentAddress, runId, ...sender }); diff --git a/tests/db/mail-triggered-run-grants.test.ts b/tests/db/mail-triggered-run-grants.test.ts index 7915a229e..24b23c429 100644 --- a/tests/db/mail-triggered-run-grants.test.ts +++ b/tests/db/mail-triggered-run-grants.test.ts @@ -7,7 +7,7 @@ import { test, } from "bun:test"; -import { eq, sql } from "drizzle-orm"; +import { and, eq, sql } from "drizzle-orm"; import { createGrantStore, @@ -18,6 +18,8 @@ import { import { grant, principal, + principalRole, + role, runToken, workflowDefinition, workflowDefinitionVersion, @@ -169,10 +171,6 @@ describe.skipIf(!harnessDbEnvAvailable())( ): ReturnType> { const materialize = createMailTriggeredRunGrantsMaterializer({ db: h.db, - principalKeyStore: createPrincipalKeyStore({ - db: h.db, - cipher: createTestCredentialCipher(), - }), runTokenStore: createRunTokenStore({ db: h.db, cipher: createTestCredentialCipher(), @@ -234,10 +232,6 @@ describe.skipIf(!harnessDbEnvAvailable())( // first deployment's snapshot to the second. const materialize = createMailTriggeredRunGrantsMaterializer({ db: h.db, - principalKeyStore: createPrincipalKeyStore({ - db: h.db, - cipher: createTestCredentialCipher(), - }), runTokenStore: createRunTokenStore({ db: h.db, cipher: createTestCredentialCipher(), @@ -1019,10 +1013,6 @@ describe.skipIf(!harnessDbEnvAvailable())( cipher: createTestCredentialCipher(), }), db: h.db, - principalKeyStore: createPrincipalKeyStore({ - db: h.db, - cipher: createTestCredentialCipher(), - }), grantStoreFor: () => countingGrantStore, }); @@ -1373,49 +1363,158 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(await runGrantCount("prn_not_derived")).toBe(0); }); - test("the first commit refuses an unlinked run principal that is not active", async () => { - await seedFrozenSnapshot(snapshot("secret:vault")); + async function seedRunPrincipal( + status: "active" | "suspended" | "deactivated", + createdAt = new Date(), + ): Promise { const derived = await deriveRunPrincipalId(TENANT, RUN_ID); - await seedPrincipal(h.db, { + await h.db.insert(principal).values({ id: derived, tenantId: TENANT, kind: "workflow", refId: RUN_ID, - status: "deactivated", + status, + createdAt, + updatedAt: createdAt, }); + return derived; + } - await expect(materializeOnce(RUN_ID)).rejects.toThrow( - /is deactivated, not active/, - ); - expect(await runGrantCount(derived)).toBe(0); - }); + for (const status of ["suspended", "deactivated"] as const) { + test(`the first fire refuses a run principal an operator ${status}`, async () => { + await seedFrozenSnapshot(snapshot("secret:vault")); + const derived = await seedRunPrincipal(status); + + const result = await materializeOnce(RUN_ID); - test("the first commit refuses an unlinked run principal that already holds grants", async () => { + expect(result).toMatchObject({ + outcome: "rejected", + status: 409, + code: "workflow_run_principal_inactive", + }); + expect(await runGrantCount(derived)).toBe(0); + }); + } + + test("an operator mail.accept deny through a role refuses the first mail", async () => { await seedFrozenSnapshot(snapshot("secret:vault")); - const derived = await deriveRunPrincipalId(TENANT, RUN_ID); - await seedPrincipal(h.db, { - id: derived, + const derived = await seedRunPrincipal("active"); + await h.db.insert(role).values({ + id: "rol_block_tenant_mail", tenantId: TENANT, - kind: "workflow", - refId: RUN_ID, - status: "active", + name: "block tenant mail", + }); + await seedGrant(h.db, { + id: "grt_role_deny", + tenantId: TENANT, + roleId: "rol_block_tenant_mail", + resource: mailAcceptResource("tenant", TENANT), + action: "accept", + effect: "deny", + origin: "system", }); + await h.db.insert(principalRole).values({ + principalId: derived, + roleId: "rol_block_tenant_mail", + }); + + const result = await materializeOnce(RUN_ID); + + expect(result).toMatchObject({ + outcome: "rejected", + code: "mail_admission_denied", + }); + expect(await runGrantCount(derived)).toBe(0); + }); + + test("the first commit keeps an operator grant given before the first trigger", async () => { + await seedFrozenSnapshot(snapshot("secret:vault")); + const derived = await seedRunPrincipal("active"); await seedGrant(h.db, { id: "grt_before_commit", tenantId: TENANT, principalId: derived, resource: "tool:write_file", - action: "use", + action: "invoke", effect: "allow", - origin: "creator", + origin: "system", }); - await expect(materializeOnce(RUN_ID)).rejects.toThrow( - /already holds grants before its first commit/, + const result = await materializeOnce(RUN_ID); + + if (result.outcome !== "materialized") { + throw new Error(`expected materialized, got ${result.outcome}`); + } + expect(result.stepGrants.map((g) => g.id)).toContain( + "grt_before_commit", ); + expect(await runGrantCount(derived)).toBe(result.stepGrants.length); + expect(await runGrantCount(derived)).toBeGreaterThan(1); + }); + + test("an operator mail.accept deny given before the first trigger refuses the first mail", async () => { + await seedFrozenSnapshot(snapshot("secret:vault")); + const derived = await seedRunPrincipal("active"); + await seedGrant(h.db, { + id: "grt_operator_deny", + tenantId: TENANT, + principalId: derived, + resource: mailAcceptResource("tenant", TENANT), + action: "accept", + effect: "deny", + origin: "system", + }); + + const result = await materializeOnce(RUN_ID); + + expect(result).toMatchObject({ + outcome: "rejected", + code: "mail_admission_denied", + }); expect(await runGrantCount(derived)).toBe(1); }); + async function invokerRowsAfterFirstFire( + deployedAgoMs: number, + ): Promise<{ expiresAt: Date | null }[]> { + await seedFrozenSnapshot({ + ...snapshot("secret:vault"), + grantRequirements: [ + { resource: "secret:vault", action: "use", source: "invoker" }, + ], + }); + const deployedAt = new Date(Date.now() - deployedAgoMs); + const derived = await seedRunPrincipal("active", deployedAt); + + const result = await materializeOnce(RUN_ID); + + expect(result.outcome).toBe("materialized"); + return h.db + .select({ expiresAt: grant.expiresAt }) + .from(grant) + .where( + and(eq(grant.principalId, derived), eq(grant.origin, "invoker")), + ); + } + + test("a first fire within a day of deploy gets an invoker allow row that ends a day after deploy", async () => { + const hourMs = 60 * 60 * 1000; + const rows = await invokerRowsAfterFirstFire(hourMs); + + expect(rows).toHaveLength(1); + const expiresAt = rows[0]?.expiresAt?.getTime() ?? 0; + // The run principal was created an hour ago, so the row has about + // 23 hours left, not a full day from the first fire. + expect(expiresAt).toBeLessThan(Date.now() + 24 * hourMs - hourMs / 2); + expect(expiresAt).toBeGreaterThan(Date.now() + 22 * hourMs); + }); + + test("a first fire more than a day after deploy gets no invoker allow row", async () => { + const rows = await invokerRowsAfterFirstFire(2 * 24 * 60 * 60 * 1000); + + expect(rows).toHaveLength(0); + }); + test("the first commit throws when the live run row cannot be linked", async () => { // A live run row with no principal link that is not a "deployed" // anchor: the guarded link updates nothing, which no first commit @@ -1561,6 +1660,60 @@ describe.skipIf(!harnessDbEnvAvailable())( } expect(await runGrantCount()).toBe(0); }); + + test("an operator allow cannot admit the first mail once the deployer rule is gone", async () => { + // As above, but an operator also allowed the sender on the run + // principal before its first trigger. A first mail needs a declared + // rule, so the operator allow does not admit it. + await seedFrozenSnapshot({ + perStep: [ + { + stepId: "work", + grants: [MAIL_TRIGGER_GRANT, "mail.accept:invoker"], + grantEffects: {}, + }, + ], + grantRequirements: [], + }); + const runPrincipalId = await deriveRunPrincipalId(TENANT, RUN_ID); + await seedPrincipal(h.db, { + id: runPrincipalId, + tenantId: TENANT, + kind: "workflow", + refId: RUN_ID, + status: "active", + }); + await seedGrant(h.db, { + id: "grt_operator_allow", + tenantId: TENANT, + principalId: runPrincipalId, + resource: mailAcceptResource("principal", CREATOR), + action: "accept", + effect: "allow", + origin: "system", + }); + + const result = await firstFireAcross( + async (tx) => { + await tx + .update(principal) + .set({ status: "deactivated" }) + .where(eq(principal.id, CREATOR)); + }, + { + senderCoordinates: [ + { coordType: "principal", id: CREATOR }, + { coordType: "tenant", id: TENANT }, + ], + }, + ); + + expect(result.outcome).toBe("rejected"); + if (result.outcome === "rejected") { + expect(result.code).toBe("mail_admission_denied"); + } + expect(await runGrantCount()).toBe(1); + }); }); describe("createCorrespondentGrantMinter (send-seam)", () => { @@ -2096,7 +2249,6 @@ describe.skipIf(!harnessDbEnvAvailable())( cipher: createTestCredentialCipher(), }), db: h.db, - principalKeyStore: keyStore(), grantStoreFor: createGrantStore, }); } diff --git a/tests/db/principal-store.test.ts b/tests/db/principal-store.test.ts index 9dc9dbfd0..d5ddc035b 100644 --- a/tests/db/principal-store.test.ts +++ b/tests/db/principal-store.test.ts @@ -12,6 +12,7 @@ import { createPrincipalStore, createPrincipalKeyStore, deactivateRunPrincipals, + insertRunPrincipal, type PrincipalKeyStore, } from "@intx/db"; import { principal, principalKey, runToken } from "@intx/db/schema"; @@ -69,28 +70,21 @@ describe.skipIf(!harnessDbEnvAvailable())( return rows.length; } - test("createIfAbsent inserts, mints a key, and is idempotent", async () => { - const store = newStore(); - const now = new Date(); + test("insertRunPrincipal inserts a keyless run principal once per run", async () => { + const createdAt = new Date("2026-01-02T03:04:05Z"); const row = { - id: "prn_first", + id: "prn_run_first", tenantId: TENANT, - kind: "user" as const, - refId: "usr_shared", - status: "active" as const, - createdAt: now, - updatedAt: now, + runId: "run_shared", + createdAt, }; - const first = await store.createIfAbsent(row); - expect(first?.id).toBe("prn_first"); - expect(await activeKeyCount("prn_first")).toBe(1); - - // A second reservation of the same (tenantId, kind, refId) -- even with a - // different surrogate id -- inserts no row and mints no second key: the - // winning transaction already minted the one active key. - const second = await store.createIfAbsent({ ...row, id: "prn_second" }); - expect(second).toBeNull(); + expect(await insertRunPrincipal(h.db, row)).toBe(true); + // A second insert for the same run -- even with a different surrogate + // id -- inserts no row. + expect( + await insertRunPrincipal(h.db, { ...row, id: "prn_run_second" }), + ).toBe(false); const rows = await h.db .select() @@ -98,14 +92,21 @@ describe.skipIf(!harnessDbEnvAvailable())( .where( and( eq(principal.tenantId, TENANT), - eq(principal.kind, "user"), - eq(principal.refId, "usr_shared"), + eq(principal.kind, "workflow"), + eq(principal.refId, "run_shared"), ), ); expect(rows).toHaveLength(1); - expect(rows[0]?.id).toBe("prn_first"); - expect(await activeKeyCount("prn_first")).toBe(1); - expect(await activeKeyCount("prn_second")).toBe(0); + expect(rows[0]).toMatchObject({ + id: "prn_run_first", + status: "active", + createdAt, + }); + const keys = await h.db + .select() + .from(principalKey) + .where(eq(principalKey.principalId, "prn_run_first")); + expect(keys).toHaveLength(0); }); test("create mints an active signing key for the new principal", async () => { @@ -175,7 +176,8 @@ describe.skipIf(!harnessDbEnvAvailable())( test("a failed key mint rolls back the principal insert", async () => { // The key store throws on mint, so the whole transaction -- the principal // insert included -- must roll back and leave no principal row. This pins - // the feature's headline invariant: a principal never exists without a key. + // the store's invariant: a principal it creates never exists without a + // key. const throwingKeyStore: PrincipalKeyStore = { generate: async () => { throw new Error("mint boom"); @@ -200,19 +202,6 @@ describe.skipIf(!harnessDbEnvAvailable())( }), ).rejects.toThrow(/mint boom/); expect(await principalExists("prn_create_rollback")).toBe(false); - - await expect( - store.createIfAbsent({ - id: "prn_ifabsent_rollback", - tenantId: TENANT, - kind: "user", - refId: "usr_ifabsent_rollback", - status: "active", - createdAt: now, - updatedAt: now, - }), - ).rejects.toThrow(/mint boom/); - expect(await principalExists("prn_ifabsent_rollback")).toBe(false); }); }, ); diff --git a/tests/db/sidecar-allocation-store.test.ts b/tests/db/sidecar-allocation-store.test.ts index 560e72277..4109541ae 100644 --- a/tests/db/sidecar-allocation-store.test.ts +++ b/tests/db/sidecar-allocation-store.test.ts @@ -5928,6 +5928,14 @@ describe.skipIf(!harnessDbEnvAvailable())( .update(workflowRun) .set({ status: "deployed" }) .where(eq(workflowRun.id, ANCHOR_RUN_ID)); + // Deploy created the run principal; no trigger has linked it yet. + await seedPrincipal(h.db, { + id: "prn-deployed-anchor", + tenantId: TENANT_ID, + kind: "workflow", + refId: ANCHOR_RUN_ID, + status: "active", + }); const store = createSidecarAllocationStore(h.db); const pending = await store.createPending({ @@ -5974,6 +5982,12 @@ describe.skipIf(!harnessDbEnvAvailable())( columns: { status: true, endedAt: true }, }), ).toEqual({ status: "failed", endedAt }); + expect( + await h.db.query.principal.findFirst({ + where: (row, { eq }) => eq(row.id, "prn-deployed-anchor"), + columns: { status: true }, + }), + ).toEqual({ status: "deactivated" }); }); test("fails active runs when a lost worker is released", async () => { diff --git a/tests/db/workflow-allocation-service.test.ts b/tests/db/workflow-allocation-service.test.ts index fd0a055da..dda299cef 100644 --- a/tests/db/workflow-allocation-service.test.ts +++ b/tests/db/workflow-allocation-service.test.ts @@ -17,11 +17,14 @@ import { SidecarInventoryUnavailableError, } from "@intx/db"; import { + principal, + principalKey, sidecar, workflowDefinition, workflowProbe, workflowRun, } from "@intx/db/schema"; +import { deriveRunPrincipalId } from "@intx/hub-common"; import { eq, sql } from "drizzle-orm"; import { adapterManifestDigest, @@ -945,6 +948,32 @@ describe.skipIf(!harnessDbEnvAvailable())( const prepared = await service.prepareProvisionedDeployment( prepareArgs("run-anchor-ordering"), ); + + // The deploy transaction creates the run principal with the anchor, + // unlinked and with no hub key. + const anchor = await h.db.query.workflowRun.findFirst({ + where: eq(workflowRun.id, prepared.anchorRunId), + }); + const runPrincipals = await h.db + .select() + .from(principal) + .where(eq(principal.refId, prepared.anchorRunId)); + expect(anchor?.principalId).toBeNull(); + expect(runPrincipals).toEqual([ + expect.objectContaining({ + id: await deriveRunPrincipalId(TENANT_ID, prepared.anchorRunId), + kind: "workflow", + status: "active", + createdAt: anchor?.createdAt, + }), + ]); + expect( + await h.db + .select() + .from(principalKey) + .where(eq(principalKey.principalId, runPrincipals[0]?.id ?? "")), + ).toEqual([]); + const allocation = await createSidecarAllocationStore( h.db, ).findByAnchorRunId(prepared.anchorRunId); diff --git a/tests/hub-api/grant-routes.test.ts b/tests/hub-api/grant-routes.test.ts index aa28f9a53..02011ea3f 100644 --- a/tests/hub-api/grant-routes.test.ts +++ b/tests/hub-api/grant-routes.test.ts @@ -425,7 +425,7 @@ describe.skipIf(!harnessDbEnvAvailable())( describe("on a workflow run's principal", () => { const TOOL = { tool: "tool:pkg" }; - async function setupRun(slug: string) { + async function setupRun(slug: string, opts = { fired: true }) { const app = makeApp(); const { tenantId, roleId } = await bootstrapTenant(app, slug); const principalId = `prn_run_${slug}`; @@ -435,10 +435,13 @@ describe.skipIf(!harnessDbEnvAvailable())( kind: "workflow", refId: `run_${slug}`, }); + // A run that has not fired is "deployed", and its principal is + // not linked to the run row yet. await seedWorkflowRun(h.db, { id: `run_${slug}`, tenantId, - principalId, + principalId: opts.fired ? principalId : null, + status: opts.fired ? "running" : "deployed", }); const runGrant = ( resource: string, @@ -470,6 +473,23 @@ describe.skipIf(!harnessDbEnvAvailable())( ); }); + test("POST refuses a tool condition on a run that has not fired", async () => { + const { app, tenantId, runGrant } = await setupRun("run-unfired", { + fired: false, + }); + + const res = await postGrant( + app, + tenantId, + runGrant("tool:search", "invoke", TOOL), + ); + + expect(res.status).toBe(400); + expect(ErrorBody.assert(await res.json()).error.message).toContain( + "a workflow run cannot evaluate this grant", + ); + }); + test("POST accepts a tool condition on credential use", async () => { const { app, tenantId, runGrant } = await setupRun("run-cred"); diff --git a/tests/hub-api/trigger-delegator-race.test.ts b/tests/hub-api/trigger-delegator-race.test.ts index e26248f78..38f736281 100644 --- a/tests/hub-api/trigger-delegator-race.test.ts +++ b/tests/hub-api/trigger-delegator-race.test.ts @@ -17,7 +17,13 @@ import { type RepoStore, type SessionService, } from "@intx/hub-sessions"; -import { grant, principal, workflowDefinition } from "@intx/db/schema"; +import { + grant, + principal, + workflowDefinition, + workflowRun, +} from "@intx/db/schema"; +import { deriveRunPrincipalId } from "@intx/hub-common"; import type { GrantRequirement } from "@intx/types"; import { createTestDb, @@ -125,9 +131,13 @@ describe.skipIf(!harnessDbEnvAvailable())( }); // Create the tenant through the route, so the caller is its owner, then - // seed a deployment that has not fired: an anchor run with no run - // principal and a frozen snapshot that holds `requirements`. - async function setup(requirements: GrantRequirement[]) { + // seed a deployment that has not fired: an anchor run, the unlinked run + // principal deploy creates, and a frozen snapshot that holds + // `requirements`. + async function setup( + requirements: GrantRequirement[], + runPrincipalStatus: "active" | "suspended" = "active", + ) { const router = createSidecarRouter({ withExecutableWorkflowRun: async (_target, send) => send(), authenticateSidecar: async ({ sidecarId }) => ({ sidecarId }), @@ -180,13 +190,33 @@ describe.skipIf(!harnessDbEnvAvailable())( anchorRunId: ANCHOR, definitionId: "wfd_first", address: `${ANCHOR}@acme.example`, + status: "deployed", + }); + const runPrincipalId = await deriveRunPrincipalId(tenantId, ANCHOR); + await seedPrincipal(h.db, { + id: runPrincipalId, + tenantId, + kind: "workflow", + refId: ANCHOR, + status: runPrincipalStatus, }); await seedWorkflowRunLaunchSpec(h.db, { anchorRunId: ANCHOR, sourceAuthorityPrincipalId: DEPLOYER, grantSnapshot: { perStep: [], grantRequirements: requirements }, }); - return { app, tenantId }; + return { app, tenantId, runPrincipalId }; + } + + async function trigger( + app: Awaited>["app"], + tenantId: string, + ) { + return app.request(`/api/tenants/${tenantId}/workflows/${ANCHOR}/mail`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ content: "hi" }), + }); } async function waitForBlocked(blockerPid: number) { @@ -298,5 +328,55 @@ describe.skipIf(!harnessDbEnvAvailable())( }); expect(await runGrantCount(tenantId)).toBe(0); }); + + test("the first trigger keeps an operator grant on the run principal and links it", async () => { + const { app, tenantId, runPrincipalId } = await setup([]); + await seedGrant(h.db, { + id: "grt_operator_tool", + tenantId, + principalId: runPrincipalId, + resource: "tool:write_file", + action: "invoke", + effect: "allow", + origin: "system", + }); + + // The deploy ack recorded the anchor's initialization. + await h.db + .update(workflowRun) + .set({ publicKey: "ab".repeat(32) }) + .where(eq(workflowRun.id, ANCHOR)); + + await trigger(app, tenantId); + + const [anchor] = await h.db + .select({ principalId: workflowRun.principalId }) + .from(workflowRun) + .where(eq(workflowRun.id, ANCHOR)); + expect(anchor?.principalId).toBe(runPrincipalId); + const ids = ( + await h.db + .select({ id: grant.id }) + .from(grant) + .where(eq(grant.principalId, runPrincipalId)) + ).map((row) => row.id); + expect(ids).toContain("grt_operator_tool"); + }); + + test("the first trigger refuses a suspended run principal", async () => { + const { app, tenantId } = await setup([], "suspended"); + + const res = await trigger(app, tenantId); + + expect(res.status).toBe(409); + expect(await res.json()).toMatchObject({ + error: { code: "workflow_run_principal_inactive" }, + }); + const [anchor] = await h.db + .select({ principalId: workflowRun.principalId }) + .from(workflowRun) + .where(eq(workflowRun.id, ANCHOR)); + expect(anchor?.principalId).toBeNull(); + }); }, ); diff --git a/tests/workflow-deploy/invoker-hub-access.test.ts b/tests/workflow-deploy/invoker-hub-access.test.ts index 4fb39175f..667b8ce7c 100644 --- a/tests/workflow-deploy/invoker-hub-access.test.ts +++ b/tests/workflow-deploy/invoker-hub-access.test.ts @@ -18,7 +18,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { and, eq } from "drizzle-orm"; -import { grant, principal, runToken } from "@intx/db/schema"; +import { grant, principal, runToken, workflowRun } from "@intx/db/schema"; import { deriveRunPrincipalId } from "@intx/hub-common"; import type { GrantRequirement } from "@intx/types"; import { harnessDbEnvAvailable } from "@intx/test-harness/db-harness"; @@ -131,9 +131,17 @@ describe.skipIf(!harnessDbEnvAvailable())( throw new Error("expected an expiring self-read grant on the run"); } expect(selfRead.origin).toBe("invoker"); - expect(selfRead.expiresAt.getTime() - selfRead.createdAt.getTime()).toBe( - INVOKER_GRANT_LIFETIME_MS, - ); + // The lifetime counts from the run principal's creation, at deploy. + const [runPrincipalA] = await t.h.db + .select({ createdAt: principal.createdAt }) + .from(principal) + .where(eq(principal.id, principalA)); + if (runPrincipalA === undefined) { + throw new Error("expected the run principal deploy created"); + } + expect( + selfRead.expiresAt.getTime() - runPrincipalA.createdAt.getTime(), + ).toBe(INVOKER_GRANT_LIFETIME_MS); await deploy(RUN_B, { resource: "secret:vault", @@ -148,12 +156,20 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(await refused.json()).toMatchObject({ error: { code: "insufficient_grants" }, }); + // Deploy created run B's principal; the refused trigger neither linked + // it to the run nor gave it grants or tokens. const principalB = await deriveRunPrincipalId(TENANT_ID, RUN_B); expect( await t.h.db - .select({ id: principal.id }) - .from(principal) - .where(eq(principal.id, principalB)), + .select({ principalId: workflowRun.principalId }) + .from(workflowRun) + .where(eq(workflowRun.id, RUN_B)), + ).toEqual([{ principalId: null }]); + expect( + await t.h.db + .select({ id: grant.id }) + .from(grant) + .where(eq(grant.principalId, principalB)), ).toEqual([]); expect( await t.h.db diff --git a/tests/workflow-deploy/mail-federation-run-completes.test.ts b/tests/workflow-deploy/mail-federation-run-completes.test.ts index 94f499fa4..ca9f6b1ea 100644 --- a/tests/workflow-deploy/mail-federation-run-completes.test.ts +++ b/tests/workflow-deploy/mail-federation-run-completes.test.ts @@ -64,6 +64,7 @@ import { workflowRun as workflowRunTable, } from "@intx/db/schema"; import { createMailTriggeredRunGrantsMaterializer } from "@intx/hub-api"; +import { deriveRunPrincipalId } from "@intx/hub-common"; import type { HarnessConfig, InferenceSource } from "@intx/types/runtime"; import type { WireGrantRule } from "@intx/types/grant-wire"; import { @@ -124,13 +125,16 @@ const SENDER_TENANT_ID = "tnt_fed_mail_sender"; const SENDER_CREATOR_PRINCIPAL_ID = "prn_fed_mail_sender_creator"; const SENDER_ASSET_ID = "ast_fed_mail_sender_wf"; const SENDER_STEP_ID = "send"; -// The sender's run principal. A sender triggered through fireMailTrigger reserves -// no run principal (it hand-delivers grants and routes without the commit path), -// so its anchor is left principal-less and resolveSenderPrincipal would fail the -// admission coordinates closed. Mint one and attach it to the sender anchor so -// the sender resolves to concrete coordinates the receiver's accept-policy can -// admit. -const SENDER_RUN_PRINCIPAL_ID = "prn_fed_mail_sender_run"; +// The sender's run principal, which its deploy creates. A sender triggered +// through fireMailTrigger never runs the commit path (it hand-delivers grants +// and routes without it), so nothing links the principal to its anchor and +// resolveSenderPrincipal would fail the admission coordinates closed. The +// test links it so the sender resolves to concrete coordinates the +// receiver's accept-policy can admit. +const SENDER_RUN_PRINCIPAL_ID = await deriveRunPrincipalId( + SENDER_TENANT_ID, + SENDER_ID, +); const receiverAddress = deriveRunAddress({ runId: RECEIVER_ID, @@ -177,7 +181,6 @@ describe.skipIf(!harnessDbEnvAvailable())( }); const materializer = createMailTriggeredRunGrantsMaterializer({ db: h.db, - principalKeyStore, runTokenStore: createRunTokenStore({ db: h.db, cipher: createTestCredentialCipher(), @@ -402,18 +405,11 @@ describe.skipIf(!harnessDbEnvAvailable())( sources: { [SENDER_STEP_ID]: [inferenceSource("sk-mock-sender")] }, }); - // Attach a run principal to the sender's anchor so it resolves to concrete - // admission coordinates. The sender fires through fireMailTrigger, which - // never runs the commit path that would mint one, so without this the - // sender resolves principal-less and the receiver's start gate would drop - // its mail as an unknown sender. - await seedPrincipal(h.db, { - id: SENDER_RUN_PRINCIPAL_ID, - tenantId: SENDER_TENANT_ID, - kind: "workflow", - refId: SENDER_ID, - status: "active", - }); + // Link the sender's run principal to its anchor so it resolves to + // concrete admission coordinates. The sender fires through + // fireMailTrigger, which never runs the commit path that would link it, + // so without this the sender resolves principal-less and the receiver's + // start gate would drop its mail as an unknown sender. await h.db .update(workflowRunTable) .set({ principalId: SENDER_RUN_PRINCIPAL_ID }) From 9edba93c4e3387ed68ef47a7c94d7bfb93b977d1 Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 14:44:48 -0700 Subject: [PATCH 06/14] Create run principals for existing deployments --- .../migrations/0120_create_run_principals.sql | 40 + .../db/migrations/meta/0120_snapshot.json | 5007 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + .../db/migration-0120-run-principals.test.ts | 164 + 4 files changed, 5218 insertions(+) create mode 100644 packages/db/migrations/0120_create_run_principals.sql create mode 100644 packages/db/migrations/meta/0120_snapshot.json create mode 100644 tests/db/migration-0120-run-principals.test.ts diff --git a/packages/db/migrations/0120_create_run_principals.sql b/packages/db/migrations/0120_create_run_principals.sql new file mode 100644 index 000000000..4d18fe9b4 --- /dev/null +++ b/packages/db/migrations/0120_create_run_principals.sql @@ -0,0 +1,40 @@ +-- Give every existing deployment its run principal. A deployment's run +-- principal is created at deploy now, and the hub expects one for each +-- deployment anchor. A deployment that predates that has one only when its +-- first trigger created it. +-- +-- The principal is the `workflow` principal whose `ref_id` is the anchor run +-- id. Its id is the one `deriveRunPrincipalId` gives: `prn_` and the lowercase +-- hex of the first 16 bytes of SHA-256 over the UTF-8 tenant id, a zero byte, +-- and the UTF-8 run id. A trigger that fires the deployment later derives the +-- same id and links this principal. +-- +-- It gets no key: a run principal holds no hub key. Its `created_at` is the +-- anchor's, since a run principal is created at deploy. The invoker-grant +-- time limit counts from it, so a deployment older than that limit that has +-- not fired gets no invoker `allow` rows at its first trigger, as a new +-- deployment would. A deployment that is no longer live gets a deactivated +-- principal, updated when the anchor ended, as a stop deactivates one. An +-- anchor that already has its principal keeps it unchanged. +-- +-- A hub that predates run principals at deploy reads an existing run +-- principal as a committed first trigger, so it would start such a run with +-- no grants. Stop such hubs before this migration runs. +INSERT INTO "principal" ("id", "tenant_id", "kind", "ref_id", "status", "created_at", "updated_at") +SELECT + 'prn_' || encode( + substring( + sha256(convert_to(r."tenant_id", 'UTF8') || '\x00'::bytea || convert_to(r."id", 'UTF8')) + from 1 for 16 + ), + 'hex' + ), + r."tenant_id", + 'workflow', + r."id", + CASE WHEN r."status" IN ('deployed', 'running') THEN 'active' ELSE 'deactivated' END, + r."created_at", + CASE WHEN r."status" IN ('deployed', 'running') THEN r."created_at" ELSE coalesce(r."ended_at", r."created_at") END +FROM "workflow_run" r +WHERE r."anchor_run_id" = r."id" +ON CONFLICT ("tenant_id", "kind", "ref_id") DO NOTHING; diff --git a/packages/db/migrations/meta/0120_snapshot.json b/packages/db/migrations/meta/0120_snapshot.json new file mode 100644 index 000000000..352983bf5 --- /dev/null +++ b/packages/db/migrations/meta/0120_snapshot.json @@ -0,0 +1,5007 @@ +{ + "id": "682948ca-dc25-4066-81cc-338b44f6959d", + "prevId": "8141ae7e-e98a-4aec-ac5c-1d2301abd631", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_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 + }, + "id_token": { + "name": "id_token", + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "columnsFrom": ["user_id"], + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "columnsFrom": ["user_id"], + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "columns": ["token"], + "nullsNotDistinct": false + } + }, + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "columns": ["email"], + "nullsNotDistinct": false + } + }, + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_trust": { + "name": "federation_trust", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_tenant_id": { + "name": "target_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federation_trust_tenant_id_tenant_id_fk": { + "name": "federation_trust_tenant_id_tenant_id_fk", + "tableFrom": "federation_trust", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "federation_trust_target_tenant_id_tenant_id_fk": { + "name": "federation_trust_target_tenant_id_tenant_id_fk", + "tableFrom": "federation_trust", + "columnsFrom": ["target_tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federation_trust_tenant_id_target_tenant_id_unique": { + "name": "federation_trust_tenant_id_target_tenant_id_unique", + "columns": ["tenant_id", "target_tenant_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant": { + "name": "tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "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": { + "tenant_domain_lower_idx": { + "name": "tenant_domain_lower_idx", + "columns": [ + { + "expression": "lower(\"domain\")", + "isExpression": true, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tenant_parent_id_tenant_id_fk": { + "name": "tenant_parent_id_tenant_id_fk", + "tableFrom": "tenant", + "columnsFrom": ["parent_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tenant_slug_unique": { + "name": "tenant_slug_unique", + "columns": ["slug"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "tenant_slug_dns_label_check": { + "name": "tenant_slug_dns_label_check", + "value": "\"tenant\".\"slug\" ~ '^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$' and length(\"tenant\".\"slug\") <= 63" + } + }, + "isRLSEnabled": false + }, + "public.principal": { + "name": "principal", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref_id": { + "name": "ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "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": { + "principal_tenant_id_tenant_id_fk": { + "name": "principal_tenant_id_tenant_id_fk", + "tableFrom": "principal", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "principal_tenant_id_kind_ref_id_unique": { + "name": "principal_tenant_id_kind_ref_id_unique", + "columns": ["tenant_id", "kind", "ref_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_key": { + "name": "principal_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "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": { + "principal_key_one_active": { + "name": "principal_key_one_active", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"principal_key\".\"status\" = 'active'", + "concurrently": false + } + }, + "foreignKeys": { + "principal_key_principal_id_principal_id_fk": { + "name": "principal_key_principal_id_principal_id_fk", + "tableFrom": "principal_key", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "principal_key_public_key_unique": { + "name": "principal_key_public_key_unique", + "columns": ["public_key"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_role": { + "name": "agent_role", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "agent_role_agent_id_workflow_definition_id_fk": { + "name": "agent_role_agent_id_workflow_definition_id_fk", + "tableFrom": "agent_role", + "columnsFrom": ["agent_id"], + "tableTo": "workflow_definition", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "agent_role_role_id_role_id_fk": { + "name": "agent_role_role_id_role_id_fk", + "tableFrom": "agent_role", + "columnsFrom": ["role_id"], + "tableTo": "role", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "agent_role_agent_id_role_id_pk": { + "name": "agent_role_agent_id_role_id_pk", + "columns": ["agent_id", "role_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_role": { + "name": "principal_role", + "schema": "", + "columns": { + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "principal_role_principal_id_principal_id_fk": { + "name": "principal_role_principal_id_principal_id_fk", + "tableFrom": "principal_role", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "principal_role_role_id_role_id_fk": { + "name": "principal_role_role_id_role_id_fk", + "tableFrom": "principal_role", + "columnsFrom": ["role_id"], + "tableTo": "role", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "principal_role_principal_id_role_id_pk": { + "name": "principal_role_principal_id_role_id_pk", + "columns": ["principal_id", "role_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "role_tenant_id_tenant_id_fk": { + "name": "role_tenant_id_tenant_id_fk", + "tableFrom": "role", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grant": { + "name": "grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "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": { + "grant_invoker_allow_principal_idx": { + "name": "grant_invoker_allow_principal_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"grant\".\"origin\" = 'invoker' and \"grant\".\"effect\" = 'allow'", + "concurrently": false + } + }, + "foreignKeys": { + "grant_tenant_id_tenant_id_fk": { + "name": "grant_tenant_id_tenant_id_fk", + "tableFrom": "grant", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "grant_role_id_role_id_fk": { + "name": "grant_role_id_role_id_fk", + "tableFrom": "grant", + "columnsFrom": ["role_id"], + "tableTo": "role", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "grant_principal_id_principal_id_fk": { + "name": "grant_principal_id_principal_id_fk", + "tableFrom": "grant", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "grant_target_exactly_one": { + "name": "grant_target_exactly_one", + "value": "num_nonnulls(\"grant\".\"principal_id\", \"grant\".\"role_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.approval": { + "name": "approval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_address": { + "name": "agent_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_definition": { + "name": "tool_definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "tool_arguments": { + "name": "tool_arguments", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "timeout_at": { + "name": "timeout_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "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": { + "approval_tenant_status_idx": { + "name": "approval_tenant_status_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "approval_anchor_run_idx": { + "name": "approval_anchor_run_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "approval_run_idx": { + "name": "approval_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "approval_tenant_id_tenant_id_fk": { + "name": "approval_tenant_id_tenant_id_fk", + "tableFrom": "approval", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "approval_anchor_run_id_workflow_run_id_fk": { + "name": "approval_anchor_run_id_workflow_run_id_fk", + "tableFrom": "approval", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "approval_run_id_workflow_run_id_fk": { + "name": "approval_run_id_workflow_run_id_fk", + "tableFrom": "approval", + "columnsFrom": ["run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "approval_correlation_id_unique": { + "name": "approval_correlation_id_unique", + "columns": ["correlation_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signal_correlation": { + "name": "signal_correlation", + "schema": "", + "columns": { + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_address": { + "name": "agent_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_name": { + "name": "signal_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_id": { + "name": "signal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "signal_correlation_tenant_id_tenant_id_fk": { + "name": "signal_correlation_tenant_id_tenant_id_fk", + "tableFrom": "signal_correlation", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "signal_correlation_anchor_run_id_workflow_run_id_fk": { + "name": "signal_correlation_anchor_run_id_workflow_run_id_fk", + "tableFrom": "signal_correlation", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "signal_correlation_run_id_workflow_run_id_fk": { + "name": "signal_correlation_run_id_workflow_run_id_fk", + "tableFrom": "signal_correlation", + "columnsFrom": ["run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider": { + "name": "provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin": { + "name": "plugin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_base_url": { + "name": "api_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_info_url": { + "name": "user_info_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "provider_tenant_id_tenant_id_fk": { + "name": "provider_tenant_id_tenant_id_fk", + "tableFrom": "provider", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_tenant_name": { + "name": "provider_tenant_name", + "columns": ["tenant_id", "name"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "default_scopes": { + "name": "default_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "oauth_client_tenant_id_tenant_id_fk": { + "name": "oauth_client_tenant_id_tenant_id_fk", + "tableFrom": "oauth_client", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "oauth_client_provider_id_provider_id_fk": { + "name": "oauth_client_provider_id_provider_id_fk", + "tableFrom": "oauth_client", + "columnsFrom": ["provider_id"], + "tableTo": "provider", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_tenant_provider": { + "name": "oauth_client_tenant_provider", + "columns": ["tenant_id", "provider_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_version": { + "name": "secret_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "refresh_secret": { + "name": "refresh_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "credential_tenant_id_tenant_id_fk": { + "name": "credential_tenant_id_tenant_id_fk", + "tableFrom": "credential", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "credential_principal_id_principal_id_fk": { + "name": "credential_principal_id_principal_id_fk", + "tableFrom": "credential", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "credential_provider_id_provider_id_fk": { + "name": "credential_provider_id_provider_id_fk", + "tableFrom": "credential", + "columnsFrom": ["provider_id"], + "tableTo": "provider", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "credential_oauth_client_id_oauth_client_id_fk": { + "name": "credential_oauth_client_id_oauth_client_id_fk", + "tableFrom": "credential", + "columnsFrom": ["oauth_client_id"], + "tableTo": "oauth_client", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_tenant_name": { + "name": "credential_tenant_name", + "columns": ["tenant_id", "name"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset": { + "name": "asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_principal_id": { + "name": "creator_principal_id", + "type": "text", + "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": {}, + "foreignKeys": { + "asset_tenant_id_tenant_id_fk": { + "name": "asset_tenant_id_tenant_id_fk", + "tableFrom": "asset", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "asset_creator_principal_id_principal_id_fk": { + "name": "asset_creator_principal_id_principal_id_fk", + "tableFrom": "asset", + "columnsFrom": ["creator_principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "asset_tenant_kind_name": { + "name": "asset_tenant_kind_name", + "columns": ["tenant_id", "kind", "name"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transaction": { + "name": "transaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "wallet_id": { + "name": "wallet_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_id": { + "name": "sender_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "transaction_wallet_id_wallet_id_fk": { + "name": "transaction_wallet_id_wallet_id_fk", + "tableFrom": "transaction", + "columnsFrom": ["wallet_id"], + "tableTo": "wallet", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "transaction_run_id_workflow_run_id_fk": { + "name": "transaction_run_id_workflow_run_id_fk", + "tableFrom": "transaction", + "columnsFrom": ["run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wallet": { + "name": "wallet", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backend_type": { + "name": "backend_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "config": { + "name": "config", + "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": {}, + "foreignKeys": { + "wallet_tenant_id_tenant_id_fk": { + "name": "wallet_tenant_id_tenant_id_fk", + "tableFrom": "wallet", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.offering": { + "name": "offering", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing": { + "name": "pricing", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "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": {}, + "foreignKeys": { + "offering_agent_id_workflow_definition_id_fk": { + "name": "offering_agent_id_workflow_definition_id_fk", + "tableFrom": "offering", + "columnsFrom": ["agent_id"], + "tableTo": "workflow_definition", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "offering_tenant_id_tenant_id_fk": { + "name": "offering_tenant_id_tenant_id_fk", + "tableFrom": "offering", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model": { + "name": "model", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_name": { + "name": "canonical_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_tenant_id_tenant_id_fk": { + "name": "model_tenant_id_tenant_id_fk", + "tableFrom": "model", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_tenant_canonical_name": { + "name": "model_tenant_canonical_name", + "columns": ["tenant_id", "canonical_name"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_offering": { + "name": "model_offering", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deployment_tags": { + "name": "deployment_tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "capabilities": { + "name": "capabilities", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "quirks": { + "name": "quirks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_offering_tenant_id_tenant_id_fk": { + "name": "model_offering_tenant_id_tenant_id_fk", + "tableFrom": "model_offering", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "model_offering_model_id_model_id_fk": { + "name": "model_offering_model_id_model_id_fk", + "tableFrom": "model_offering", + "columnsFrom": ["model_id"], + "tableTo": "model", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "model_offering_provider_id_model_provider_id_fk": { + "name": "model_offering_provider_id_model_provider_id_fk", + "tableFrom": "model_offering", + "columnsFrom": ["provider_id"], + "tableTo": "model_provider", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_offering_tenant_model_provider": { + "name": "model_offering_tenant_model_provider", + "columns": ["tenant_id", "model_id", "provider_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_pricing": { + "name": "model_pricing", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "offering_id": { + "name": "offering_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_token_price": { + "name": "input_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_token_price": { + "name": "output_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_read_token_price": { + "name": "cache_read_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_write_token_price": { + "name": "cache_write_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thinking_token_price": { + "name": "thinking_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_request_fee": { + "name": "per_request_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_image_fee": { + "name": "per_image_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_audio_fee": { + "name": "per_audio_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effective_from": { + "name": "effective_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "model_pricing_tenant_id_tenant_id_fk": { + "name": "model_pricing_tenant_id_tenant_id_fk", + "tableFrom": "model_pricing", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "model_pricing_offering_id_model_offering_id_fk": { + "name": "model_pricing_offering_id_model_offering_id_fk", + "tableFrom": "model_pricing", + "columnsFrom": ["offering_id"], + "tableTo": "model_offering", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_pricing_offering_currency_effective_from": { + "name": "model_pricing_offering_currency_effective_from", + "columns": ["offering_id", "currency", "effective_from"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_provider": { + "name": "model_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin": { + "name": "plugin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wallet_id": { + "name": "wallet_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_provider_tenant_id_tenant_id_fk": { + "name": "model_provider_tenant_id_tenant_id_fk", + "tableFrom": "model_provider", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "model_provider_credential_id_credential_id_fk": { + "name": "model_provider_credential_id_credential_id_fk", + "tableFrom": "model_provider", + "columnsFrom": ["credential_id"], + "tableTo": "credential", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "model_provider_wallet_id_wallet_id_fk": { + "name": "model_provider_wallet_id_wallet_id_fk", + "tableFrom": "model_provider", + "columnsFrom": ["wallet_id"], + "tableTo": "wallet", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_provider_tenant_name": { + "name": "model_provider_tenant_name", + "columns": ["tenant_id", "name"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "model_provider_auth_xor": { + "name": "model_provider_auth_xor", + "value": "(\"model_provider\".\"credential_id\" is not null) <> (\"model_provider\".\"wallet_id\" is not null)" + } + }, + "isRLSEnabled": false + }, + "public.sidecar": { + "name": "sidecar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reconnect_window_ended_at": { + "name": "reconnect_window_ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'online'" + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sidecar_token_hash_sha256_unique": { + "name": "sidecar_token_hash_sha256_unique", + "columns": ["token_hash_sha256"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sidecar_allocation": { + "name": "sidecar_allocation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ensure_accepted_generation": { + "name": "ensure_accepted_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconciliation_lease_id": { + "name": "reconciliation_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reconciliation_lease_expires_at": { + "name": "reconciliation_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "initialization_lease_id": { + "name": "initialization_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ensure_attempts": { + "name": "ensure_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "destroy_attempts": { + "name": "destroy_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deployment_cleanup_confirmed": { + "name": "deployment_cleanup_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_hold_released": { + "name": "provider_hold_released", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "connect_deadline": { + "name": "connect_deadline", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "max_disconnected_ms": { + "name": "max_disconnected_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "first_deploy_failed_at": { + "name": "first_deploy_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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": { + "sidecar_allocation_anchor_run_idx": { + "name": "sidecar_allocation_anchor_run_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "sidecar_allocation_sidecar_idx": { + "name": "sidecar_allocation_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "sidecar_allocation_reconciliation_idx": { + "name": "sidecar_allocation_reconciliation_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing', 'releasing') and \"sidecar_allocation\".\"next_attempt_at\" is not null", + "concurrently": false + }, + "sidecar_allocation_dispatchable_anchor_idx": { + "name": "sidecar_allocation_dispatchable_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing')", + "concurrently": false + } + }, + "foreignKeys": { + "sidecar_allocation_anchor_run_id_workflow_run_id_fk": { + "name": "sidecar_allocation_anchor_run_id_workflow_run_id_fk", + "tableFrom": "sidecar_allocation", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "sidecar_allocation_tenant_id_tenant_id_fk": { + "name": "sidecar_allocation_tenant_id_tenant_id_fk", + "tableFrom": "sidecar_allocation", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "sidecar_allocation_sidecar_id_sidecar_id_fk": { + "name": "sidecar_allocation_sidecar_id_sidecar_id_fk", + "tableFrom": "sidecar_allocation", + "columnsFrom": ["sidecar_id"], + "tableTo": "sidecar", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "sidecar_allocation_status_check": { + "name": "sidecar_allocation_status_check", + "value": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing', 'releasing', 'destroy_failed', 'released', 'failed')" + }, + "sidecar_allocation_generation_check": { + "name": "sidecar_allocation_generation_check", + "value": "\"sidecar_allocation\".\"generation\" >= 0" + }, + "sidecar_allocation_max_disconnected_check": { + "name": "sidecar_allocation_max_disconnected_check", + "value": "\"sidecar_allocation\".\"max_disconnected_ms\" > 0" + }, + "sidecar_allocation_accepted_generation_check": { + "name": "sidecar_allocation_accepted_generation_check", + "value": "\"sidecar_allocation\".\"ensure_accepted_generation\" is null or \"sidecar_allocation\".\"ensure_accepted_generation\" <= \"sidecar_allocation\".\"generation\"" + }, + "sidecar_allocation_initialization_check": { + "name": "sidecar_allocation_initialization_check", + "value": "\"sidecar_allocation\".\"initialization_lease_id\" is null or (\"sidecar_allocation\".\"status\" = 'allocated' and \"sidecar_allocation\".\"ensure_accepted_generation\" is not null and \"sidecar_allocation\".\"ensure_accepted_generation\" = \"sidecar_allocation\".\"generation\")" + } + }, + "isRLSEnabled": false + }, + "public.agent_session": { + "name": "agent_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_session_tenant_id_tenant_id_fk": { + "name": "agent_session_tenant_id_tenant_id_fk", + "tableFrom": "agent_session", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "agent_session_agent_id_workflow_definition_id_fk": { + "name": "agent_session_agent_id_workflow_definition_id_fk", + "tableFrom": "agent_session", + "columnsFrom": ["agent_id"], + "tableTo": "workflow_definition", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "agent_session_principal_id_principal_id_fk": { + "name": "agent_session_principal_id_principal_id_fk", + "tableFrom": "agent_session", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_asset": { + "name": "session_asset", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_pack_sha": { + "name": "asset_pack_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "materialized_at": { + "name": "materialized_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_asset_pack_sha_idx": { + "name": "session_asset_pack_sha_idx", + "columns": [ + { + "expression": "asset_pack_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "session_asset_instance_id_mount_path_pk": { + "name": "session_asset_instance_id_mount_path_pk", + "columns": ["instance_id", "mount_path"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inference_turn": { + "name": "inference_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inference_turn_instance_id_started_at_idx": { + "name": "inference_turn_instance_id_started_at_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "inference_turn_session_id_agent_session_id_fk": { + "name": "inference_turn_session_id_agent_session_id_fk", + "tableFrom": "inference_turn", + "columnsFrom": ["session_id"], + "tableTo": "agent_session", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "inference_turn_tenant_id_tenant_id_fk": { + "name": "inference_turn_tenant_id_tenant_id_fk", + "tableFrom": "inference_turn", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_mail": { + "name": "session_mail", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "raw": { + "name": "raw", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_mail_instance_id_created_at_idx": { + "name": "session_mail_instance_id_created_at_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "session_mail_session_id_created_at_idx": { + "name": "session_mail_session_id_created_at_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "session_mail_session_id_agent_session_id_fk": { + "name": "session_mail_session_id_agent_session_id_fk", + "tableFrom": "session_mail", + "columnsFrom": ["session_id"], + "tableTo": "agent_session", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "session_mail_tenant_id_tenant_id_fk": { + "name": "session_mail_tenant_id_tenant_id_fk", + "tableFrom": "session_mail", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.turn_part": { + "name": "turn_part", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "turn_part_turn_id_inference_turn_id_fk": { + "name": "turn_part_turn_id_inference_turn_id_fk", + "tableFrom": "turn_part", + "columnsFrom": ["turn_id"], + "tableTo": "inference_turn", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "turn_part_session_id_agent_session_id_fk": { + "name": "turn_part_session_id_agent_session_id_fk", + "tableFrom": "turn_part", + "columnsFrom": ["session_id"], + "tableTo": "agent_session", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_token": { + "name": "git_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref_pattern": { + "name": "ref_pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actions": { + "name": "actions", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "git_token_user_id_name_active_idx": { + "name": "git_token_user_id_name_active_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"git_token\".\"revoked_at\" is null", + "concurrently": false + } + }, + "foreignKeys": { + "git_token_tenant_id_tenant_id_fk": { + "name": "git_token_tenant_id_tenant_id_fk", + "tableFrom": "git_token", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "no action" + }, + "git_token_user_id_user_id_fk": { + "name": "git_token_user_id_user_id_fk", + "tableFrom": "git_token", + "columnsFrom": ["user_id"], + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "git_token_principal_id_principal_id_fk": { + "name": "git_token_principal_id_principal_id_fk", + "tableFrom": "git_token", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_token_token_hash_sha256_unique": { + "name": "git_token_token_hash_sha256_unique", + "columns": ["token_hash_sha256"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.run_token": { + "name": "run_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "sealed_token": { + "name": "sealed_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "run_token_principal_consumer_idx": { + "name": "run_token_principal_consumer_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "run_token_principal_id_principal_id_fk": { + "name": "run_token_principal_id_principal_id_fk", + "tableFrom": "run_token", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "run_token_token_hash_sha256_unique": { + "name": "run_token_token_hash_sha256_unique", + "columns": ["token_hash_sha256"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_definition": { + "name": "workflow_definition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creator_principal_id": { + "name": "creator_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wire_hash": { + "name": "wire_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_requirements": { + "name": "grant_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "model_requirements": { + "name": "model_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_bindings": { + "name": "credential_bindings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lifecycle_policy": { + "name": "lifecycle_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'deployed'" + }, + "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": { + "workflow_definition_tenant_idx": { + "name": "workflow_definition_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_definition_asset_wire_hash_idx": { + "name": "workflow_definition_asset_wire_hash_idx", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "wire_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_definition_tenant_id_tenant_id_fk": { + "name": "workflow_definition_tenant_id_tenant_id_fk", + "tableFrom": "workflow_definition", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workflow_definition_creator_principal_id_principal_id_fk": { + "name": "workflow_definition_creator_principal_id_principal_id_fk", + "tableFrom": "workflow_definition", + "columnsFrom": ["creator_principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "no action" + }, + "workflow_definition_asset_id_asset_id_fk": { + "name": "workflow_definition_asset_id_asset_id_fk", + "tableFrom": "workflow_definition", + "columnsFrom": ["asset_id"], + "tableTo": "asset", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_definition_version": { + "name": "workflow_definition_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "approved_wire_hash": { + "name": "approved_wire_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_snapshot": { + "name": "grant_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_definition_version_definition_idx": { + "name": "workflow_definition_version_definition_idx", + "columns": [ + { + "expression": "definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_definition_version_definition_id_workflow_definition_id_fk": { + "name": "workflow_definition_version_definition_id_workflow_definition_id_fk", + "tableFrom": "workflow_definition_version", + "columnsFrom": ["definition_id"], + "tableTo": "workflow_definition", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workflow_definition_version_definition_version": { + "name": "workflow_definition_version_definition_version", + "columns": ["definition_id", "version"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run": { + "name": "workflow_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel_id": { + "name": "kernel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_preferences": { + "name": "model_preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_refs": { + "name": "credential_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lifecycle_policy": { + "name": "lifecycle_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "operator_cancellation_requested_at": { + "name": "operator_cancellation_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_deadline": { + "name": "cancellation_deadline", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity_release_at": { + "name": "capacity_release_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "infrastructure_failed_at": { + "name": "infrastructure_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "legacy_mail_accept": { + "name": "legacy_mail_accept", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_definition_idx": { + "name": "workflow_run_definition_idx", + "columns": [ + { + "expression": "definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_run_address_idx": { + "name": "workflow_run_address_idx", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"workflow_run\".\"address\" is not null", + "concurrently": false + }, + "workflow_run_anchor_idx": { + "name": "workflow_run_anchor_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run\".\"id\" = \"workflow_run\".\"anchor_run_id\"", + "concurrently": false + }, + "workflow_run_live_anchor_idx": { + "name": "workflow_run_live_anchor_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run\".\"id\" = \"workflow_run\".\"anchor_run_id\" and \"workflow_run\".\"status\" in ('deployed', 'running')", + "concurrently": false + }, + "workflow_run_infrastructure_failed_idx": { + "name": "workflow_run_infrastructure_failed_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run\".\"infrastructure_failed_at\" is not null", + "concurrently": false + }, + "workflow_run_sidecar_idx": { + "name": "workflow_run_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run\".\"sidecar_id\" is not null", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_run_definition_id_workflow_definition_id_fk": { + "name": "workflow_run_definition_id_workflow_definition_id_fk", + "tableFrom": "workflow_run", + "columnsFrom": ["definition_id"], + "tableTo": "workflow_definition", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workflow_run_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workflow_run_tenant_id_tenant_id_fk": { + "name": "workflow_run_tenant_id_tenant_id_fk", + "tableFrom": "workflow_run", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workflow_run_principal_id_principal_id_fk": { + "name": "workflow_run_principal_id_principal_id_fk", + "tableFrom": "workflow_run", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "set null" + }, + "workflow_run_sidecar_id_sidecar_id_fk": { + "name": "workflow_run_sidecar_id_sidecar_id_fk", + "tableFrom": "workflow_run", + "columnsFrom": ["sidecar_id"], + "tableTo": "sidecar", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_run_legacy_mail_accept_check": { + "name": "workflow_run_legacy_mail_accept_check", + "value": "\"workflow_run\".\"legacy_mail_accept\" is null or \"workflow_run\".\"legacy_mail_accept\" in ('invoker-correspondent', 'correspondent')" + } + }, + "isRLSEnabled": false + }, + "public.workflow_run_dispatch": { + "name": "workflow_run_dispatch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'mail'" + }, + "sender_address": { + "name": "sender_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_message": { + "name": "raw_message", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "step_grants": { + "name": "step_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "acknowledged_generation": { + "name": "acknowledged_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "delivery_lease_id": { + "name": "delivery_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_lease_expires_at": { + "name": "delivery_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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()" + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_dispatch_anchor_message_idx": { + "name": "workflow_run_dispatch_anchor_message_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_run_dispatch_delivery_idx": { + "name": "workflow_run_dispatch_delivery_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run_dispatch\".\"status\" = 'pending'", + "concurrently": false + }, + "workflow_run_dispatch_anchor_status_idx": { + "name": "workflow_run_dispatch_anchor_status_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_run_dispatch_unresolved_anchor_idx": { + "name": "workflow_run_dispatch_unresolved_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run_dispatch\".\"status\" in ('pending', 'acknowledged') or (\"workflow_run_dispatch\".\"status\" = 'abandoned' and \"workflow_run_dispatch\".\"next_attempt_at\" is not null)", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_run_dispatch_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_dispatch_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_dispatch", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_run_dispatch_status_check": { + "name": "workflow_run_dispatch_status_check", + "value": "\"workflow_run_dispatch\".\"status\" in ('pending', 'acknowledged', 'abandoned', 'settled', 'failed')" + }, + "workflow_run_dispatch_kind_check": { + "name": "workflow_run_dispatch_kind_check", + "value": "\"workflow_run_dispatch\".\"kind\" in ('mail', 'signal')" + }, + "workflow_run_dispatch_attempt_count_check": { + "name": "workflow_run_dispatch_attempt_count_check", + "value": "\"workflow_run_dispatch\".\"attempt_count\" >= 0" + }, + "workflow_run_dispatch_acknowledged_generation_check": { + "name": "workflow_run_dispatch_acknowledged_generation_check", + "value": "\"workflow_run_dispatch\".\"acknowledged_generation\" is null or \"workflow_run_dispatch\".\"acknowledged_generation\" >= 0" + }, + "workflow_run_dispatch_acknowledged_state_check": { + "name": "workflow_run_dispatch_acknowledged_state_check", + "value": "\"workflow_run_dispatch\".\"status\" <> 'acknowledged' or \"workflow_run_dispatch\".\"acknowledged_generation\" is not null" + }, + "workflow_run_dispatch_pending_schedule_check": { + "name": "workflow_run_dispatch_pending_schedule_check", + "value": "\"workflow_run_dispatch\".\"status\" <> 'pending' or \"workflow_run_dispatch\".\"next_attempt_at\" is not null" + }, + "workflow_run_dispatch_mail_sender_check": { + "name": "workflow_run_dispatch_mail_sender_check", + "value": "\"workflow_run_dispatch\".\"kind\" <> 'mail' or \"workflow_run_dispatch\".\"status\" in ('abandoned', 'settled', 'failed') or \"workflow_run_dispatch\".\"sender_address\" is not null" + } + }, + "isRLSEnabled": false + }, + "public.workflow_pending_projection": { + "name": "workflow_pending_projection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "history_required": { + "name": "history_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "workflow_pending_projection_anchor_idx": { + "name": "workflow_pending_projection_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_pending_projection_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_pending_projection_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_pending_projection", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run_launch_spec": { + "name": "workflow_run_launch_spec", + "schema": "", + "columns": { + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_domain": { + "name": "deployment_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_authority_principal_id": { + "name": "source_authority_principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "frozen_approval_bundle": { + "name": "frozen_approval_bundle", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "definition_artifact_id": { + "name": "definition_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_snapshot": { + "name": "grant_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_offering_ids": { + "name": "source_offering_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "default_source_offering_id": { + "name": "default_source_offering_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_package_pins": { + "name": "tool_package_pins", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_launch_spec_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_launch_spec_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_launch_spec", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workflow_run_launch_spec_source_authority_principal_id_principal_id_fk": { + "name": "workflow_run_launch_spec_source_authority_principal_id_principal_id_fk", + "tableFrom": "workflow_run_launch_spec", + "columnsFrom": ["source_authority_principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "workflow_run_launch_spec_definition_artifact_id_workflow_definition_artifact_id_fk": { + "name": "workflow_run_launch_spec_definition_artifact_id_workflow_definition_artifact_id_fk", + "tableFrom": "workflow_run_launch_spec", + "columnsFrom": ["definition_artifact_id"], + "tableTo": "workflow_definition_artifact", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run_execution": { + "name": "workflow_run_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_execution_run_id_id_idx": { + "name": "workflow_run_execution_run_id_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_run_execution_status_idx": { + "name": "workflow_run_execution_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_run_execution_run_id_workflow_run_id_fk": { + "name": "workflow_run_execution_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_execution", + "columnsFrom": ["run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_probe": { + "name": "workflow_probe", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_asset_id": { + "name": "definition_asset_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entry": { + "name": "entry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pin": { + "name": "pin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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": { + "workflow_probe_active_idx": { + "name": "workflow_probe_active_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_probe\".\"status\" in ('pending', 'provisioning', 'probing', 'releasing')", + "concurrently": false + }, + "workflow_probe_sidecar_idx": { + "name": "workflow_probe_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_probe_tenant_id_tenant_id_fk": { + "name": "workflow_probe_tenant_id_tenant_id_fk", + "tableFrom": "workflow_probe", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "workflow_probe_definition_asset_id_asset_id_fk": { + "name": "workflow_probe_definition_asset_id_asset_id_fk", + "tableFrom": "workflow_probe", + "columnsFrom": ["definition_asset_id"], + "tableTo": "asset", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "workflow_probe_sidecar_id_sidecar_id_fk": { + "name": "workflow_probe_sidecar_id_sidecar_id_fk", + "tableFrom": "workflow_probe", + "columnsFrom": ["sidecar_id"], + "tableTo": "sidecar", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_probe_status_check": { + "name": "workflow_probe_status_check", + "value": "\"workflow_probe\".\"status\" in ('pending', 'provisioning', 'probing', 'releasing', 'succeeded', 'failed')" + }, + "workflow_probe_succeeded_result_check": { + "name": "workflow_probe_succeeded_result_check", + "value": "\"workflow_probe\".\"status\" <> 'succeeded' or \"workflow_probe\".\"result\" is not null" + }, + "workflow_probe_generation_check": { + "name": "workflow_probe_generation_check", + "value": "\"workflow_probe\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_definition_artifact": { + "name": "workflow_definition_artifact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_definition_artifact_revision_idx": { + "name": "workflow_definition_artifact_revision_idx", + "columns": [ + { + "expression": "revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_definition_artifact_revision_id_workflow_definition_revision_id_fk": { + "name": "workflow_definition_artifact_revision_id_workflow_definition_revision_id_fk", + "tableFrom": "workflow_definition_artifact", + "columnsFrom": ["revision_id"], + "tableTo": "workflow_definition_revision", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_definition_artifact_state_check": { + "name": "workflow_definition_artifact_state_check", + "value": "(\"workflow_definition_artifact\".\"result\" is null and \"workflow_definition_artifact\".\"lease_owner\" is not null and \"workflow_definition_artifact\".\"lease_expires_at\" is not null) or (\"workflow_definition_artifact\".\"result\" is not null and \"workflow_definition_artifact\".\"lease_owner\" is null and \"workflow_definition_artifact\".\"lease_expires_at\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.workflow_definition_revision": { + "name": "workflow_definition_revision", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_commit_sha": { + "name": "asset_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entry": { + "name": "entry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "closure": { + "name": "closure", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_definition_revision_tenant_id_tenant_id_fk": { + "name": "workflow_definition_revision_tenant_id_tenant_id_fk", + "tableFrom": "workflow_definition_revision", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "workflow_definition_revision_asset_id_asset_id_fk": { + "name": "workflow_definition_revision_asset_id_asset_id_fk", + "tableFrom": "workflow_definition_revision", + "columnsFrom": ["asset_id"], + "tableTo": "asset", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 3a6521637..c29473538 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -834,6 +834,13 @@ "when": 1793841949636, "tag": "0119_principal_key_public_only", "breakpoints": true + }, + { + "idx": 120, + "version": "7", + "when": 1793841983485, + "tag": "0120_create_run_principals", + "breakpoints": true } ] } diff --git a/tests/db/migration-0120-run-principals.test.ts b/tests/db/migration-0120-run-principals.test.ts new file mode 100644 index 000000000..811505c58 --- /dev/null +++ b/tests/db/migration-0120-run-principals.test.ts @@ -0,0 +1,164 @@ +// Migration 0120 gives existing deployments their run principals once, +// against real data, so this replays it over seeded rows. + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; +import { asc, eq, sql } from "drizzle-orm"; + +import { principal, principalKey } from "@intx/db/schema"; +import { deriveRunPrincipalId } from "@intx/hub-common"; +import { + createTestDb, + harnessDbEnvAvailable, + type TestDb, +} from "@intx/test-harness/db-harness"; +import { + seedPrincipal, + seedTenants, + seedWorkflowRun, +} from "@intx/test-harness/seed"; + +const MIGRATION_SQL = readFileSync( + join( + import.meta.dir, + "../../packages/db/migrations/0120_create_run_principals.sql", + ), + "utf8", +); +const TENANT = "tnt-migration-0120"; + +describe.skipIf(!harnessDbEnvAvailable())( + "migration 0120 run principals", + () => { + let h: TestDb; + beforeAll(async () => { + h = await createTestDb(); + }); + afterAll(async () => { + await h.close(); + }); + beforeEach(async () => { + await h.reset(); + await seedTenants(h.db, [{ id: TENANT }]); + }); + + async function runPrincipalOf(runId: string) { + return h.db + .select({ + id: principal.id, + kind: principal.kind, + status: principal.status, + createdAt: principal.createdAt, + updatedAt: principal.updatedAt, + }) + .from(principal) + .where(eq(principal.refId, runId)) + .orderBy(asc(principal.id)); + } + + test("gives each anchor without one a keyless principal with the derived id", async () => { + const liveAt = new Date("2026-01-02T03:04:05.000Z"); + const deployedAt = new Date("2026-01-03T03:04:05.000Z"); + const endedAt = new Date("2026-01-04T03:04:05.000Z"); + // A non-ASCII run id checks that the SQL hashes the same UTF-8 bytes. + const unicodeRun = "run_migration_ünïcode"; + await seedWorkflowRun(h.db, { + id: "run_migration_live", + anchorRunId: "run_migration_live", + tenantId: TENANT, + status: "deployed", + createdAt: liveAt, + }); + await seedWorkflowRun(h.db, { + id: unicodeRun, + anchorRunId: unicodeRun, + tenantId: TENANT, + status: "running", + createdAt: liveAt, + }); + await seedWorkflowRun(h.db, { + id: "run_migration_ended", + anchorRunId: "run_migration_ended", + tenantId: TENANT, + status: "cancelled", + createdAt: deployedAt, + endedAt, + }); + + await h.db.execute(sql.raw(MIGRATION_SQL)); + + expect(await runPrincipalOf("run_migration_live")).toEqual([ + { + id: await deriveRunPrincipalId(TENANT, "run_migration_live"), + kind: "workflow", + status: "active", + createdAt: liveAt, + updatedAt: liveAt, + }, + ]); + expect(await runPrincipalOf(unicodeRun)).toEqual([ + { + id: await deriveRunPrincipalId(TENANT, unicodeRun), + kind: "workflow", + status: "active", + createdAt: liveAt, + updatedAt: liveAt, + }, + ]); + expect(await runPrincipalOf("run_migration_ended")).toEqual([ + { + id: await deriveRunPrincipalId(TENANT, "run_migration_ended"), + kind: "workflow", + status: "deactivated", + createdAt: deployedAt, + updatedAt: endedAt, + }, + ]); + expect(await h.db.select().from(principalKey)).toEqual([]); + }); + + test("keeps an anchor's existing principal and skips child runs", async () => { + await seedPrincipal(h.db, { + id: "prn_migration_fired", + tenantId: TENANT, + kind: "workflow", + refId: "run_migration_fired", + status: "suspended", + }); + await seedWorkflowRun(h.db, { + id: "run_migration_fired", + anchorRunId: "run_migration_fired", + tenantId: TENANT, + principalId: "prn_migration_fired", + status: "running", + }); + await seedWorkflowRun(h.db, { + id: "run_migration_child", + anchorRunId: "run_migration_fired", + tenantId: TENANT, + status: "running", + }); + + await h.db.execute(sql.raw(MIGRATION_SQL)); + // A second replay changes nothing. + await h.db.execute(sql.raw(MIGRATION_SQL)); + + expect(await runPrincipalOf("run_migration_fired")).toEqual([ + expect.objectContaining({ + id: "prn_migration_fired", + status: "suspended", + }), + ]); + expect(await runPrincipalOf("run_migration_child")).toEqual([]); + }); + }, +); From dc2b4e8302716e3eb10fe9bc1fd3bb38aba27493 Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 15:19:34 -0700 Subject: [PATCH 07/14] Remove the unlocked deploy-ack key writer Every deploy frame carries an allocation, so the listener that wrote the key for an ack with no allocation could no longer fire. It is gone rather than kept as a second key writer that takes no allocation lock. --- apps/hub/src/server.ts | 1 - docs/unified-execution-host-design.md | 22 +- packages/db/src/sender-key-resolver.ts | 9 +- .../src/hub-session-orchestrator.test.ts | 192 +----------------- .../src/hub-session-orchestrator.ts | 38 +--- .../hub-sessions/src/ws/sidecar-events.ts | 5 - .../hub-sessions/src/ws/sidecar-handler.ts | 14 -- 7 files changed, 23 insertions(+), 258 deletions(-) diff --git a/apps/hub/src/server.ts b/apps/hub/src/server.ts index 83bba83fd..0dc8834f5 100644 --- a/apps/hub/src/server.ts +++ b/apps/hub/src/server.ts @@ -392,7 +392,6 @@ export async function createHubServer({ createHubSessionOrchestrator({ events: sidecarRouter.events, router: sidecarRouter, - db, eventCollectors, }); diff --git a/docs/unified-execution-host-design.md b/docs/unified-execution-host-design.md index d0770aafe..d54b84af9 100644 --- a/docs/unified-execution-host-design.md +++ b/docs/unified-execution-host-design.md @@ -97,8 +97,8 @@ capability ceiling. layer over the agent repo store. Their tool/inference/reactor _composition_ moves into the child and is reused. - **Identity is preserved.** A deploy keeps its run's - `run_@` address; the deploy-ack listener, mail routing, - grants, and reconnect continue to find it. + `run_@` address; the key record, mail routing, grants, and + reconnect continue to find it. ## 2. Target architecture @@ -987,10 +987,10 @@ same child/workflow-run model as the multi-step path: - **Address shape.** A deploy mints one run address, `run_@` via `formatRunAddress(generateId("workflowRun"), domain)`. Single-agent and multi-step deploys share the one address shape; there is no separate - launched-agent form and no address-space discriminator. The deploy-ack - listener persists the published identity key against that address, while the - sidecar credential authorizes reconnect routing - (`packages/hub-sessions/src/hub-session-orchestrator.ts`). + launched-agent form and no address-space discriminator. A provisioned + deployment's completed initialization records the published identity key + under the allocation lock, while the sidecar credential authorizes + reconnect routing (`packages/db/src/sidecar-allocation-store.ts`). - **Workflow-run repo id.** The child's workflow-run repo for a single-agent deploy is keyed by `deriveWorkflowRunRepoId(address)` (`packages/workflow-deploy/src/addresses.ts`), which sanitizes the run @@ -1743,10 +1743,10 @@ The unified host is proven by four classes of check: - Multi-turn conversation produces a durable, resumable workflow-run log. - Kill + respawn mid-conversation: `discoverInFlightRuns` resumes and conversation state is restored from the substrate. - - Identity: the run keeps its `run_` address; the deploy-ack listener - persists the public key; grants resolve from the run's `grants.json` and - an authorize call that depends on a granted resource succeeds while an - ungranted one fails closed. + - Identity: the run keeps its `run_` address; its completed + initialization records the public key; grants resolve from the run's + `grants.json` and an authorize call that depends on a granted resource + succeeds while an ungranted one fails closed. 3. **Multiplex + workflow-declared isolation integration tests.** - One child hosts two deployments' runs concurrently; assert no @@ -1867,7 +1867,7 @@ are explicitly **not** a go-live gate for INTR-209. (`buildSingleStepAgentDefinition`); `packages/types/src/agent-address.ts` (`formatRunAddress`, `parseRunAddress`, `isRunAddress`); - `packages/hub-sessions/src/hub-session-orchestrator.ts` (deploy-ack listener); + `packages/db/src/sidecar-allocation-store.ts` (initialization key record); `packages/hub-sessions/src/hub-session-lookups.ts` (`resolveRoutableAddress`); sidecar reconnect identity and hosted bindings in `packages/hub-sessions/src/ws/sidecar-token-authenticator.ts`. diff --git a/packages/db/src/sender-key-resolver.ts b/packages/db/src/sender-key-resolver.ts index fcb03dc0b..159cd624a 100644 --- a/packages/db/src/sender-key-resolver.ts +++ b/packages/db/src/sender-key-resolver.ts @@ -137,10 +137,11 @@ export async function resolveSenderKey( if (localPart.startsWith(RUN_PREFIX)) { // A run sender resolves to the sidecar-minted key the hub recorded on the - // deployment anchor at `agent.deploy.ack`. `workflow_run.address` is unique - // among the runs that set it, so there is at most one row. A row whose - // `public_key` is still null is a deployed-but-not-yet-acked run -- the - // expected pre-ack state -- so it resolves to nothing rather than erroring, + // deployment anchor when the deployment's initialization completed. + // `workflow_run.address` is unique among the runs that set it, so there is + // at most one row. A row whose `public_key` is still null is a deployment + // whose initialization has not completed -- an expected state -- so it + // resolves to nothing rather than erroring, // unlike the keyless-principal invariant break below. const [row] = await db .select({ diff --git a/packages/hub-sessions/src/hub-session-orchestrator.test.ts b/packages/hub-sessions/src/hub-session-orchestrator.test.ts index 1a7c28019..c16f5ac1e 100644 --- a/packages/hub-sessions/src/hub-session-orchestrator.test.ts +++ b/packages/hub-sessions/src/hub-session-orchestrator.test.ts @@ -1,13 +1,10 @@ import { describe, test, expect, beforeEach } from "bun:test"; -import { getTableName, is, Table, type SQL } from "drizzle-orm"; -import type { DB } from "@intx/db"; import type { InferenceEvent } from "@intx/types/runtime"; import type { AgentRepoStore } from "./agent-repo"; import type { LocalRepoStore } from "./repo-store"; import type { EventCollectorRegistry } from "./event-collector-registry"; -import { agentSession, workflowRun } from "@intx/db/schema"; import { createHubSessionOrchestrator } from "./hub-session-orchestrator"; import { @@ -19,126 +16,16 @@ import { // Fixtures and mocks // --------------------------------------------------------------------------- -const TENANT_ID = "tnt_1"; const INSTANCE_ID = "run_1"; const AGENT_ADDRESS = "run_1@tenant.local"; const SESSION_ID = "ses_1"; -const RUN_PRINCIPAL_ID = "prn_run"; -const RUN_SESSION_ID = "ses_run"; - -// The projection `resolveRoutableAddress` reads off a `workflow_run` row. -type RunRow = { - id: string; - tenantId: string; - principalId: string | null; - address: string; - status: string; - publicKey: string | null; - endedAt: Date | null; -}; - -function makeRun(overrides: Partial = {}): RunRow { - return { - id: INSTANCE_ID, - tenantId: TENANT_ID, - principalId: RUN_PRINCIPAL_ID, - address: AGENT_ADDRESS, - status: "running", - publicKey: null, - endedAt: null, - ...overrides, - }; -} - -type UpdateCall = { - table: string; - set: Record; - guard: SQL; +type RouterCall = { + kind: "dispatchAgentEvent"; + addr: string; + event: unknown; }; -type MockDBOpts = { - run?: RunRow | undefined; - // The live agent_session id `resolveRunSessionId` finds for a run's - // principal; null/undefined leaves the run session-less. - runSessionId?: string | null; - recordUpdates?: UpdateCall[]; -}; - -function createMockDB(opts: MockDBOpts) { - const updates = opts.recordUpdates ?? []; - - function tableName(t: unknown): string { - return is(t, Table) ? getTableName(t) : ""; - } - - /* eslint-disable @typescript-eslint/no-unsafe-type-assertion -- - * drizzle PgDatabase type cannot be structurally satisfied in tests */ - return { - query: { - agent: { findFirst: async () => undefined }, - agentSession: { findFirst: async () => undefined }, - provider: { findFirst: async () => undefined, findMany: async () => [] }, - credential: { - findFirst: async () => undefined, - findMany: async () => [], - }, - oauthClient: { findFirst: async () => undefined }, - tenant: { findFirst: async () => undefined }, - }, - update(t: unknown) { - return { - set(values: Record) { - return { - // Capture the where-condition so a test can render it to SQL and - // assert a status flip is guarded, not an unconditional write. - where: async (guard: SQL) => { - updates.push({ table: tableName(t), set: values, guard }); - }, - }; - }, - }; - }, - select() { - return { - from: (t: unknown) => { - // `resolveRoutableAddress` selects the routing endpoint from - // `workflow_run`; `resolveRunSessionId` then reads the run principal's - // live `agent_session`. Back whichever the test seeded. - let rows: unknown[] = []; - if (t === workflowRun && opts.run !== undefined) { - rows = [opts.run]; - } else if (t === agentSession && opts.runSessionId != null) { - rows = [{ id: opts.runSessionId }]; - } - return { - where: () => ({ - limit: () => Promise.resolve(rows), - orderBy: () => ({ limit: () => Promise.resolve(rows) }), - }), - innerJoin: () => ({ - where: () => ({ limit: () => Promise.resolve([]) }), - }), - }; - }, - }; - }, - } as unknown as DB["db"]; - /* eslint-enable @typescript-eslint/no-unsafe-type-assertion */ -} - -type RouterCall = - | { - kind: "dispatchAgentEvent"; - addr: string; - event: unknown; - } - | { - kind: "noteSenderDeploySettled"; - addr: string; - outcome: { recorded: string } | { failed: string }; - }; - function createRouterFacade(): { facade: Parameters[0]["router"]; calls: RouterCall[]; @@ -150,9 +37,6 @@ function createRouterFacade(): { dispatchAgentEvent(addr, event) { calls.push({ kind: "dispatchAgentEvent", addr, event }); }, - noteSenderDeploySettled(addr, outcome) { - calls.push({ kind: "noteSenderDeploySettled", addr, outcome }); - }, }, }; } @@ -246,13 +130,10 @@ type Harness = { router: ReturnType; collectors: ReturnType; repo: ReturnType; - updates: UpdateCall[]; dispose: () => void; }; -function setup(opts: MockDBOpts = {}): Harness { - const updates: UpdateCall[] = []; - const db = createMockDB({ ...opts, recordUpdates: updates }); +function setup(): Harness { const events = createSidecarEmitter(); const router = createRouterFacade(); const collectors = createCollectorRegistry(); @@ -261,7 +142,6 @@ function setup(opts: MockDBOpts = {}): Harness { const orchestrator = createHubSessionOrchestrator({ events, router: router.facade, - db, eventCollectors: collectors.registry, }); @@ -270,7 +150,6 @@ function setup(opts: MockDBOpts = {}): Harness { router, collectors, repo, - updates, dispose: orchestrator.dispose, }; } @@ -283,7 +162,7 @@ describe("createHubSessionOrchestrator", () => { let harness: Harness; beforeEach(() => { - harness = setup({ run: makeRun(), runSessionId: RUN_SESSION_ID }); + harness = setup(); }); describe("agent.event", () => { @@ -328,65 +207,6 @@ describe("createHubSessionOrchestrator", () => { }); }); - describe("agent.deploy.ack", () => { - test("stores the public key on the active run", async () => { - await harness.events.emitAndAwait("agent.deploy.ack", { - agentAddress: AGENT_ADDRESS, - publicKey: "deadbeef", - }); - expect(harness.updates).toHaveLength(1); - expect(harness.updates[0]?.set).toEqual({ publicKey: "deadbeef" }); - expect(harness.updates[0]?.table).toBe("workflow_run"); - // The durable write settles the sender's deploy so its parked pre-ack mail - // wakes, keyed byte-identically on the deploy address. - expect(harness.router.calls).toContainEqual({ - kind: "noteSenderDeploySettled", - addr: AGENT_ADDRESS, - outcome: { recorded: "deadbeef" }, - }); - }); - - test("persists the public key for a workflow-derived deployment address", async () => { - harness = setup({}); - await harness.events.emitAndAwait("agent.deploy.ack", { - agentAddress: "run_abc@workflow.interchange", - publicKey: "deadbeef", - }); - // A workflow-derived deployment address has no agent_instance row; its - // key is persisted on the deployment's anchor workflow_run row, keyed by - // address. - expect(harness.updates).toHaveLength(1); - expect(harness.updates[0]?.set).toEqual({ publicKey: "deadbeef" }); - expect(harness.updates[0]?.table).toBe("workflow_run"); - expect(harness.router.calls).toContainEqual({ - kind: "noteSenderDeploySettled", - addr: "run_abc@workflow.interchange", - outcome: { recorded: "deadbeef" }, - }); - }); - - test("defers an allocated deployment key until initialization completes", async () => { - await harness.events.emitAndAwait("agent.deploy.ack", { - agentAddress: "run_abc@workflow.interchange", - publicKey: "deadbeef", - allocated: { - allocationId: "alloc-1", - anchorRunId: "run_abc", - generation: 3, - }, - }); - - expect(harness.updates).toEqual([]); - // The allocated path settles its own key under the allocation lock, so the - // non-allocated projection neither writes nor settles here. - expect( - harness.router.calls.filter( - (c) => c.kind === "noteSenderDeploySettled", - ), - ).toEqual([]); - }); - }); - describe("mail.persisted", () => { test("dispatches a mail.delivered agent event for the recipient", () => { const raw = new TextEncoder().encode( diff --git a/packages/hub-sessions/src/hub-session-orchestrator.ts b/packages/hub-sessions/src/hub-session-orchestrator.ts index d29fbf39f..2e7ad62e1 100644 --- a/packages/hub-sessions/src/hub-session-orchestrator.ts +++ b/packages/hub-sessions/src/hub-session-orchestrator.ts @@ -10,17 +10,13 @@ // than the full SidecarRouter, so tests can drive subscriber behavior // with a small stub and an isolated emitter. -import { eq } from "drizzle-orm"; import { type } from "arktype"; -import type { DB } from "@intx/db"; -import { workflowRun } from "@intx/db/schema"; import { parseMailToEmail } from "@intx/mime"; import { parseInferenceEvent } from "@intx/types/runtime"; import { getLogger } from "@intx/log"; import type { EventCollectorRegistry } from "./event-collector-registry"; import type { SidecarEventEmitter } from "./ws/sidecar-events"; -import type { SenderDeploySettledOutcome } from "./ws/sidecar-handler"; const log = getLogger(["hub", "orchestrator"]); @@ -29,16 +25,11 @@ const log = getLogger(["hub", "orchestrator"]); * from the rest of the router API. */ export type HubSessionRouterFacade = { dispatchAgentEvent(agentAddress: string, event: unknown): void; - noteSenderDeploySettled( - address: string, - outcome: SenderDeploySettledOutcome, - ): void; }; export type HubSessionOrchestratorDeps = { events: SidecarEventEmitter; router: HubSessionRouterFacade; - db: DB["db"]; eventCollectors: EventCollectorRegistry; }; @@ -52,7 +43,7 @@ export type HubSessionOrchestrator = { export function createHubSessionOrchestrator( deps: HubSessionOrchestratorDeps, ): HubSessionOrchestrator { - const { events, router, db, eventCollectors } = deps; + const { events, router, eventCollectors } = deps; const unsubscribers: (() => void)[] = []; @@ -89,33 +80,6 @@ export function createHubSessionOrchestrator( }), ); - unsubscribers.push( - events.on("agent.deploy.ack", async (event) => { - const { agentAddress, publicKey, allocated } = event; - // Provisioned initialization publishes its key only after its deploy - // succeeds under the allocation generation fence. - if (allocated !== undefined) return; - - // Every deploy address names a workflow run whose public key lives on its - // single self-anchored workflow_run row, keyed by address. Persist it - // there as the deployment's published identity. Reconnect routing is - // allocation-authenticated, so this projection is not connection - // authority. - await db - .update(workflowRun) - .set({ publicKey }) - .where(eq(workflowRun.address, agentAddress)); - - // The key is now durable. Wake any mail the run parked while pre-ack so it - // is delivered with the sender key co-delivered, closing the window where - // a run sends before its key is recorded. The write above happens-before - // this settle, so a re-drive resolves the recorded key. The address is the - // run's own deploy address, byte-identical to the sender address its mail - // was sent under. - router.noteSenderDeploySettled(agentAddress, { recorded: publicKey }); - }), - ); - unsubscribers.push( events.on("mail.persisted", (row) => { const parsed = parseMailToEmail(row.raw, row.id); diff --git a/packages/hub-sessions/src/ws/sidecar-events.ts b/packages/hub-sessions/src/ws/sidecar-events.ts index bb079a060..5f9665bdb 100644 --- a/packages/hub-sessions/src/ws/sidecar-events.ts +++ b/packages/hub-sessions/src/ws/sidecar-events.ts @@ -156,11 +156,6 @@ export type SidecarEventMap = { "agent.deploy.ack": { agentAddress: string; publicKey: string; - allocated?: { - allocationId: string; - anchorRunId: string; - generation: number; - }; }; /** Notification. Emitted when the sidecar reports a change to an diff --git a/packages/hub-sessions/src/ws/sidecar-handler.ts b/packages/hub-sessions/src/ws/sidecar-handler.ts index dfbc83d16..07c30f8bd 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.ts @@ -5071,23 +5071,9 @@ export function createSidecarRouter( if (events.listenerCount("agent.deploy.ack") > 0) { try { - const conn = connections.get(ws); - const identity = - conn === undefined - ? undefined - : owningAllocation(conn, frame.agentAddress); await events.emitAndAwait("agent.deploy.ack", { agentAddress: frame.agentAddress, publicKey: frame.publicKey, - ...(identity !== undefined - ? { - allocated: { - allocationId: identity.allocationId, - anchorRunId: identity.anchorRunId, - generation: identity.generation, - }, - } - : {}), }); } catch (err) { if (!stillAnswered()) return; From b4defc0f019b9282627ef5b33502ab544176772d Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 15:23:43 -0700 Subject: [PATCH 08/14] Record public-only keys without a key store Recording a key whose private half lives elsewhere needs no cipher, and the callers that record a sidecar's key hold a transaction but no key store. Module functions that take the caller's transaction let them record and retire keys under the caller's own lock. --- packages/db/src/index.ts | 2 + packages/db/src/principal-key-store.ts | 221 +++++++++--------- packages/hub-api/src/routes/workflows.test.ts | 2 - .../src/run-grant-materialization.test.ts | 2 - tests/db/backfill-principal-keys.test.ts | 11 +- tests/db/principal-key-store.test.ts | 66 ++---- tests/db/principal-store.test.ts | 2 - 7 files changed, 149 insertions(+), 157 deletions(-) diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index a1269c831..8ad373b9a 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -41,6 +41,8 @@ export { } from "./principal-store"; export { createPrincipalKeyStore, + recordPublicKey, + retireActiveKey, type CreatePrincipalKeyStoreDeps, type PrincipalKeyStore, } from "./principal-key-store"; diff --git a/packages/db/src/principal-key-store.ts b/packages/db/src/principal-key-store.ts index bca9968ae..30bcea916 100644 --- a/packages/db/src/principal-key-store.ts +++ b/packages/db/src/principal-key-store.ts @@ -20,41 +20,33 @@ export type CreatePrincipalKeyStoreDeps = { cipher: CredentialCipher; }; +// The `principal_key` table holds a principal's Ed25519 signing key. A key is +// either hub-custodied or public-only. For a hub-custodied key the hub mints, +// seals, and signs with the private key on the principal's behalf, so a +// signature ATTRIBUTES an action to a principal but is NOT non-repudiable +// against the hub operator, who holds the key. For a public-only key another +// party holds the private key: the hub records the public key it is given and +// refuses to sign. See docs/AUTH.md. +// +// This module owns the one-active-key-per-principal invariant end to end: +// `sign` and `getPublicKey` resolve the single active row; `generate` and +// `recordPublicKey` make a key active, which the table's partial unique index +// rejects if the principal already has one; and `retireActiveKey` retires the +// active key so another can take its place. The private seed never leaves +// this module -- `sign` decrypts, signs, and discards it; no function returns +// private material. + /** - * Store for the `principal_key` table -- a principal's Ed25519 signing key. - * - * A key is either hub-custodied or public-only. For a hub-custodied key the hub - * mints, seals, and signs with the private key on the principal's behalf, so a - * signature ATTRIBUTES an action to a principal but is NOT non-repudiable - * against the hub operator, who holds the key. For a public-only key another - * party holds the private key: the hub records the public key it is given and - * refuses to sign. See docs/AUTH.md. - * - * The store owns the one-active-key-per-principal invariant end to end: `sign` - * and `getPublicKey` resolve the single active row; `generate` and - * `recordPublicKey` insert a new active key, which the table's partial unique - * index rejects if the principal already has one; and `retireActive` retires - * the active key so another can take its place. The private seed never leaves - * this module -- `sign` decrypts, signs, and discards it; no method returns - * private material. + * Store for a principal's hub-custodied key: it mints, signs with, and reads + * the active key. The public-only functions below need no cipher, so they are + * module functions that a caller without a key store can use. */ export function createPrincipalKeyStore({ db, cipher, }: CreatePrincipalKeyStoreDeps) { - async function loadActive(principalId: string, tx?: DBExecutor) { - const [row] = await (tx ?? db) - .select() - .from(principalKey) - .where( - and( - eq(principalKey.principalId, principalId), - eq(principalKey.status, "active"), - ), - ) - .limit(1); - return row; - } + const loadActive = (principalId: string, tx?: DBExecutor) => + loadActiveKey(tx ?? db, principalId); return { /** @@ -130,85 +122,102 @@ export function createPrincipalKeyStore({ } return row.publicKey; }, + }; +} - /** - * Make `publicKey` the principal's active key when another party holds - * the private key. The row holds no private key, so `sign` refuses it. - * - * Recording the public-only key that is already active is a no-op, and - * recording a public-only key the principal held before and retired makes - * that row active again. The call throws when the key belongs to another - * principal, when the key is hub-custodied, or when the principal already - * has a different active key: retire that key first. - * - * The caller serializes writers for this principal. The method reads the - * current rows and then writes, so it takes the transaction that holds the - * caller's lock rather than opening its own. - */ - async recordPublicKey( - principalId: string, - publicKey: string, - tx: DBExecutor, - ): Promise { - const [existing] = await tx - .select() - .from(principalKey) - .where(eq(principalKey.publicKey, publicKey)) - .limit(1); - if (existing !== undefined && existing.principalId !== principalId) { - throw new Error( - `principalKeyStore.recordPublicKey: key ${publicKey} belongs to principal ${existing.principalId}, not ${principalId}`, - ); - } - if (existing !== undefined && existing.privateKey !== null) { - throw new Error( - `principalKeyStore.recordPublicKey: key ${publicKey} of principal ${principalId} is hub-custodied, not public-only`, - ); - } - if (existing?.status === "active") return; +export type PrincipalKeyStore = ReturnType; - const active = await loadActive(principalId, tx); - if (active !== undefined) { - throw new Error( - `principalKeyStore.recordPublicKey: principal ${principalId} already has active key ${active.publicKey}; retire it first`, - ); - } - const now = new Date(); - if (existing !== undefined) { - await tx - .update(principalKey) - .set({ status: "active", updatedAt: now }) - .where(eq(principalKey.id, existing.id)); - return; - } - await tx.insert(principalKey).values({ - id: generateId("principalKey"), - principalId, - publicKey, - privateKey: null, - status: "active", - createdAt: now, - updatedAt: now, - }); - }, +async function loadActiveKey(executor: DBExecutor, principalId: string) { + const [row] = await executor + .select() + .from(principalKey) + .where( + and( + eq(principalKey.principalId, principalId), + eq(principalKey.status, "active"), + ), + ) + .limit(1); + return row; +} - /** - * Retire the principal's active key, so it no longer signs or answers for - * the principal and another key can become active. A principal with no - * active key is left as it is. - */ - async retireActive(principalId: string, tx?: DBExecutor): Promise { - await (tx ?? db) - .update(principalKey) - .set({ status: "retired", updatedAt: new Date() }) - .where( - and( - eq(principalKey.principalId, principalId), - eq(principalKey.status, "active"), - ), - ); - }, - }; +/** + * Make `publicKey` the principal's active key when another party holds the + * private key. The row holds no private key, so `sign` refuses it. + * + * Recording the public-only key that is already active is a no-op, and + * recording a public-only key the principal held before and retired makes + * that row active again. The call throws when the key belongs to another + * principal, when the key is hub-custodied, or when the principal already + * has a different active key: retire that key first. + * + * The caller serializes writers for this principal. The function reads the + * current rows and then writes, so it takes the transaction that holds the + * caller's lock rather than opening its own. + */ +export async function recordPublicKey( + tx: DBExecutor, + principalId: string, + publicKey: string, +): Promise { + const [existing] = await tx + .select() + .from(principalKey) + .where(eq(principalKey.publicKey, publicKey)) + .limit(1); + if (existing !== undefined && existing.principalId !== principalId) { + throw new Error( + `recordPublicKey: key ${publicKey} belongs to principal ${existing.principalId}, not ${principalId}`, + ); + } + if (existing !== undefined && existing.privateKey !== null) { + throw new Error( + `recordPublicKey: key ${publicKey} of principal ${principalId} is hub-custodied, not public-only`, + ); + } + if (existing?.status === "active") return; + + const active = await loadActiveKey(tx, principalId); + if (active !== undefined) { + throw new Error( + `recordPublicKey: principal ${principalId} already has active key ${active.publicKey}; retire it first`, + ); + } + const now = new Date(); + if (existing !== undefined) { + await tx + .update(principalKey) + .set({ status: "active", updatedAt: now }) + .where(eq(principalKey.id, existing.id)); + return; + } + await tx.insert(principalKey).values({ + id: generateId("principalKey"), + principalId, + publicKey, + privateKey: null, + status: "active", + createdAt: now, + updatedAt: now, + }); } -export type PrincipalKeyStore = ReturnType; +/** + * Retire the principal's active key, so it no longer signs or answers for + * the principal and another key can become active. A principal with no + * active key is left as it is. + */ +export async function retireActiveKey( + executor: DBExecutor, + principalId: string, +): Promise { + await executor + .update(principalKey) + .set({ status: "retired", updatedAt: new Date() }) + .where( + and( + eq(principalKey.principalId, principalId), + eq(principalKey.status, "active"), + ), + ); +} diff --git a/packages/hub-api/src/routes/workflows.test.ts b/packages/hub-api/src/routes/workflows.test.ts index 626296efb..57fe05333 100644 --- a/packages/hub-api/src/routes/workflows.test.ts +++ b/packages/hub-api/src/routes/workflows.test.ts @@ -677,8 +677,6 @@ function createMockPrincipalKeyStore(): PrincipalKeyStore { // signing a verifiable message is covered by the run-mail-send integration // test. sign: async () => new Uint8Array(64), - recordPublicKey: async () => undefined, - retireActive: async () => undefined, }; } diff --git a/packages/hub-api/src/run-grant-materialization.test.ts b/packages/hub-api/src/run-grant-materialization.test.ts index a3d4870a2..15325a584 100644 --- a/packages/hub-api/src/run-grant-materialization.test.ts +++ b/packages/hub-api/src/run-grant-materialization.test.ts @@ -1730,8 +1730,6 @@ describe("createCorrespondentGrantMinter", () => { generate: async () => "pky_stub", sign: async () => new Uint8Array(64), getPublicKey: async () => "pky_stub_public", - recordPublicKey: async () => undefined, - retireActive: async () => undefined, }; // A DB stand-in for the minter's unlocked rule check. It serves the sending diff --git a/tests/db/backfill-principal-keys.test.ts b/tests/db/backfill-principal-keys.test.ts index 17be00c26..12d74c15f 100644 --- a/tests/db/backfill-principal-keys.test.ts +++ b/tests/db/backfill-principal-keys.test.ts @@ -8,7 +8,12 @@ import { } from "bun:test"; import { and, eq } from "drizzle-orm"; -import { backfillPrincipalKeys, createPrincipalKeyStore } from "@intx/db"; +import { + backfillPrincipalKeys, + createPrincipalKeyStore, + recordPublicKey, + retireActiveKey, +} from "@intx/db"; import { principalKey } from "@intx/db/schema"; import { createTestCredentialCipher } from "@intx/test-harness/crypto"; import { @@ -150,9 +155,9 @@ describe.skipIf(!harnessDbEnvAvailable())( test("a workflow principal whose only key is retired gets no new key", async () => { const store = newStore(); await h.db.transaction((tx) => - store.recordPublicKey("prn_wf_active", "aa".repeat(32), tx), + recordPublicKey(tx, "prn_wf_active", "aa".repeat(32)), ); - await store.retireActive("prn_wf_active"); + await retireActiveKey(h.db, "prn_wf_active"); await backfillPrincipalKeys(h.db, store); diff --git a/tests/db/principal-key-store.test.ts b/tests/db/principal-key-store.test.ts index 07d97bf79..25168adbc 100644 --- a/tests/db/principal-key-store.test.ts +++ b/tests/db/principal-key-store.test.ts @@ -13,6 +13,8 @@ import { parsePrincipalKeyRow, pgErrorCode, PG_UNIQUE_VIOLATION, + recordPublicKey, + retireActiveKey, } from "@intx/db"; import { principalKey } from "@intx/db/schema"; import { isCiphertext, verifyEd25519 } from "@intx/crypto"; @@ -115,9 +117,7 @@ describe.skipIf(!harnessDbEnvAvailable())( test("recordPublicKey makes a public-only key active, and sign refuses it", async () => { const store = createPrincipalKeyStore({ db: h.db, cipher }); - await h.db.transaction((tx) => - store.recordPublicKey(PRINCIPAL, KEY_A, tx), - ); + await h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, KEY_A)); expect(await store.getPublicKey(PRINCIPAL)).toBe(KEY_A); const rows = await keyRows(PRINCIPAL); @@ -129,25 +129,18 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("recordPublicKey of the active key is a no-op", async () => { - const store = createPrincipalKeyStore({ db: h.db, cipher }); - await h.db.transaction((tx) => - store.recordPublicKey(PRINCIPAL, KEY_A, tx), - ); + await h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, KEY_A)); const [before] = await keyRows(PRINCIPAL); - await h.db.transaction((tx) => - store.recordPublicKey(PRINCIPAL, KEY_A, tx), - ); + await h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, KEY_A)); expect(await keyRows(PRINCIPAL)).toEqual(before ? [before] : []); }); test("recordPublicKey makes a retired key of the same principal active again", async () => { const store = createPrincipalKeyStore({ db: h.db, cipher }); - await h.db.transaction((tx) => - store.recordPublicKey(PRINCIPAL, KEY_A, tx), - ); - await store.retireActive(PRINCIPAL); + await h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, KEY_A)); + await retireActiveKey(h.db, PRINCIPAL); const retiredAt = new Date("2020-01-01T00:00:00Z"); await h.db .update(principalKey) @@ -156,9 +149,7 @@ describe.skipIf(!harnessDbEnvAvailable())( const [retired] = await keyRows(PRINCIPAL); if (retired === undefined) throw new Error("expected a key row"); - await h.db.transaction((tx) => - store.recordPublicKey(PRINCIPAL, KEY_A, tx), - ); + await h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, KEY_A)); const rows = await keyRows(PRINCIPAL); expect(rows).toHaveLength(1); @@ -170,41 +161,34 @@ describe.skipIf(!harnessDbEnvAvailable())( test("recordPublicKey throws when the principal has a different active key", async () => { const store = createPrincipalKeyStore({ db: h.db, cipher }); - await h.db.transaction((tx) => - store.recordPublicKey(PRINCIPAL, KEY_A, tx), - ); + await h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, KEY_A)); await expect( - h.db.transaction((tx) => store.recordPublicKey(PRINCIPAL, KEY_B, tx)), + h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, KEY_B)), ).rejects.toThrow(/already has active key .*; retire it first/); expect(await store.getPublicKey(PRINCIPAL)).toBe(KEY_A); }); test("recordPublicKey of a retired key throws when a different key is active", async () => { const store = createPrincipalKeyStore({ db: h.db, cipher }); - await h.db.transaction((tx) => - store.recordPublicKey(PRINCIPAL, KEY_A, tx), - ); - await store.retireActive(PRINCIPAL); - await h.db.transaction((tx) => - store.recordPublicKey(PRINCIPAL, KEY_B, tx), - ); + await h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, KEY_A)); + await retireActiveKey(h.db, PRINCIPAL); + await h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, KEY_B)); await expect( - h.db.transaction((tx) => store.recordPublicKey(PRINCIPAL, KEY_A, tx)), + h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, KEY_A)), ).rejects.toThrow(/already has active key .*; retire it first/); expect(await store.getPublicKey(PRINCIPAL)).toBe(KEY_B); }); test("recordPublicKey throws for a key that belongs to another principal", async () => { - const store = createPrincipalKeyStore({ db: h.db, cipher }); await seedPrincipal(h.db, { id: OTHER_PRINCIPAL, tenantId: TENANT }); await h.db.transaction((tx) => - store.recordPublicKey(OTHER_PRINCIPAL, KEY_A, tx), + recordPublicKey(tx, OTHER_PRINCIPAL, KEY_A), ); await expect( - h.db.transaction((tx) => store.recordPublicKey(PRINCIPAL, KEY_A, tx)), + h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, KEY_A)), ).rejects.toThrow(/belongs to principal prn_pk_other/); expect(await keyRows(PRINCIPAL)).toHaveLength(0); }); @@ -214,12 +198,12 @@ describe.skipIf(!harnessDbEnvAvailable())( const minted = await store.generate(PRINCIPAL); await expect( - h.db.transaction((tx) => store.recordPublicKey(PRINCIPAL, minted, tx)), + h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, minted)), ).rejects.toThrow(/is hub-custodied, not public-only/); - await store.retireActive(PRINCIPAL); + await retireActiveKey(h.db, PRINCIPAL); await expect( - h.db.transaction((tx) => store.recordPublicKey(PRINCIPAL, minted, tx)), + h.db.transaction((tx) => recordPublicKey(tx, PRINCIPAL, minted)), ).rejects.toThrow(/is hub-custodied, not public-only/); await expect(store.getPublicKey(PRINCIPAL)).rejects.toThrow( /no active key/, @@ -227,12 +211,11 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("recordPublicKey in a rolled-back transaction leaves no row", async () => { - const store = createPrincipalKeyStore({ db: h.db, cipher }); const rollback = new Error("roll back"); await expect( h.db.transaction(async (tx) => { - await store.recordPublicKey(PRINCIPAL, KEY_A, tx); + await recordPublicKey(tx, PRINCIPAL, KEY_A); throw rollback; }), ).rejects.toBe(rollback); @@ -240,17 +223,16 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(await keyRows(PRINCIPAL)).toHaveLength(0); }); - test("retireActive leaves a principal with no active key as it is", async () => { - const store = createPrincipalKeyStore({ db: h.db, cipher }); - await store.retireActive(PRINCIPAL); + test("retireActiveKey leaves a principal with no active key as it is", async () => { + await retireActiveKey(h.db, PRINCIPAL); expect(await keyRows(PRINCIPAL)).toHaveLength(0); }); - test("retireActive retires a hub-minted key so a new key can be minted", async () => { + test("retireActiveKey retires a hub-minted key so a new key can be minted", async () => { const store = createPrincipalKeyStore({ db: h.db, cipher }); const first = await store.generate(PRINCIPAL); - await store.retireActive(PRINCIPAL); + await retireActiveKey(h.db, PRINCIPAL); await expect(store.getPublicKey(PRINCIPAL)).rejects.toThrow( /no active key/, ); diff --git a/tests/db/principal-store.test.ts b/tests/db/principal-store.test.ts index d5ddc035b..eb350b4d2 100644 --- a/tests/db/principal-store.test.ts +++ b/tests/db/principal-store.test.ts @@ -184,8 +184,6 @@ describe.skipIf(!harnessDbEnvAvailable())( }, sign: async () => new Uint8Array(64), getPublicKey: async () => "pky_unused", - recordPublicKey: async () => undefined, - retireActive: async () => undefined, }; const store = createPrincipalStore(h.db, throwingKeyStore); const now = new Date(); From b3cf56888fad6144bfcac6b73fcfab05f1c6349a Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 15:55:38 -0700 Subject: [PATCH 09/14] Record each deployment's sidecar key on its run principal The sidecar running a deployment holds the private half of its key, so the run principal that answers for the deployment holds the public half as a public-only key. The deploy ack records it there and on the anchor, and existing keys are copied over, so the two copies agree. A replacement sidecar can report a new key, so the replaced one is retired. The ack's key must be 32 bytes of lowercase hex, the form the copy checks, so a malformed key is refused at the wire. --- bun.lock | 1 + docs/AUTH.md | 2 + .../0121_run_principal_sidecar_keys.sql | 98 + .../db/migrations/meta/0121_snapshot.json | 5007 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/src/index.ts | 4 + packages/db/src/run-principal-key.ts | 63 + packages/db/src/sidecar-allocation-store.ts | 16 +- .../hub-sessions/src/session-service.test.ts | 81 +- packages/hub-sessions/src/session-service.ts | 37 +- packages/types/src/sidecar.test.ts | 27 + packages/types/src/sidecar.ts | 7 +- tests/db/deployed-window.test.ts | 18 +- ...on-0121-run-principal-sidecar-keys.test.ts | 209 + tests/db/run-principal-key.test.ts | 94 + tests/db/sender-key-resolver.test.ts | 2 +- tests/db/sidecar-allocation-store.test.ts | 80 +- .../db/sidecar-lifecycle-integration.test.ts | 4 +- tests/lib/package.json | 1 + tests/lib/seed.ts | 23 +- 20 files changed, 5715 insertions(+), 66 deletions(-) create mode 100644 packages/db/migrations/0121_run_principal_sidecar_keys.sql create mode 100644 packages/db/migrations/meta/0121_snapshot.json create mode 100644 packages/db/src/run-principal-key.ts create mode 100644 tests/db/migration-0121-run-principal-sidecar-keys.test.ts create mode 100644 tests/db/run-principal-key.test.ts diff --git a/bun.lock b/bun.lock index 8bf83b413..7de82bb66 100644 --- a/bun.lock +++ b/bun.lock @@ -692,6 +692,7 @@ "dependencies": { "@intx/crypto": "workspace:*", "@intx/db": "workspace:*", + "@intx/hub-common": "workspace:*", "@intx/types": "workspace:*", "drizzle-orm": "catalog:", "postgres": "catalog:", diff --git a/docs/AUTH.md b/docs/AUTH.md index 2f38694c2..2a1d6b546 100644 --- a/docs/AUTH.md +++ b/docs/AUTH.md @@ -68,6 +68,8 @@ Hub custody is an **attribution** model, not non-repudiation. A signature with a Every user principal is minted a signing key when it is created, in the same transaction as the principal row, so a user principal never exists without a key. A workflow run principal is different: deploy creates it with no hub-held key, and the run's first trigger links it to the run. The hub seals a hub-custodied key's seed under `PRINCIPAL_KEY_ENCRYPTION_KEY` (32 bytes, hex) -- a separate key from `CREDENTIAL_ENCRYPTION_KEY`, so the two rotate independently -- and refuses to start without it. +A workflow run signs its mail with a key that the sidecar running its deployment mints. The sidecar holds the private key. When the deployment finishes initializing, the hub records the public key as the active, public-only key of the run principal. When the hub replaces the sidecar, it retires that key, and the key the replacement sidecar reports becomes active when the replacement's initialization completes. A replacement that reuses the replaced sidecar's disk reports the same key, and that key becomes active again. The hub resolves no retired key, so while the key stays retired it vouches for no key for mail that the replaced sidecar signed. A recipient sidecar that cached the retired key keeps it until the run's next mail carries the replacement's key. + ### Request resolution flow ``` diff --git a/packages/db/migrations/0121_run_principal_sidecar_keys.sql b/packages/db/migrations/0121_run_principal_sidecar_keys.sql new file mode 100644 index 000000000..404837071 --- /dev/null +++ b/packages/db/migrations/0121_run_principal_sidecar_keys.sql @@ -0,0 +1,98 @@ +-- Put each deployment's sidecar key on its run principal. The sidecar running +-- a deployment mints the deployment's key and holds the private half; the hub +-- records the public half on the deployment anchor, and from now on also as +-- the active key of the anchor's run principal, with no private key. +-- +-- A run principal that fired before run principals were created at deploy got +-- a hub-minted key at its first trigger. Nothing signs with that key, and it +-- would block recording the sidecar key, so it is deleted. A definition-level +-- workflow principal, whose `ref_id` names a definition rather than a run, +-- keeps its key. +-- +-- The copy refuses data it cannot copy faithfully rather than skipping it: +-- a key on a run row that is not a deployment anchor, a key that is not 32 +-- bytes of lowercase hex, two anchors with one key, a key on an anchor with no +-- run principal, or a key another principal already holds. A terminal +-- anchor's key is copied too, so its mail still verifies. +-- +-- The checks, the copy and the final check run in one statement under a lock +-- on the tables they read, so another writer cannot record, clear or mint +-- a key between them. +DO $$ +BEGIN + LOCK TABLE "workflow_run", "principal", "principal_key" IN SHARE ROW EXCLUSIVE MODE; + IF EXISTS ( + SELECT 1 FROM "workflow_run" + WHERE "public_key" IS NOT NULL AND "anchor_run_id" IS DISTINCT FROM "id" + ) THEN + RAISE EXCEPTION 'a workflow_run that is not a deployment anchor holds a public key'; + END IF; + IF EXISTS ( + SELECT 1 FROM "workflow_run" + WHERE "public_key" IS NOT NULL AND "public_key" !~ '^[0-9a-f]{64}$' + ) THEN + RAISE EXCEPTION 'a workflow_run public key is not 32 bytes of lowercase hex'; + END IF; + IF EXISTS ( + SELECT "public_key" FROM "workflow_run" + WHERE "public_key" IS NOT NULL + GROUP BY "public_key" HAVING count(*) > 1 + ) THEN + RAISE EXCEPTION 'two workflow_run rows hold the same public key'; + END IF; + IF EXISTS ( + SELECT 1 FROM "workflow_run" r + WHERE r."public_key" IS NOT NULL AND NOT EXISTS ( + SELECT 1 FROM "principal" p + WHERE p."tenant_id" = r."tenant_id" AND p."kind" = 'workflow' AND p."ref_id" = r."id" + ) + ) THEN + RAISE EXCEPTION 'a deployment anchor with a public key has no run principal'; + END IF; + IF EXISTS ( + SELECT 1 FROM "workflow_run" r + JOIN "principal_key" k ON k."public_key" = r."public_key" + JOIN "principal" p ON p."id" = k."principal_id" + WHERE NOT (p."tenant_id" = r."tenant_id" AND p."kind" = 'workflow' AND p."ref_id" = r."id") + ) THEN + RAISE EXCEPTION 'a deployment public key is already held by another principal'; + END IF; + + DELETE FROM "principal_key" k + USING "principal" p, "workflow_run" r + WHERE k."principal_id" = p."id" + AND p."kind" = 'workflow' + AND p."tenant_id" = r."tenant_id" + AND p."ref_id" = r."id" + AND k."private_key" IS NOT NULL; + + INSERT INTO "principal_key" ("id", "principal_id", "public_key", "private_key", "status", "created_at", "updated_at") + SELECT + 'pky_' || replace(gen_random_uuid()::text, '-', ''), + p."id", + r."public_key", + NULL, + 'active', + now(), + now() + FROM "workflow_run" r + JOIN "principal" p + ON p."tenant_id" = r."tenant_id" AND p."kind" = 'workflow' AND p."ref_id" = r."id" + WHERE r."public_key" IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM "principal_key" k + WHERE k."principal_id" = p."id" AND k."status" = 'active' + ); + + IF EXISTS ( + SELECT 1 FROM "workflow_run" r + JOIN "principal" p + ON p."tenant_id" = r."tenant_id" AND p."kind" = 'workflow' AND p."ref_id" = r."id" + WHERE r."public_key" IS NOT NULL AND NOT EXISTS ( + SELECT 1 FROM "principal_key" k + WHERE k."principal_id" = p."id" AND k."status" = 'active' AND k."public_key" = r."public_key" + ) + ) THEN + RAISE EXCEPTION 'a deployment public key was not copied to its run principal'; + END IF; +END $$; diff --git a/packages/db/migrations/meta/0121_snapshot.json b/packages/db/migrations/meta/0121_snapshot.json new file mode 100644 index 000000000..1dc0177bb --- /dev/null +++ b/packages/db/migrations/meta/0121_snapshot.json @@ -0,0 +1,5007 @@ +{ + "id": "7e5f6f90-f79a-4bec-8972-dd7adcad714f", + "prevId": "682948ca-dc25-4066-81cc-338b44f6959d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_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 + }, + "id_token": { + "name": "id_token", + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "columnsFrom": ["user_id"], + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "columnsFrom": ["user_id"], + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "columns": ["token"], + "nullsNotDistinct": false + } + }, + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "columns": ["email"], + "nullsNotDistinct": false + } + }, + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_trust": { + "name": "federation_trust", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_tenant_id": { + "name": "target_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federation_trust_tenant_id_tenant_id_fk": { + "name": "federation_trust_tenant_id_tenant_id_fk", + "tableFrom": "federation_trust", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "federation_trust_target_tenant_id_tenant_id_fk": { + "name": "federation_trust_target_tenant_id_tenant_id_fk", + "tableFrom": "federation_trust", + "columnsFrom": ["target_tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federation_trust_tenant_id_target_tenant_id_unique": { + "name": "federation_trust_tenant_id_target_tenant_id_unique", + "columns": ["tenant_id", "target_tenant_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant": { + "name": "tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "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": { + "tenant_domain_lower_idx": { + "name": "tenant_domain_lower_idx", + "columns": [ + { + "expression": "lower(\"domain\")", + "isExpression": true, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tenant_parent_id_tenant_id_fk": { + "name": "tenant_parent_id_tenant_id_fk", + "tableFrom": "tenant", + "columnsFrom": ["parent_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tenant_slug_unique": { + "name": "tenant_slug_unique", + "columns": ["slug"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "tenant_slug_dns_label_check": { + "name": "tenant_slug_dns_label_check", + "value": "\"tenant\".\"slug\" ~ '^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$' and length(\"tenant\".\"slug\") <= 63" + } + }, + "isRLSEnabled": false + }, + "public.principal": { + "name": "principal", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref_id": { + "name": "ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "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": { + "principal_tenant_id_tenant_id_fk": { + "name": "principal_tenant_id_tenant_id_fk", + "tableFrom": "principal", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "principal_tenant_id_kind_ref_id_unique": { + "name": "principal_tenant_id_kind_ref_id_unique", + "columns": ["tenant_id", "kind", "ref_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_key": { + "name": "principal_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "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": { + "principal_key_one_active": { + "name": "principal_key_one_active", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"principal_key\".\"status\" = 'active'", + "concurrently": false + } + }, + "foreignKeys": { + "principal_key_principal_id_principal_id_fk": { + "name": "principal_key_principal_id_principal_id_fk", + "tableFrom": "principal_key", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "principal_key_public_key_unique": { + "name": "principal_key_public_key_unique", + "columns": ["public_key"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_role": { + "name": "agent_role", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "agent_role_agent_id_workflow_definition_id_fk": { + "name": "agent_role_agent_id_workflow_definition_id_fk", + "tableFrom": "agent_role", + "columnsFrom": ["agent_id"], + "tableTo": "workflow_definition", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "agent_role_role_id_role_id_fk": { + "name": "agent_role_role_id_role_id_fk", + "tableFrom": "agent_role", + "columnsFrom": ["role_id"], + "tableTo": "role", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "agent_role_agent_id_role_id_pk": { + "name": "agent_role_agent_id_role_id_pk", + "columns": ["agent_id", "role_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_role": { + "name": "principal_role", + "schema": "", + "columns": { + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "principal_role_principal_id_principal_id_fk": { + "name": "principal_role_principal_id_principal_id_fk", + "tableFrom": "principal_role", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "principal_role_role_id_role_id_fk": { + "name": "principal_role_role_id_role_id_fk", + "tableFrom": "principal_role", + "columnsFrom": ["role_id"], + "tableTo": "role", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "principal_role_principal_id_role_id_pk": { + "name": "principal_role_principal_id_role_id_pk", + "columns": ["principal_id", "role_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "role_tenant_id_tenant_id_fk": { + "name": "role_tenant_id_tenant_id_fk", + "tableFrom": "role", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grant": { + "name": "grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "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": { + "grant_invoker_allow_principal_idx": { + "name": "grant_invoker_allow_principal_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"grant\".\"origin\" = 'invoker' and \"grant\".\"effect\" = 'allow'", + "concurrently": false + } + }, + "foreignKeys": { + "grant_tenant_id_tenant_id_fk": { + "name": "grant_tenant_id_tenant_id_fk", + "tableFrom": "grant", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "grant_role_id_role_id_fk": { + "name": "grant_role_id_role_id_fk", + "tableFrom": "grant", + "columnsFrom": ["role_id"], + "tableTo": "role", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "grant_principal_id_principal_id_fk": { + "name": "grant_principal_id_principal_id_fk", + "tableFrom": "grant", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "grant_target_exactly_one": { + "name": "grant_target_exactly_one", + "value": "num_nonnulls(\"grant\".\"principal_id\", \"grant\".\"role_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.approval": { + "name": "approval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_address": { + "name": "agent_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_definition": { + "name": "tool_definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "tool_arguments": { + "name": "tool_arguments", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "timeout_at": { + "name": "timeout_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "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": { + "approval_tenant_status_idx": { + "name": "approval_tenant_status_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "approval_anchor_run_idx": { + "name": "approval_anchor_run_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "approval_run_idx": { + "name": "approval_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "approval_tenant_id_tenant_id_fk": { + "name": "approval_tenant_id_tenant_id_fk", + "tableFrom": "approval", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "approval_anchor_run_id_workflow_run_id_fk": { + "name": "approval_anchor_run_id_workflow_run_id_fk", + "tableFrom": "approval", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "approval_run_id_workflow_run_id_fk": { + "name": "approval_run_id_workflow_run_id_fk", + "tableFrom": "approval", + "columnsFrom": ["run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "approval_correlation_id_unique": { + "name": "approval_correlation_id_unique", + "columns": ["correlation_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signal_correlation": { + "name": "signal_correlation", + "schema": "", + "columns": { + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_address": { + "name": "agent_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_name": { + "name": "signal_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_id": { + "name": "signal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "signal_correlation_tenant_id_tenant_id_fk": { + "name": "signal_correlation_tenant_id_tenant_id_fk", + "tableFrom": "signal_correlation", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "signal_correlation_anchor_run_id_workflow_run_id_fk": { + "name": "signal_correlation_anchor_run_id_workflow_run_id_fk", + "tableFrom": "signal_correlation", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "signal_correlation_run_id_workflow_run_id_fk": { + "name": "signal_correlation_run_id_workflow_run_id_fk", + "tableFrom": "signal_correlation", + "columnsFrom": ["run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider": { + "name": "provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin": { + "name": "plugin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_base_url": { + "name": "api_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_info_url": { + "name": "user_info_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "provider_tenant_id_tenant_id_fk": { + "name": "provider_tenant_id_tenant_id_fk", + "tableFrom": "provider", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_tenant_name": { + "name": "provider_tenant_name", + "columns": ["tenant_id", "name"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "default_scopes": { + "name": "default_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "oauth_client_tenant_id_tenant_id_fk": { + "name": "oauth_client_tenant_id_tenant_id_fk", + "tableFrom": "oauth_client", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "oauth_client_provider_id_provider_id_fk": { + "name": "oauth_client_provider_id_provider_id_fk", + "tableFrom": "oauth_client", + "columnsFrom": ["provider_id"], + "tableTo": "provider", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_tenant_provider": { + "name": "oauth_client_tenant_provider", + "columns": ["tenant_id", "provider_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_version": { + "name": "secret_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "refresh_secret": { + "name": "refresh_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "credential_tenant_id_tenant_id_fk": { + "name": "credential_tenant_id_tenant_id_fk", + "tableFrom": "credential", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "credential_principal_id_principal_id_fk": { + "name": "credential_principal_id_principal_id_fk", + "tableFrom": "credential", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "credential_provider_id_provider_id_fk": { + "name": "credential_provider_id_provider_id_fk", + "tableFrom": "credential", + "columnsFrom": ["provider_id"], + "tableTo": "provider", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "credential_oauth_client_id_oauth_client_id_fk": { + "name": "credential_oauth_client_id_oauth_client_id_fk", + "tableFrom": "credential", + "columnsFrom": ["oauth_client_id"], + "tableTo": "oauth_client", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_tenant_name": { + "name": "credential_tenant_name", + "columns": ["tenant_id", "name"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset": { + "name": "asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_principal_id": { + "name": "creator_principal_id", + "type": "text", + "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": {}, + "foreignKeys": { + "asset_tenant_id_tenant_id_fk": { + "name": "asset_tenant_id_tenant_id_fk", + "tableFrom": "asset", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "asset_creator_principal_id_principal_id_fk": { + "name": "asset_creator_principal_id_principal_id_fk", + "tableFrom": "asset", + "columnsFrom": ["creator_principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "asset_tenant_kind_name": { + "name": "asset_tenant_kind_name", + "columns": ["tenant_id", "kind", "name"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transaction": { + "name": "transaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "wallet_id": { + "name": "wallet_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_id": { + "name": "sender_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "transaction_wallet_id_wallet_id_fk": { + "name": "transaction_wallet_id_wallet_id_fk", + "tableFrom": "transaction", + "columnsFrom": ["wallet_id"], + "tableTo": "wallet", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "transaction_run_id_workflow_run_id_fk": { + "name": "transaction_run_id_workflow_run_id_fk", + "tableFrom": "transaction", + "columnsFrom": ["run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wallet": { + "name": "wallet", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backend_type": { + "name": "backend_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "config": { + "name": "config", + "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": {}, + "foreignKeys": { + "wallet_tenant_id_tenant_id_fk": { + "name": "wallet_tenant_id_tenant_id_fk", + "tableFrom": "wallet", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.offering": { + "name": "offering", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing": { + "name": "pricing", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "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": {}, + "foreignKeys": { + "offering_agent_id_workflow_definition_id_fk": { + "name": "offering_agent_id_workflow_definition_id_fk", + "tableFrom": "offering", + "columnsFrom": ["agent_id"], + "tableTo": "workflow_definition", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "offering_tenant_id_tenant_id_fk": { + "name": "offering_tenant_id_tenant_id_fk", + "tableFrom": "offering", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model": { + "name": "model", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_name": { + "name": "canonical_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_tenant_id_tenant_id_fk": { + "name": "model_tenant_id_tenant_id_fk", + "tableFrom": "model", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_tenant_canonical_name": { + "name": "model_tenant_canonical_name", + "columns": ["tenant_id", "canonical_name"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_offering": { + "name": "model_offering", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deployment_tags": { + "name": "deployment_tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "capabilities": { + "name": "capabilities", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "quirks": { + "name": "quirks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_offering_tenant_id_tenant_id_fk": { + "name": "model_offering_tenant_id_tenant_id_fk", + "tableFrom": "model_offering", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "model_offering_model_id_model_id_fk": { + "name": "model_offering_model_id_model_id_fk", + "tableFrom": "model_offering", + "columnsFrom": ["model_id"], + "tableTo": "model", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "model_offering_provider_id_model_provider_id_fk": { + "name": "model_offering_provider_id_model_provider_id_fk", + "tableFrom": "model_offering", + "columnsFrom": ["provider_id"], + "tableTo": "model_provider", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_offering_tenant_model_provider": { + "name": "model_offering_tenant_model_provider", + "columns": ["tenant_id", "model_id", "provider_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_pricing": { + "name": "model_pricing", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "offering_id": { + "name": "offering_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_token_price": { + "name": "input_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_token_price": { + "name": "output_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_read_token_price": { + "name": "cache_read_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_write_token_price": { + "name": "cache_write_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thinking_token_price": { + "name": "thinking_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_request_fee": { + "name": "per_request_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_image_fee": { + "name": "per_image_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_audio_fee": { + "name": "per_audio_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effective_from": { + "name": "effective_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "model_pricing_tenant_id_tenant_id_fk": { + "name": "model_pricing_tenant_id_tenant_id_fk", + "tableFrom": "model_pricing", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "model_pricing_offering_id_model_offering_id_fk": { + "name": "model_pricing_offering_id_model_offering_id_fk", + "tableFrom": "model_pricing", + "columnsFrom": ["offering_id"], + "tableTo": "model_offering", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_pricing_offering_currency_effective_from": { + "name": "model_pricing_offering_currency_effective_from", + "columns": ["offering_id", "currency", "effective_from"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_provider": { + "name": "model_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin": { + "name": "plugin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wallet_id": { + "name": "wallet_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_provider_tenant_id_tenant_id_fk": { + "name": "model_provider_tenant_id_tenant_id_fk", + "tableFrom": "model_provider", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "model_provider_credential_id_credential_id_fk": { + "name": "model_provider_credential_id_credential_id_fk", + "tableFrom": "model_provider", + "columnsFrom": ["credential_id"], + "tableTo": "credential", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "model_provider_wallet_id_wallet_id_fk": { + "name": "model_provider_wallet_id_wallet_id_fk", + "tableFrom": "model_provider", + "columnsFrom": ["wallet_id"], + "tableTo": "wallet", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_provider_tenant_name": { + "name": "model_provider_tenant_name", + "columns": ["tenant_id", "name"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "model_provider_auth_xor": { + "name": "model_provider_auth_xor", + "value": "(\"model_provider\".\"credential_id\" is not null) <> (\"model_provider\".\"wallet_id\" is not null)" + } + }, + "isRLSEnabled": false + }, + "public.sidecar": { + "name": "sidecar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reconnect_window_ended_at": { + "name": "reconnect_window_ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'online'" + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sidecar_token_hash_sha256_unique": { + "name": "sidecar_token_hash_sha256_unique", + "columns": ["token_hash_sha256"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sidecar_allocation": { + "name": "sidecar_allocation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ensure_accepted_generation": { + "name": "ensure_accepted_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconciliation_lease_id": { + "name": "reconciliation_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reconciliation_lease_expires_at": { + "name": "reconciliation_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "initialization_lease_id": { + "name": "initialization_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ensure_attempts": { + "name": "ensure_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "destroy_attempts": { + "name": "destroy_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deployment_cleanup_confirmed": { + "name": "deployment_cleanup_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_hold_released": { + "name": "provider_hold_released", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "connect_deadline": { + "name": "connect_deadline", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "max_disconnected_ms": { + "name": "max_disconnected_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "first_deploy_failed_at": { + "name": "first_deploy_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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": { + "sidecar_allocation_anchor_run_idx": { + "name": "sidecar_allocation_anchor_run_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "sidecar_allocation_sidecar_idx": { + "name": "sidecar_allocation_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "sidecar_allocation_reconciliation_idx": { + "name": "sidecar_allocation_reconciliation_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing', 'releasing') and \"sidecar_allocation\".\"next_attempt_at\" is not null", + "concurrently": false + }, + "sidecar_allocation_dispatchable_anchor_idx": { + "name": "sidecar_allocation_dispatchable_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing')", + "concurrently": false + } + }, + "foreignKeys": { + "sidecar_allocation_anchor_run_id_workflow_run_id_fk": { + "name": "sidecar_allocation_anchor_run_id_workflow_run_id_fk", + "tableFrom": "sidecar_allocation", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "sidecar_allocation_tenant_id_tenant_id_fk": { + "name": "sidecar_allocation_tenant_id_tenant_id_fk", + "tableFrom": "sidecar_allocation", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "sidecar_allocation_sidecar_id_sidecar_id_fk": { + "name": "sidecar_allocation_sidecar_id_sidecar_id_fk", + "tableFrom": "sidecar_allocation", + "columnsFrom": ["sidecar_id"], + "tableTo": "sidecar", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "sidecar_allocation_status_check": { + "name": "sidecar_allocation_status_check", + "value": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing', 'releasing', 'destroy_failed', 'released', 'failed')" + }, + "sidecar_allocation_generation_check": { + "name": "sidecar_allocation_generation_check", + "value": "\"sidecar_allocation\".\"generation\" >= 0" + }, + "sidecar_allocation_max_disconnected_check": { + "name": "sidecar_allocation_max_disconnected_check", + "value": "\"sidecar_allocation\".\"max_disconnected_ms\" > 0" + }, + "sidecar_allocation_accepted_generation_check": { + "name": "sidecar_allocation_accepted_generation_check", + "value": "\"sidecar_allocation\".\"ensure_accepted_generation\" is null or \"sidecar_allocation\".\"ensure_accepted_generation\" <= \"sidecar_allocation\".\"generation\"" + }, + "sidecar_allocation_initialization_check": { + "name": "sidecar_allocation_initialization_check", + "value": "\"sidecar_allocation\".\"initialization_lease_id\" is null or (\"sidecar_allocation\".\"status\" = 'allocated' and \"sidecar_allocation\".\"ensure_accepted_generation\" is not null and \"sidecar_allocation\".\"ensure_accepted_generation\" = \"sidecar_allocation\".\"generation\")" + } + }, + "isRLSEnabled": false + }, + "public.agent_session": { + "name": "agent_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_session_tenant_id_tenant_id_fk": { + "name": "agent_session_tenant_id_tenant_id_fk", + "tableFrom": "agent_session", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "agent_session_agent_id_workflow_definition_id_fk": { + "name": "agent_session_agent_id_workflow_definition_id_fk", + "tableFrom": "agent_session", + "columnsFrom": ["agent_id"], + "tableTo": "workflow_definition", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "agent_session_principal_id_principal_id_fk": { + "name": "agent_session_principal_id_principal_id_fk", + "tableFrom": "agent_session", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_asset": { + "name": "session_asset", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_pack_sha": { + "name": "asset_pack_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "materialized_at": { + "name": "materialized_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_asset_pack_sha_idx": { + "name": "session_asset_pack_sha_idx", + "columns": [ + { + "expression": "asset_pack_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "session_asset_instance_id_mount_path_pk": { + "name": "session_asset_instance_id_mount_path_pk", + "columns": ["instance_id", "mount_path"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inference_turn": { + "name": "inference_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inference_turn_instance_id_started_at_idx": { + "name": "inference_turn_instance_id_started_at_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "inference_turn_session_id_agent_session_id_fk": { + "name": "inference_turn_session_id_agent_session_id_fk", + "tableFrom": "inference_turn", + "columnsFrom": ["session_id"], + "tableTo": "agent_session", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "inference_turn_tenant_id_tenant_id_fk": { + "name": "inference_turn_tenant_id_tenant_id_fk", + "tableFrom": "inference_turn", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_mail": { + "name": "session_mail", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "raw": { + "name": "raw", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_mail_instance_id_created_at_idx": { + "name": "session_mail_instance_id_created_at_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "session_mail_session_id_created_at_idx": { + "name": "session_mail_session_id_created_at_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "session_mail_session_id_agent_session_id_fk": { + "name": "session_mail_session_id_agent_session_id_fk", + "tableFrom": "session_mail", + "columnsFrom": ["session_id"], + "tableTo": "agent_session", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "session_mail_tenant_id_tenant_id_fk": { + "name": "session_mail_tenant_id_tenant_id_fk", + "tableFrom": "session_mail", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.turn_part": { + "name": "turn_part", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "turn_part_turn_id_inference_turn_id_fk": { + "name": "turn_part_turn_id_inference_turn_id_fk", + "tableFrom": "turn_part", + "columnsFrom": ["turn_id"], + "tableTo": "inference_turn", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "turn_part_session_id_agent_session_id_fk": { + "name": "turn_part_session_id_agent_session_id_fk", + "tableFrom": "turn_part", + "columnsFrom": ["session_id"], + "tableTo": "agent_session", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_token": { + "name": "git_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref_pattern": { + "name": "ref_pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actions": { + "name": "actions", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "git_token_user_id_name_active_idx": { + "name": "git_token_user_id_name_active_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"git_token\".\"revoked_at\" is null", + "concurrently": false + } + }, + "foreignKeys": { + "git_token_tenant_id_tenant_id_fk": { + "name": "git_token_tenant_id_tenant_id_fk", + "tableFrom": "git_token", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "no action" + }, + "git_token_user_id_user_id_fk": { + "name": "git_token_user_id_user_id_fk", + "tableFrom": "git_token", + "columnsFrom": ["user_id"], + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "git_token_principal_id_principal_id_fk": { + "name": "git_token_principal_id_principal_id_fk", + "tableFrom": "git_token", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_token_token_hash_sha256_unique": { + "name": "git_token_token_hash_sha256_unique", + "columns": ["token_hash_sha256"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.run_token": { + "name": "run_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "sealed_token": { + "name": "sealed_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "run_token_principal_consumer_idx": { + "name": "run_token_principal_consumer_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "run_token_principal_id_principal_id_fk": { + "name": "run_token_principal_id_principal_id_fk", + "tableFrom": "run_token", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "run_token_token_hash_sha256_unique": { + "name": "run_token_token_hash_sha256_unique", + "columns": ["token_hash_sha256"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_definition": { + "name": "workflow_definition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creator_principal_id": { + "name": "creator_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wire_hash": { + "name": "wire_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_requirements": { + "name": "grant_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "model_requirements": { + "name": "model_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_bindings": { + "name": "credential_bindings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lifecycle_policy": { + "name": "lifecycle_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'deployed'" + }, + "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": { + "workflow_definition_tenant_idx": { + "name": "workflow_definition_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_definition_asset_wire_hash_idx": { + "name": "workflow_definition_asset_wire_hash_idx", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "wire_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_definition_tenant_id_tenant_id_fk": { + "name": "workflow_definition_tenant_id_tenant_id_fk", + "tableFrom": "workflow_definition", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workflow_definition_creator_principal_id_principal_id_fk": { + "name": "workflow_definition_creator_principal_id_principal_id_fk", + "tableFrom": "workflow_definition", + "columnsFrom": ["creator_principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "no action" + }, + "workflow_definition_asset_id_asset_id_fk": { + "name": "workflow_definition_asset_id_asset_id_fk", + "tableFrom": "workflow_definition", + "columnsFrom": ["asset_id"], + "tableTo": "asset", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_definition_version": { + "name": "workflow_definition_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "approved_wire_hash": { + "name": "approved_wire_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_snapshot": { + "name": "grant_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_definition_version_definition_idx": { + "name": "workflow_definition_version_definition_idx", + "columns": [ + { + "expression": "definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_definition_version_definition_id_workflow_definition_id_fk": { + "name": "workflow_definition_version_definition_id_workflow_definition_id_fk", + "tableFrom": "workflow_definition_version", + "columnsFrom": ["definition_id"], + "tableTo": "workflow_definition", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workflow_definition_version_definition_version": { + "name": "workflow_definition_version_definition_version", + "columns": ["definition_id", "version"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run": { + "name": "workflow_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel_id": { + "name": "kernel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_preferences": { + "name": "model_preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_refs": { + "name": "credential_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lifecycle_policy": { + "name": "lifecycle_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "operator_cancellation_requested_at": { + "name": "operator_cancellation_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_deadline": { + "name": "cancellation_deadline", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity_release_at": { + "name": "capacity_release_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "infrastructure_failed_at": { + "name": "infrastructure_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "legacy_mail_accept": { + "name": "legacy_mail_accept", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_definition_idx": { + "name": "workflow_run_definition_idx", + "columns": [ + { + "expression": "definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_run_address_idx": { + "name": "workflow_run_address_idx", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"workflow_run\".\"address\" is not null", + "concurrently": false + }, + "workflow_run_anchor_idx": { + "name": "workflow_run_anchor_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run\".\"id\" = \"workflow_run\".\"anchor_run_id\"", + "concurrently": false + }, + "workflow_run_live_anchor_idx": { + "name": "workflow_run_live_anchor_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run\".\"id\" = \"workflow_run\".\"anchor_run_id\" and \"workflow_run\".\"status\" in ('deployed', 'running')", + "concurrently": false + }, + "workflow_run_infrastructure_failed_idx": { + "name": "workflow_run_infrastructure_failed_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run\".\"infrastructure_failed_at\" is not null", + "concurrently": false + }, + "workflow_run_sidecar_idx": { + "name": "workflow_run_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run\".\"sidecar_id\" is not null", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_run_definition_id_workflow_definition_id_fk": { + "name": "workflow_run_definition_id_workflow_definition_id_fk", + "tableFrom": "workflow_run", + "columnsFrom": ["definition_id"], + "tableTo": "workflow_definition", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workflow_run_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workflow_run_tenant_id_tenant_id_fk": { + "name": "workflow_run_tenant_id_tenant_id_fk", + "tableFrom": "workflow_run", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workflow_run_principal_id_principal_id_fk": { + "name": "workflow_run_principal_id_principal_id_fk", + "tableFrom": "workflow_run", + "columnsFrom": ["principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "set null" + }, + "workflow_run_sidecar_id_sidecar_id_fk": { + "name": "workflow_run_sidecar_id_sidecar_id_fk", + "tableFrom": "workflow_run", + "columnsFrom": ["sidecar_id"], + "tableTo": "sidecar", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_run_legacy_mail_accept_check": { + "name": "workflow_run_legacy_mail_accept_check", + "value": "\"workflow_run\".\"legacy_mail_accept\" is null or \"workflow_run\".\"legacy_mail_accept\" in ('invoker-correspondent', 'correspondent')" + } + }, + "isRLSEnabled": false + }, + "public.workflow_run_dispatch": { + "name": "workflow_run_dispatch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'mail'" + }, + "sender_address": { + "name": "sender_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_message": { + "name": "raw_message", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "step_grants": { + "name": "step_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "acknowledged_generation": { + "name": "acknowledged_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "delivery_lease_id": { + "name": "delivery_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_lease_expires_at": { + "name": "delivery_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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()" + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_dispatch_anchor_message_idx": { + "name": "workflow_run_dispatch_anchor_message_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_run_dispatch_delivery_idx": { + "name": "workflow_run_dispatch_delivery_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run_dispatch\".\"status\" = 'pending'", + "concurrently": false + }, + "workflow_run_dispatch_anchor_status_idx": { + "name": "workflow_run_dispatch_anchor_status_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_run_dispatch_unresolved_anchor_idx": { + "name": "workflow_run_dispatch_unresolved_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_run_dispatch\".\"status\" in ('pending', 'acknowledged') or (\"workflow_run_dispatch\".\"status\" = 'abandoned' and \"workflow_run_dispatch\".\"next_attempt_at\" is not null)", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_run_dispatch_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_dispatch_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_dispatch", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_run_dispatch_status_check": { + "name": "workflow_run_dispatch_status_check", + "value": "\"workflow_run_dispatch\".\"status\" in ('pending', 'acknowledged', 'abandoned', 'settled', 'failed')" + }, + "workflow_run_dispatch_kind_check": { + "name": "workflow_run_dispatch_kind_check", + "value": "\"workflow_run_dispatch\".\"kind\" in ('mail', 'signal')" + }, + "workflow_run_dispatch_attempt_count_check": { + "name": "workflow_run_dispatch_attempt_count_check", + "value": "\"workflow_run_dispatch\".\"attempt_count\" >= 0" + }, + "workflow_run_dispatch_acknowledged_generation_check": { + "name": "workflow_run_dispatch_acknowledged_generation_check", + "value": "\"workflow_run_dispatch\".\"acknowledged_generation\" is null or \"workflow_run_dispatch\".\"acknowledged_generation\" >= 0" + }, + "workflow_run_dispatch_acknowledged_state_check": { + "name": "workflow_run_dispatch_acknowledged_state_check", + "value": "\"workflow_run_dispatch\".\"status\" <> 'acknowledged' or \"workflow_run_dispatch\".\"acknowledged_generation\" is not null" + }, + "workflow_run_dispatch_pending_schedule_check": { + "name": "workflow_run_dispatch_pending_schedule_check", + "value": "\"workflow_run_dispatch\".\"status\" <> 'pending' or \"workflow_run_dispatch\".\"next_attempt_at\" is not null" + }, + "workflow_run_dispatch_mail_sender_check": { + "name": "workflow_run_dispatch_mail_sender_check", + "value": "\"workflow_run_dispatch\".\"kind\" <> 'mail' or \"workflow_run_dispatch\".\"status\" in ('abandoned', 'settled', 'failed') or \"workflow_run_dispatch\".\"sender_address\" is not null" + } + }, + "isRLSEnabled": false + }, + "public.workflow_pending_projection": { + "name": "workflow_pending_projection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "history_required": { + "name": "history_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "workflow_pending_projection_anchor_idx": { + "name": "workflow_pending_projection_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_pending_projection_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_pending_projection_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_pending_projection", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run_launch_spec": { + "name": "workflow_run_launch_spec", + "schema": "", + "columns": { + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_domain": { + "name": "deployment_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_authority_principal_id": { + "name": "source_authority_principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "frozen_approval_bundle": { + "name": "frozen_approval_bundle", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "definition_artifact_id": { + "name": "definition_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_snapshot": { + "name": "grant_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_offering_ids": { + "name": "source_offering_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "default_source_offering_id": { + "name": "default_source_offering_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_package_pins": { + "name": "tool_package_pins", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_launch_spec_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_launch_spec_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_launch_spec", + "columnsFrom": ["anchor_run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workflow_run_launch_spec_source_authority_principal_id_principal_id_fk": { + "name": "workflow_run_launch_spec_source_authority_principal_id_principal_id_fk", + "tableFrom": "workflow_run_launch_spec", + "columnsFrom": ["source_authority_principal_id"], + "tableTo": "principal", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "workflow_run_launch_spec_definition_artifact_id_workflow_definition_artifact_id_fk": { + "name": "workflow_run_launch_spec_definition_artifact_id_workflow_definition_artifact_id_fk", + "tableFrom": "workflow_run_launch_spec", + "columnsFrom": ["definition_artifact_id"], + "tableTo": "workflow_definition_artifact", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run_execution": { + "name": "workflow_run_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_execution_run_id_id_idx": { + "name": "workflow_run_execution_run_id_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "workflow_run_execution_status_idx": { + "name": "workflow_run_execution_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_run_execution_run_id_workflow_run_id_fk": { + "name": "workflow_run_execution_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_execution", + "columnsFrom": ["run_id"], + "tableTo": "workflow_run", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_probe": { + "name": "workflow_probe", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_asset_id": { + "name": "definition_asset_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entry": { + "name": "entry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pin": { + "name": "pin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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": { + "workflow_probe_active_idx": { + "name": "workflow_probe_active_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"workflow_probe\".\"status\" in ('pending', 'provisioning', 'probing', 'releasing')", + "concurrently": false + }, + "workflow_probe_sidecar_idx": { + "name": "workflow_probe_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_probe_tenant_id_tenant_id_fk": { + "name": "workflow_probe_tenant_id_tenant_id_fk", + "tableFrom": "workflow_probe", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "workflow_probe_definition_asset_id_asset_id_fk": { + "name": "workflow_probe_definition_asset_id_asset_id_fk", + "tableFrom": "workflow_probe", + "columnsFrom": ["definition_asset_id"], + "tableTo": "asset", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "workflow_probe_sidecar_id_sidecar_id_fk": { + "name": "workflow_probe_sidecar_id_sidecar_id_fk", + "tableFrom": "workflow_probe", + "columnsFrom": ["sidecar_id"], + "tableTo": "sidecar", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_probe_status_check": { + "name": "workflow_probe_status_check", + "value": "\"workflow_probe\".\"status\" in ('pending', 'provisioning', 'probing', 'releasing', 'succeeded', 'failed')" + }, + "workflow_probe_succeeded_result_check": { + "name": "workflow_probe_succeeded_result_check", + "value": "\"workflow_probe\".\"status\" <> 'succeeded' or \"workflow_probe\".\"result\" is not null" + }, + "workflow_probe_generation_check": { + "name": "workflow_probe_generation_check", + "value": "\"workflow_probe\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_definition_artifact": { + "name": "workflow_definition_artifact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_definition_artifact_revision_idx": { + "name": "workflow_definition_artifact_revision_idx", + "columns": [ + { + "expression": "revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workflow_definition_artifact_revision_id_workflow_definition_revision_id_fk": { + "name": "workflow_definition_artifact_revision_id_workflow_definition_revision_id_fk", + "tableFrom": "workflow_definition_artifact", + "columnsFrom": ["revision_id"], + "tableTo": "workflow_definition_revision", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_definition_artifact_state_check": { + "name": "workflow_definition_artifact_state_check", + "value": "(\"workflow_definition_artifact\".\"result\" is null and \"workflow_definition_artifact\".\"lease_owner\" is not null and \"workflow_definition_artifact\".\"lease_expires_at\" is not null) or (\"workflow_definition_artifact\".\"result\" is not null and \"workflow_definition_artifact\".\"lease_owner\" is null and \"workflow_definition_artifact\".\"lease_expires_at\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.workflow_definition_revision": { + "name": "workflow_definition_revision", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_commit_sha": { + "name": "asset_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entry": { + "name": "entry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "closure": { + "name": "closure", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_definition_revision_tenant_id_tenant_id_fk": { + "name": "workflow_definition_revision_tenant_id_tenant_id_fk", + "tableFrom": "workflow_definition_revision", + "columnsFrom": ["tenant_id"], + "tableTo": "tenant", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "workflow_definition_revision_asset_id_asset_id_fk": { + "name": "workflow_definition_revision_asset_id_asset_id_fk", + "tableFrom": "workflow_definition_revision", + "columnsFrom": ["asset_id"], + "tableTo": "asset", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index c29473538..862cd22e9 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -841,6 +841,13 @@ "when": 1793841983485, "tag": "0120_create_run_principals", "breakpoints": true + }, + { + "idx": 121, + "version": "7", + "when": 1793842006891, + "tag": "0121_run_principal_sidecar_keys", + "breakpoints": true } ] } diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 8ad373b9a..af2bd6cb0 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -46,6 +46,10 @@ export { type CreatePrincipalKeyStoreDeps, type PrincipalKeyStore, } from "./principal-key-store"; +export { + recordRunPrincipalKey, + retireRunPrincipalKey, +} from "./run-principal-key"; export { createRunTokenStore, deleteRunTokensForPrincipals, diff --git a/packages/db/src/run-principal-key.ts b/packages/db/src/run-principal-key.ts new file mode 100644 index 000000000..f3780b9ff --- /dev/null +++ b/packages/db/src/run-principal-key.ts @@ -0,0 +1,63 @@ +import { and, eq } from "drizzle-orm"; + +import type { DBExecutor } from "./client"; +import { recordPublicKey, retireActiveKey } from "./principal-key-store"; +import { principal } from "./schema/principals"; + +/** + * The run principal of a run: the `kind = 'workflow'` principal whose `refId` + * is the run id. A deployment's run principal exists from deploy. Returns + * null when the run has none. + */ +async function loadRunPrincipalId( + executor: DBExecutor, + tenantId: string, + runId: string, +): Promise { + const [row] = await executor + .select({ id: principal.id }) + .from(principal) + .where( + and( + eq(principal.tenantId, tenantId), + eq(principal.kind, "workflow"), + eq(principal.refId, runId), + ), + ) + .limit(1); + return row?.id ?? null; +} + +/** + * Make `publicKey`, which the sidecar running the run minted, the active key + * of the run's principal. The sidecar holds the private key. The caller holds + * the run's anchor row lock, which serializes every writer of a run's key. + * Throws when the run has no run principal. + */ +export async function recordRunPrincipalKey( + tx: DBExecutor, + tenantId: string, + runId: string, + publicKey: string, +): Promise { + const principalId = await loadRunPrincipalId(tx, tenantId, runId); + if (principalId === null) { + throw new Error(`Run ${runId} has no run principal to hold its key`); + } + await recordPublicKey(tx, principalId, publicKey); +} + +/** + * Retire the active key of the run's principal, so the key a replaced sidecar + * held no longer answers for the run. A run with no run principal, or with no + * active key, is left as it is. + */ +export async function retireRunPrincipalKey( + tx: DBExecutor, + tenantId: string, + runId: string, +): Promise { + const principalId = await loadRunPrincipalId(tx, tenantId, runId); + if (principalId === null) return; + await retireActiveKey(tx, principalId); +} diff --git a/packages/db/src/sidecar-allocation-store.ts b/packages/db/src/sidecar-allocation-store.ts index 6a67d69d7..4cff78846 100644 --- a/packages/db/src/sidecar-allocation-store.ts +++ b/packages/db/src/sidecar-allocation-store.ts @@ -21,6 +21,10 @@ import { import type { DB, DBExecutor } from "./client"; import { deactivateRunPrincipals } from "./principal-store"; +import { + recordRunPrincipalKey, + retireRunPrincipalKey, +} from "./run-principal-key"; import { createWorkflowPendingProjectionStore } from "./workflow-pending-projection-store"; import { assertSidecarHasRoom, @@ -581,6 +585,14 @@ function buildSidecarAllocationStore( .returning({ id: workflowRun.id }); if (anchor === undefined) throw new Error("Initialization anchor is missing"); + // The anchor update above holds the anchor row lock, which serializes + // this with every other writer of the run's key. + await recordRunPrincipalKey( + tx, + args.tenantId, + args.anchorRunId, + completion.publicKey, + ); } // Check the lease again: ownership can expire while the transaction holds // the allocation lock, such as while a completion waits on the anchor @@ -1529,11 +1541,13 @@ function buildSidecarAllocationStore( // the credential refs with the generation advance so a replacement // cannot mistake stale local sidecar state for a restored // workflow. Leftover refs are the previous attempt, and a first - // grant commit would freeze them. + // grant commit would freeze them. The replacement sidecar mints a + // new key, so the run principal's key retires with it. await tx .update(workflowRun) .set({ publicKey: null, credentialRefs: null }) .where(eq(workflowRun.id, updated.anchorRunId)); + await retireRunPrincipalKey(tx, updated.tenantId, updated.anchorRunId); return parseSidecarAllocationRow(updated); }); diff --git a/packages/hub-sessions/src/session-service.test.ts b/packages/hub-sessions/src/session-service.test.ts index f0ea592c0..ac8c9619c 100644 --- a/packages/hub-sessions/src/session-service.test.ts +++ b/packages/hub-sessions/src/session-service.test.ts @@ -14,6 +14,8 @@ import { WorkflowProjectionDefinition, } from "@intx/types/sidecar"; import { + principal as principalTable, + principalKey as principalKeyTable, sidecarAllocation, workflowRun as workflowRunTable, } from "@intx/db/schema"; @@ -485,6 +487,28 @@ describe("deployCodeSourcedWorkflow", () => { // before reaching the insert. let capturedAnchorRow: Record | undefined; let capturedAnchorUpdate: Record | undefined; + + // Recording a deployment's key reads its run principal and the principal's + // keys, then inserts the key. Answer those reads with the run principal and + // no keys, and pass every other read to the wrapped double. + function withRunPrincipalKeys(handle: T): T { + const inner = handle as { + select?: () => { from: (table: unknown) => unknown }; + insert?: unknown; + }; + return { + ...handle, + select: () => ({ + from: (table: unknown) => + table === principalTable + ? { where: () => ({ limit: async () => [{ id: "prn_run" }] }) } + : table === principalKeyTable + ? { where: () => ({ limit: async () => [] }) } + : inner.select?.().from(table), + }), + insert: inner.insert ?? (() => ({ values: async () => undefined })), + }; + } // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- test stub: only the definition existence query, the anchor insert, and the success-path public-key update are exercised const CAPTURING_DB = { query: { @@ -544,7 +568,8 @@ describe("deployCodeSourcedWorkflow", () => { }; }, }), - transaction: (run: (tx: unknown) => Promise) => run(CAPTURING_DB), + transaction: (run: (tx: unknown) => Promise) => + run(withRunPrincipalKeys(CAPTURING_DB)), } as unknown as DB["db"]; async function capturePreparedDeployError( @@ -628,7 +653,7 @@ describe("deployCodeSourcedWorkflow", () => { const db = { ...CAPTURING_DB, transaction: async (run: (handle: typeof tx) => Promise) => { - const result = await run(tx); + const result = await run(withRunPrincipalKeys(tx)); if (writes.some(({ status }) => status === "releasing")) events.push("rejection committed"); return result; @@ -754,24 +779,26 @@ describe("deployCodeSourcedWorkflow", () => { let nextKey = publicKey; let nextMarker = initializationLeaseId; const result = await run( - transactionHandle((values) => { - if ("publicKey" in values) { - if ( - values.publicKey !== null && - typeof values.publicKey !== "string" - ) - throw new Error("Invalid key write"); - nextKey = values.publicKey; - } - if ("initializationLeaseId" in values) { - if ( - values.initializationLeaseId !== null && - typeof values.initializationLeaseId !== "string" - ) - throw new Error("Invalid marker write"); - nextMarker = values.initializationLeaseId; - } - }), + withRunPrincipalKeys( + transactionHandle((values) => { + if ("publicKey" in values) { + if ( + values.publicKey !== null && + typeof values.publicKey !== "string" + ) + throw new Error("Invalid key write"); + nextKey = values.publicKey; + } + if ("initializationLeaseId" in values) { + if ( + values.initializationLeaseId !== null && + typeof values.initializationLeaseId !== "string" + ) + throw new Error("Invalid marker write"); + nextMarker = values.initializationLeaseId; + } + }), + ), ); if (transactions === 1 || scenario !== "rolled back publication") { publicKey = nextKey; @@ -1063,7 +1090,7 @@ describe("deployCodeSourcedWorkflow", () => { const db = { ...CAPTURING_DB, transaction: async (run: (handle: typeof tx) => Promise) => - run(tx), + run(withRunPrincipalKeys(tx)), } as unknown as DB["db"]; const allocationRouter = createMockAllocationRouter(); allocationRouter.sendAgentDeployToAllocation = async ( @@ -1168,7 +1195,7 @@ describe("deployCodeSourcedWorkflow", () => { const db = { ...CAPTURING_DB, transaction: async (run: (handle: typeof tx) => Promise) => - run(tx), + run(withRunPrincipalKeys(tx)), select: () => ({ from: () => ({ where: async () => { @@ -1308,7 +1335,7 @@ describe("deployCodeSourcedWorkflow", () => { workflowRun: { findFirst: async () => ({ publicKey: null }) }, }, transaction: async (run: (handle: typeof tx) => Promise) => { - const result = await run(tx); + const result = await run(withRunPrincipalKeys(tx)); if (lostResponse === "reservation") throw new Error("Transaction response lost after commit"); return result; @@ -1502,7 +1529,7 @@ describe("deployCodeSourcedWorkflow", () => { const db = { ...CAPTURING_DB, transaction: async (run: (tx: typeof handle) => Promise) => - run(handle), + run(withRunPrincipalKeys(handle)), } as unknown as DB["db"]; const preparedRepoStore = createMockRepoStore(); preparedRepoStore.repoStore.resolveRef = async () => null; @@ -2009,7 +2036,8 @@ describe("deployCodeSourcedWorkflow", () => { }), }), }), - transaction: (run: (tx: unknown) => Promise) => run(COUNTING_DB), + transaction: (run: (tx: unknown) => Promise) => + run(withRunPrincipalKeys(COUNTING_DB)), update: () => ({ set: () => ({ where: () => ({ @@ -2195,7 +2223,8 @@ describe("deployCodeSourcedWorkflow", () => { }), }), }), - transaction: (run: (tx: unknown) => Promise) => run(COUNTING_DB), + transaction: (run: (tx: unknown) => Promise) => + run(withRunPrincipalKeys(COUNTING_DB)), update: () => ({ set: () => ({ where: () => ({ diff --git a/packages/hub-sessions/src/session-service.ts b/packages/hub-sessions/src/session-service.ts index 50422c5bd..edb77523d 100644 --- a/packages/hub-sessions/src/session-service.ts +++ b/packages/hub-sessions/src/session-service.ts @@ -7,6 +7,7 @@ import { createWorkflowDefinitionArtifactStore, deactivateRunPrincipals, insertRunPrincipal, + recordRunPrincipalKey, resolveInferenceMaterials, type DB, type SidecarAllocation, @@ -1013,18 +1014,30 @@ export async function deployCodeSourcedWorkflow( // "running", and skipping the stamp there would strand a live run with a null // key. A 0-row update is an anomaly (nothing should remove a deployed anchor // on the success path), but the deploy succeeded, so log it rather than - // failing a live run. - const stamped = await args.db - .update(workflowRunTable) - .set({ publicKey }) - .where( - and( - eq(workflowRunTable.id, args.anchorRunId), - eq(workflowRunTable.anchorRunId, args.anchorRunId), - eq(workflowRunTable.tenantId, args.tenantId), - ), - ) - .returning({ id: workflowRunTable.id }); + // failing a live run. The update holds the anchor row lock, which + // serializes recording the key on the run principal. + const stamped = await args.db.transaction(async (tx) => { + const rows = await tx + .update(workflowRunTable) + .set({ publicKey }) + .where( + and( + eq(workflowRunTable.id, args.anchorRunId), + eq(workflowRunTable.anchorRunId, args.anchorRunId), + eq(workflowRunTable.tenantId, args.tenantId), + ), + ) + .returning({ id: workflowRunTable.id }); + if (rows.length > 0) { + await recordRunPrincipalKey( + tx, + args.tenantId, + args.anchorRunId, + publicKey, + ); + } + return rows; + }); if (stamped.length === 0) { logger.error`anchor-before-frame: anchor ${args.anchorRunId} vanished before its public key could be stamped on a successful deploy`; } diff --git a/packages/types/src/sidecar.test.ts b/packages/types/src/sidecar.test.ts index 232011bce..1b91b4299 100644 --- a/packages/types/src/sidecar.test.ts +++ b/packages/types/src/sidecar.test.ts @@ -2,6 +2,7 @@ import { describe, test, expect } from "bun:test"; import { type } from "arktype"; import { APPROVAL_SNAPSHOT_MAX_BYTES } from "./runtime"; import { + AgentDeployAckFrame, AgentDeployFrame, fitDeploymentError, CredentialsUpdateFrame, @@ -55,6 +56,32 @@ describe("MailOutboundFrame sender ownership claim", () => { }); }); +describe("AgentDeployAckFrame public key", () => { + // The hub records the key as the run principal's key, which must be the + // 32-byte key the sidecar minted, in the lowercase hex it encodes. + const ack = { + type: "agent.deploy.ack", + requestId: "req-1", + agentAddress: "run_deploy@tenant.example", + generation: 1, + publicKey: "ab".repeat(32), + }; + + test("accepts 32 bytes of lowercase hex", () => { + expect(AgentDeployAckFrame(ack) instanceof type.errors).toBe(false); + }); + + test.each([ + ["uppercase hex", "AB".repeat(32)], + ["a 16-byte key", "ab".repeat(16)], + ["non-hex characters", "zz".repeat(32)], + ])("rejects %s", (_label, publicKey) => { + expect( + AgentDeployAckFrame({ ...ack, publicKey }) instanceof type.errors, + ).toBe(true); + }); +}); + describe("DeployApplyErrorCategory", () => { const allCategories = [ "tarball.missing", diff --git a/packages/types/src/sidecar.ts b/packages/types/src/sidecar.ts index 670f9cb83..faa05398e 100644 --- a/packages/types/src/sidecar.ts +++ b/packages/types/src/sidecar.ts @@ -261,15 +261,16 @@ export type HelloFrame = typeof HelloFrame.infer; /** * Acknowledges a successful agent deployment. Includes the agent's Ed25519 - * public key (hex-encoded) for published identity and content provenance. - * Echoes the request id and incarnation of the `agent.deploy` it answers. + * public key, 32 bytes as lowercase hex, for published identity and content + * provenance. Echoes the request id and incarnation of the `agent.deploy` it + * answers. */ export const AgentDeployAckFrame = type({ type: "'agent.deploy.ack'", requestId: "string", agentAddress: "string", generation: Generation, - publicKey: "string", + publicKey: /^[0-9a-f]{64}$/, }); export type AgentDeployAckFrame = typeof AgentDeployAckFrame.infer; diff --git a/tests/db/deployed-window.test.ts b/tests/db/deployed-window.test.ts index 23bd559f5..f6b59ccaf 100644 --- a/tests/db/deployed-window.test.ts +++ b/tests/db/deployed-window.test.ts @@ -10,6 +10,7 @@ import { import { eq } from "drizzle-orm"; import { createWorkflowRunStore } from "@intx/db"; +import { deriveRunPrincipalId } from "@intx/hub-common"; import { workflowDefinition, workflowRun } from "@intx/db/schema"; import { createTestDb, @@ -86,21 +87,16 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(beforeTrigger?.status).toBe("deployed"); expect(beforeTrigger?.principalId).toBeNull(); - // The first trigger mints a principal and reconciles the anchor. - await seedPrincipal(h.db, { - id: "prn-run", - tenantId: TENANT, - kind: "workflow", - refId: RUN_ID, - status: "active", - }); + // The first trigger links the run principal the deploy created and + // reconciles the anchor. + const runPrincipalId = await deriveRunPrincipalId(TENANT, RUN_ID); const store = createWorkflowRunStore(h.db); const linked = await store.anchorWithPrincipal({ id: RUN_ID, anchorRunId: RUN_ID, tenantId: TENANT, definitionId: DEFINITION, - principalId: "prn-run", + principalId: runPrincipalId, status: "running", }); expect(linked).toBe(true); @@ -110,7 +106,7 @@ describe.skipIf(!harnessDbEnvAvailable())( .from(workflowRun) .where(eq(workflowRun.id, RUN_ID)); expect(afterFirst?.status).toBe("running"); - expect(afterFirst?.principalId).toBe("prn-run"); + expect(afterFirst?.principalId).toBe(runPrincipalId); // A second trigger (a redelivery) reconciles nothing: the three-part guard // no longer matches once the anchor is "running", so it is a no-op on the @@ -137,7 +133,7 @@ describe.skipIf(!harnessDbEnvAvailable())( .from(workflowRun) .where(eq(workflowRun.id, RUN_ID)); expect(afterSecond?.status).toBe("running"); - expect(afterSecond?.principalId).toBe("prn-run"); + expect(afterSecond?.principalId).toBe(runPrincipalId); }); }, ); diff --git a/tests/db/migration-0121-run-principal-sidecar-keys.test.ts b/tests/db/migration-0121-run-principal-sidecar-keys.test.ts new file mode 100644 index 000000000..f3be22438 --- /dev/null +++ b/tests/db/migration-0121-run-principal-sidecar-keys.test.ts @@ -0,0 +1,209 @@ +// Migration 0121 moves each deployment's sidecar key onto its run principal +// once, against real data, so this replays it over seeded rows. + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; +import { asc, eq, sql } from "drizzle-orm"; + +import { createPrincipalKeyStore, recordPublicKey } from "@intx/db"; +import { principalKey, workflowRun } from "@intx/db/schema"; +import { createNoopCredentialCipher } from "@intx/crypto"; +import { + createTestDb, + harnessDbEnvAvailable, + type TestDb, +} from "@intx/test-harness/db-harness"; +import { + seedPrincipal, + seedTenants, + seedWorkflowRun, +} from "@intx/test-harness/seed"; + +const MIGRATION_SQL = readFileSync( + join( + import.meta.dir, + "../../packages/db/migrations/0121_run_principal_sidecar_keys.sql", + ), + "utf8", +); +const TENANT = "tnt-migration-0121"; +const KEY_A = "aa".repeat(32); +const KEY_B = "bb".repeat(32); + +describe.skipIf(!harnessDbEnvAvailable())( + "migration 0121 run principal sidecar keys", + () => { + let h: TestDb; + beforeAll(async () => { + h = await createTestDb(); + }); + afterAll(async () => { + await h.close(); + }); + beforeEach(async () => { + await h.reset(); + await seedTenants(h.db, [{ id: TENANT }]); + }); + + // An anchor run and the run principal deploy created, with the key the + // deploy ack recorded only on the anchor, as before the migration. + async function seedAnchor( + runId: string, + publicKey: string | null, + status: "deployed" | "running" | "failed" = "running", + ): Promise { + await seedWorkflowRun(h.db, { + id: runId, + anchorRunId: runId, + tenantId: TENANT, + status, + }); + await h.db + .update(workflowRun) + .set({ publicKey }) + .where(eq(workflowRun.id, runId)); + const principalId = `prn_${runId}`; + await seedPrincipal(h.db, { + id: principalId, + tenantId: TENANT, + kind: "workflow", + refId: runId, + status: "active", + }); + return principalId; + } + + async function keysOf(principalId: string) { + return h.db + .select({ + publicKey: principalKey.publicKey, + privateKey: principalKey.privateKey, + status: principalKey.status, + }) + .from(principalKey) + .where(eq(principalKey.principalId, principalId)) + .orderBy(asc(principalKey.createdAt)); + } + + // The query builder returns a thenable; adopt it so a rejection reaches + // the assertion. + function replay(): Promise { + return Promise.resolve(h.db.execute(sql.raw(MIGRATION_SQL))); + } + + function hubKeys() { + return createPrincipalKeyStore({ + db: h.db, + cipher: createNoopCredentialCipher(), + }); + } + + test("replaces a run principal's hub key with its deployment's sidecar key", async () => { + const fired = await seedAnchor("run_fired", KEY_A); + const ended = await seedAnchor("run_ended", KEY_B, "failed"); + const unacked = await seedAnchor("run_unacked", null, "deployed"); + // A run principal that fired before deploy created it got a hub key. + await hubKeys().generate(fired); + // A definition-level workflow principal keeps its key. + await seedPrincipal(h.db, { + id: "prn_definition", + tenantId: TENANT, + kind: "workflow", + refId: "wfd_definition", + status: "active", + }); + const definitionKey = await hubKeys().generate("prn_definition"); + + await h.db.execute(sql.raw(MIGRATION_SQL)); + // A second replay changes nothing. + await h.db.execute(sql.raw(MIGRATION_SQL)); + + expect(await keysOf(fired)).toEqual([ + { publicKey: KEY_A, privateKey: null, status: "active" }, + ]); + expect(await keysOf(ended)).toEqual([ + { publicKey: KEY_B, privateKey: null, status: "active" }, + ]); + expect(await keysOf(unacked)).toEqual([]); + expect(await keysOf("prn_definition")).toEqual([ + expect.objectContaining({ publicKey: definitionKey, status: "active" }), + ]); + }); + + test("refuses a key that is not 32 bytes of lowercase hex", async () => { + await seedAnchor("run_malformed", "not-a-key"); + + await expect(replay()).rejects.toThrow(/not 32 bytes of lowercase hex/); + }); + + test("refuses two deployments that hold the same key", async () => { + await seedAnchor("run_first", KEY_A); + await seedAnchor("run_second", KEY_A); + + await expect(replay()).rejects.toThrow(/hold the same public key/); + }); + + test("refuses a deployment key another principal already holds", async () => { + await seedAnchor("run_keyed", KEY_A); + await seedPrincipal(h.db, { + id: "prn_other", + tenantId: TENANT, + kind: "user", + refId: "usr_other", + status: "active", + }); + await recordPublicKey(h.db, "prn_other", KEY_A); + + await expect(replay()).rejects.toThrow( + /already held by another principal/, + ); + }); + + test("refuses when a run principal already holds a different key", async () => { + const principalId = await seedAnchor("run_keyed", KEY_A); + await recordPublicKey(h.db, principalId, KEY_B); + + await expect(replay()).rejects.toThrow( + /was not copied to its run principal/, + ); + }); + + test("refuses a keyed deployment with no run principal", async () => { + await seedWorkflowRun(h.db, { + id: "run_orphan", + anchorRunId: "run_orphan", + tenantId: TENANT, + }); + await h.db + .update(workflowRun) + .set({ publicKey: KEY_A }) + .where(eq(workflowRun.id, "run_orphan")); + + await expect(replay()).rejects.toThrow(/has no run principal/); + }); + + test("refuses a key on a run that is not a deployment anchor", async () => { + await seedAnchor("run_parent", null); + await seedWorkflowRun(h.db, { + id: "run_child", + anchorRunId: "run_parent", + tenantId: TENANT, + }); + await h.db + .update(workflowRun) + .set({ publicKey: KEY_A }) + .where(eq(workflowRun.id, "run_child")); + + await expect(replay()).rejects.toThrow(/not a deployment anchor/); + }); + }, +); diff --git a/tests/db/run-principal-key.test.ts b/tests/db/run-principal-key.test.ts new file mode 100644 index 000000000..3c9150b5b --- /dev/null +++ b/tests/db/run-principal-key.test.ts @@ -0,0 +1,94 @@ +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; +import { asc, eq } from "drizzle-orm"; + +import { recordRunPrincipalKey, retireRunPrincipalKey } from "@intx/db"; +import { principalKey } from "@intx/db/schema"; +import { + createTestDb, + harnessDbEnvAvailable, + type TestDb, +} from "@intx/test-harness/db-harness"; +import { seedPrincipal, seedTenants } from "@intx/test-harness/seed"; + +const TENANT = "tnt_rpk"; +const RUN_ID = "run_rpk"; +const RUN_PRINCIPAL = "prn_run_rpk"; +const FIRST_KEY = "aa".repeat(32); +const REPLACEMENT_KEY = "bb".repeat(32); + +// A deployment's sidecar key moves through the run principal: the first +// sidecar records it, a replacement retires it, and the replacement records +// its own key -- or the first key again when it reuses the same disk. +describe.skipIf(!harnessDbEnvAvailable())( + "run principal keys (real DB)", + () => { + let h: TestDb; + + beforeAll(async () => { + h = await createTestDb(); + }); + + afterAll(async () => { + await h.close(); + }); + + beforeEach(async () => { + await h.reset(); + await seedTenants(h.db, [{ id: TENANT }]); + await seedPrincipal(h.db, { + id: RUN_PRINCIPAL, + tenantId: TENANT, + kind: "workflow", + refId: RUN_ID, + status: "active", + }); + }); + + async function keys() { + return h.db + .select({ + publicKey: principalKey.publicKey, + status: principalKey.status, + }) + .from(principalKey) + .where(eq(principalKey.principalId, RUN_PRINCIPAL)) + .orderBy(asc(principalKey.publicKey)); + } + + test("a replacement retires the key and records its own", async () => { + await recordRunPrincipalKey(h.db, TENANT, RUN_ID, FIRST_KEY); + await retireRunPrincipalKey(h.db, TENANT, RUN_ID); + await recordRunPrincipalKey(h.db, TENANT, RUN_ID, REPLACEMENT_KEY); + + expect(await keys()).toEqual([ + { publicKey: FIRST_KEY, status: "retired" }, + { publicKey: REPLACEMENT_KEY, status: "active" }, + ]); + }); + + test("a replacement that reports the retired key makes it active again", async () => { + await recordRunPrincipalKey(h.db, TENANT, RUN_ID, FIRST_KEY); + await retireRunPrincipalKey(h.db, TENANT, RUN_ID); + await recordRunPrincipalKey(h.db, TENANT, RUN_ID, FIRST_KEY); + + expect(await keys()).toEqual([ + { publicKey: FIRST_KEY, status: "active" }, + ]); + }); + + test("a run with no run principal cannot record a key and has none to retire", async () => { + await expect( + recordRunPrincipalKey(h.db, TENANT, "run_missing", FIRST_KEY), + ).rejects.toThrow(/has no run principal/); + await retireRunPrincipalKey(h.db, TENANT, "run_missing"); + expect(await keys()).toEqual([]); + }); + }, +); diff --git a/tests/db/sender-key-resolver.test.ts b/tests/db/sender-key-resolver.test.ts index 69e667374..ba1a5f518 100644 --- a/tests/db/sender-key-resolver.test.ts +++ b/tests/db/sender-key-resolver.test.ts @@ -467,7 +467,7 @@ describe.skipIf(!harnessDbEnvAvailable())("resolveSenderKey (real DB)", () => { id: "run_a2", tenantId: "tnt_a", address: "run_a2@a.localhost", - publicKey: RUN_PUBLIC_KEY, + publicKey: "cd".repeat(32), status: "completed", }); await seedWorkflowRun(h.db, { diff --git a/tests/db/sidecar-allocation-store.test.ts b/tests/db/sidecar-allocation-store.test.ts index 4109541ae..3402388fe 100644 --- a/tests/db/sidecar-allocation-store.test.ts +++ b/tests/db/sidecar-allocation-store.test.ts @@ -29,9 +29,11 @@ import { SIDECAR_CLEANUP_PREPARATION_TIMEOUT_FAILURE_CODE, SIDECAR_CLEANUP_OPERATOR_RELEASED_FAILURE_CODE, SidecarReuseRejectedError, + recordPublicKey, type RetainedSidecarIncarnation, } from "@intx/db"; import { + principalKey, sidecar, sidecarAllocation, workflowDefinition, @@ -55,6 +57,7 @@ import { const TENANT_ID = "tnt-allocation"; const DEFINITION_ID = "wfd-allocation"; const ANCHOR_RUN_ID = "dep-allocation"; +const ANCHOR_RUN_PRINCIPAL_ID = "prn-dep-allocation"; const PRINCIPAL_ID = "prn-allocation"; const MANIFEST_DIGEST = "1".repeat(64); const OTHER_MANIFEST_DIGEST = "2".repeat(64); @@ -72,6 +75,18 @@ describe.skipIf(!harnessDbEnvAvailable())( await h.close(); }); + // The keys of the anchor's run principal, oldest first. + async function runPrincipalKeys() { + return h.db + .select({ + publicKey: principalKey.publicKey, + status: principalKey.status, + }) + .from(principalKey) + .where(eq(principalKey.principalId, ANCHOR_RUN_PRINCIPAL_ID)) + .orderBy(principalKey.createdAt); + } + beforeEach(async () => { await h.reset(); await seedTenants(h.db, [{ id: TENANT_ID }]); @@ -93,6 +108,14 @@ describe.skipIf(!harnessDbEnvAvailable())( tenantId: TENANT_ID, definitionId: DEFINITION_ID, }); + // The run principal deploy creates; it holds the deployment's key. + await seedPrincipal(h.db, { + id: ANCHOR_RUN_PRINCIPAL_ID, + tenantId: TENANT_ID, + kind: "workflow", + refId: ANCHOR_RUN_ID, + status: "active", + }); await createWorkflowRunLaunchSpecStore(h.db).create({ anchorRunId: ANCHOR_RUN_ID, sessionId: "ses-allocation", @@ -3459,12 +3482,46 @@ describe.skipIf(!harnessDbEnvAvailable())( where: eq(workflowRun.id, ANCHOR_RUN_ID), }), ).toMatchObject({ publicKey: "new-key", credentialRefs }); + // The same completion makes the key the run principal's, public-only. + expect(await runPrincipalKeys()).toEqual([ + { publicKey: "new-key", status: "active" }, + ]); expect( await store.completeInitialization({ ...initialization, publicKey: "late-key", }), ).toBe(false); + expect(await runPrincipalKeys()).toEqual([ + { publicKey: "new-key", status: "active" }, + ]); + }); + + test("a completion whose key cannot be recorded leaves the anchor and lease unchanged", async () => { + const { store, allocation, leaseId, initialization } = + await createClaimedAllocation("alloc-key-refused"); + expect(await store.beginInitialization(initialization)).toBe(true); + // Another principal already holds the key the sidecar reports. + await recordPublicKey(h.db, PRINCIPAL_ID, "ab".repeat(32)); + + await expect( + store.completeInitialization({ + ...initialization, + publicKey: "ab".repeat(32), + }), + ).rejects.toThrow(/belongs to principal/); + + expect( + ( + await h.db.query.workflowRun.findFirst({ + where: eq(workflowRun.id, ANCHOR_RUN_ID), + }) + )?.publicKey, + ).toBeNull(); + expect(await runPrincipalKeys()).toEqual([]); + expect((await store.findById(allocation.id))?.initializationLeaseId).toBe( + leaseId, + ); }); test("a proven rejection survives disconnect and a new reconciliation claim", async () => { @@ -4032,6 +4089,11 @@ describe.skipIf(!harnessDbEnvAvailable())( credentialRefs: previousRefs, }) .where(eq(workflowRun.id, ANCHOR_RUN_ID)); + await recordPublicKey( + h.db, + ANCHOR_RUN_PRINCIPAL_ID, + "generation-1-public-key", + ); expect( await store.beginReplacement({ @@ -4054,6 +4116,9 @@ describe.skipIf(!harnessDbEnvAvailable())( publicKey: "generation-1-public-key", credentialRefs: previousRefs, }); + expect(await runPrincipalKeys()).toEqual([ + { publicKey: "generation-1-public-key", status: "active" }, + ]); const replacing = await store.beginReplacement({ allocationId: pending.id, @@ -4081,6 +4146,10 @@ describe.skipIf(!harnessDbEnvAvailable())( columns: { publicKey: true, credentialRefs: true }, }), ).toEqual({ publicKey: null, credentialRefs: null }); + // The replaced sidecar's key no longer answers for the run. + expect(await runPrincipalKeys()).toEqual([ + { publicKey: "generation-1-public-key", status: "retired" }, + ]); await h.db .update(sidecarAllocation) @@ -5928,14 +5997,7 @@ describe.skipIf(!harnessDbEnvAvailable())( .update(workflowRun) .set({ status: "deployed" }) .where(eq(workflowRun.id, ANCHOR_RUN_ID)); - // Deploy created the run principal; no trigger has linked it yet. - await seedPrincipal(h.db, { - id: "prn-deployed-anchor", - tenantId: TENANT_ID, - kind: "workflow", - refId: ANCHOR_RUN_ID, - status: "active", - }); + // The fixture's run principal is unlinked: no trigger has fired. const store = createSidecarAllocationStore(h.db); const pending = await store.createPending({ @@ -5984,7 +6046,7 @@ describe.skipIf(!harnessDbEnvAvailable())( ).toEqual({ status: "failed", endedAt }); expect( await h.db.query.principal.findFirst({ - where: (row, { eq }) => eq(row.id, "prn-deployed-anchor"), + where: (row, { eq }) => eq(row.id, ANCHOR_RUN_PRINCIPAL_ID), columns: { status: true }, }), ).toEqual({ status: "deactivated" }); diff --git a/tests/db/sidecar-lifecycle-integration.test.ts b/tests/db/sidecar-lifecycle-integration.test.ts index 4026fb1c1..a68ea77ee 100644 --- a/tests/db/sidecar-lifecycle-integration.test.ts +++ b/tests/db/sidecar-lifecycle-integration.test.ts @@ -13,6 +13,7 @@ import path from "node:path"; import { eq, inArray } from "drizzle-orm"; import { generateKeyPair, sha256 } from "@intx/crypto"; +import { hexEncode } from "@intx/types"; import { createSidecarAllocationStore, createWorkflowPendingProjectionStore, @@ -98,11 +99,12 @@ describe.skipIf(!harnessDbEnvAvailable())( tenantId: TENANT, address, status: "failed", + // Each copy's deployment holds its own key on its run principal. + publicKey: hexEncode(await sha256(id)), }); await h.db .update(workflowRun) .set({ - publicKey: "ab".repeat(32), endedAt: now, failureCode: "original_failure", failureMessage: "Original workflow failure", diff --git a/tests/lib/package.json b/tests/lib/package.json index 6ac36d09b..4188cdb2d 100644 --- a/tests/lib/package.json +++ b/tests/lib/package.json @@ -42,6 +42,7 @@ "dependencies": { "@intx/crypto": "workspace:*", "@intx/db": "workspace:*", + "@intx/hub-common": "workspace:*", "@intx/types": "workspace:*", "drizzle-orm": "catalog:", "postgres": "catalog:" diff --git a/tests/lib/seed.ts b/tests/lib/seed.ts index cdd278523..3688e528c 100644 --- a/tests/lib/seed.ts +++ b/tests/lib/seed.ts @@ -8,7 +8,12 @@ // assertions actually depend on. import type { DB } from "@intx/db"; -import { createPrincipalKeyStore } from "@intx/db"; +import { + createPrincipalKeyStore, + insertRunPrincipal, + recordRunPrincipalKey, +} from "@intx/db"; +import { deriveRunPrincipalId } from "@intx/hub-common"; import type { WorkflowRunCredentialRefs } from "@intx/db/schema"; import { TenantSlug, type GrantWalkSnapshot } from "@intx/types"; import { createNoopCredentialCipher } from "@intx/crypto"; @@ -383,7 +388,9 @@ export async function seedWorkflowRun( await db.insert(workflowRun).values({ id: r.id, tenantId: r.tenantId, - anchorRunId: r.anchorRunId ?? null, + // Only a deployment anchor holds a key, so a keyed run is its own anchor + // unless the caller names one. + anchorRunId: r.anchorRunId ?? (r.publicKey != null ? r.id : null), definitionId, principalId: r.principalId ?? null, address: r.address ?? null, @@ -395,6 +402,18 @@ export async function seedWorkflowRun( ...(r.createdAt !== undefined ? { createdAt: r.createdAt } : {}), ...(r.endedAt !== undefined ? { endedAt: r.endedAt } : {}), }); + // A recorded key also lives on the run principal, as a deploy records it. + // The run principal is the one a test already seeded for the run, or the + // one deploy would have created. + if (r.publicKey != null) { + await insertRunPrincipal(db, { + id: await deriveRunPrincipalId(r.tenantId, r.id), + tenantId: r.tenantId, + runId: r.id, + createdAt: r.createdAt ?? new Date(), + }); + await recordRunPrincipalKey(db, r.tenantId, r.id, r.publicKey); + } } export type SeedWorkflowRunLaunchSpec = { From 589d013f4b50968fcc4fdfc5b8878d5659eb894f Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 16:00:33 -0700 Subject: [PATCH 10/14] Read run signing keys from the run principal A replaced sidecar's key is retired on its run principal, so reading the active key there stops a retired key from verifying mail. --- apps/sidecar/src/index.ts | 6 +- docs/INBOUND_MAIL_POLICY.md | 3 +- packages/db/src/index.ts | 3 + packages/db/src/run-principal-key.ts | 84 +++++++++++- packages/db/src/sender-key-resolver.ts | 51 +++++--- packages/db/src/sidecar-allocation-store.ts | 21 +-- .../hub-agent/src/sender-key-cache.test.ts | 4 +- packages/hub-agent/src/sender-key-cache.ts | 9 +- packages/hub-api/src/routes/runs.ts | 8 +- packages/hub-api/src/routes/workflows.test.ts | 27 +++- .../src/run-grant-materialization.test.ts | 36 ++++-- .../hub-api/src/run-grant-materialization.ts | 32 +++-- packages/hub-api/src/workflow-run-trigger.ts | 9 +- .../hub-sessions/src/hub-session-lookups.ts | 21 ++- .../hub-sessions/src/session-service.test.ts | 43 +++--- packages/hub-sessions/src/session-service.ts | 31 +++-- .../src/workflow-allocation-service.ts | 14 +- .../src/workflow-lifecycle-service.ts | 5 +- .../src/ws/sidecar-handler.mail.test.ts | 6 +- .../src/ws/sidecar-handler.test.ts | 2 +- .../hub-sessions/src/ws/sidecar-handler.ts | 31 +++-- .../ws/sidecar-token-authenticator.test.ts | 26 +++- .../src/ws/sidecar-token-authenticator.ts | 10 +- tests/db/mail-accept-backfill.test.ts | 37 +++++- tests/db/mail-triggered-run-grants.test.ts | 122 +++++++++--------- tests/db/run-principal-key.test.ts | 64 ++++++++- tests/db/sender-key-resolver.test.ts | 29 ++++- tests/db/sidecar-allocation-store.test.ts | 41 +++--- tests/db/sidecar-token-authenticator.test.ts | 40 +++--- tests/db/workflow-allocation-service.test.ts | 11 +- tests/db/workflow-lifecycle-service.test.ts | 34 +++-- tests/hub-api/find-routable-by-id.test.ts | 26 ++++ .../hub-api/resolve-routable-address.test.ts | 2 - tests/hub-api/trigger-delegator-race.test.ts | 6 +- tests/lib/seed.ts | 32 +++-- ...ederation-derived-grants-roundtrip.test.ts | 31 ++++- 36 files changed, 667 insertions(+), 290 deletions(-) diff --git a/apps/sidecar/src/index.ts b/apps/sidecar/src/index.ts index 08f0ef260..6d8e09c3b 100644 --- a/apps/sidecar/src/index.ts +++ b/apps/sidecar/src/index.ts @@ -547,9 +547,9 @@ const orchestrator = createSidecarOrchestrator({ // Report the sidecar's cached rotatable senders on every (re)connect so the // hub re-resolves each current key and re-pushes it, catching a rotation that // landed while the sidecar was disconnected. The cache owns the "only user - // senders rotate" filter (a run sender's key is the immutable - // workflow_run.public_key); the link stays source-opaque and reports whatever - // this returns. + // senders rotate" filter (a run sender's key reaches the cache with that + // run's mail, on its grants barrier); the link stays source-opaque and + // reports whatever this returns. getCachedSenderAddresses: () => senderKeyCache.rotatableAddresses(), // When the Hub's `welcome` routes a held incarnation again, re-drive any // workflow-run pack the disconnect cancelled. The Hub has routed the diff --git a/docs/INBOUND_MAIL_POLICY.md b/docs/INBOUND_MAIL_POLICY.md index 721d16d90..407050605 100644 --- a/docs/INBOUND_MAIL_POLICY.md +++ b/docs/INBOUND_MAIL_POLICY.md @@ -106,7 +106,8 @@ sender if its principal is gone, which deletes the file. A hub-side resolve that itself faults changes nothing, so the entry stays unreadable until a later reconnect. A run sender is repaired by the next grants barrier that co-delivers its key instead, because the hub never re-resolves a run sender on reconnect: a -run's key is immutable. +run's key changes only when its sidecar is replaced, and the run's next mail +carries the replacement's key. ## Admission weighs every finding, not one diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index af2bd6cb0..9fc08c301 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -47,8 +47,11 @@ export { type PrincipalKeyStore, } from "./principal-key-store"; export { + loadRunPrincipalActiveKey, recordRunPrincipalKey, retireRunPrincipalKey, + runPrincipalActiveKeySql, + runPrincipalHasActiveKey, } from "./run-principal-key"; export { createRunTokenStore, diff --git a/packages/db/src/run-principal-key.ts b/packages/db/src/run-principal-key.ts index f3780b9ff..9e757e67c 100644 --- a/packages/db/src/run-principal-key.ts +++ b/packages/db/src/run-principal-key.ts @@ -1,8 +1,90 @@ -import { and, eq } from "drizzle-orm"; +import { + and, + eq, + getTableName, + sql, + type AnyColumn, + type SQL, +} from "drizzle-orm"; import type { DBExecutor } from "./client"; import { recordPublicKey, retireActiveKey } from "./principal-key-store"; import { principal } from "./schema/principals"; +import { principalKey } from "./schema/principal-keys"; + +// A column of the enclosing query, named with its table. Drizzle renders a +// bare column reference unqualified in a single-table statement, and inside +// these subqueries an unqualified `tenant_id` or `id` would bind to the +// principal tables instead of the enclosing row. The qualifier is the real +// table name, so the columns must come from an unaliased table. +function enclosing(column: AnyColumn): SQL { + return sql`${sql.identifier(getTableName(column.table))}.${sql.identifier(column.name)}`; +} + +// The run principal and its active key, joined under aliases of their own so +// that no name in the subquery collides with the enclosing query. +function runPrincipalKeyJoin(tenantId: AnyColumn, runId: AnyColumn): SQL { + return sql`from "principal" "run_principal" inner join "principal_key" "run_principal_key" on "run_principal_key"."principal_id" = "run_principal"."id" and "run_principal_key"."status" = 'active' where "run_principal"."tenant_id" = ${enclosing(tenantId)} and "run_principal"."kind" = 'workflow' and "run_principal"."ref_id" = ${enclosing(runId)}`; +} + +/** + * A condition that holds when the run's principal has an active key: the + * key the sidecar running the deployment recorded when its initialization + * completed. It is the durable proof that the deployment's current allocation + * generation finished initializing. `tenantId` and `runId` are columns of the + * enclosing query. + */ +export function runPrincipalHasActiveKey( + tenantId: AnyColumn, + runId: AnyColumn, +): SQL { + return sql`exists (select 1 ${runPrincipalKeyJoin(tenantId, runId)})`; +} + +/** + * The active key of the run's principal as a scalar subquery, for a query + * that reads many runs. Null when the run's principal holds no active key. + * `tenantId` and `runId` are columns of the enclosing query. + */ +export function runPrincipalActiveKeySql( + tenantId: AnyColumn, + runId: AnyColumn, +): SQL { + return sql< + string | null + >`(select "run_principal_key"."public_key" ${runPrincipalKeyJoin(tenantId, runId)} limit 1)`; +} + +/** + * The active key of the run's principal, or null when the run has none: its + * deployment has not finished initializing, or its sidecar was replaced and + * the replacement has not recorded a key yet. + */ +export async function loadRunPrincipalActiveKey( + executor: DBExecutor, + tenantId: string, + runId: string, +): Promise { + const [row] = await executor + .select({ publicKey: principalKey.publicKey }) + .from(principal) + .innerJoin( + principalKey, + and( + eq(principalKey.principalId, principal.id), + eq(principalKey.status, "active"), + ), + ) + .where( + and( + eq(principal.tenantId, tenantId), + eq(principal.kind, "workflow"), + eq(principal.refId, runId), + ), + ) + .limit(1); + return row?.publicKey ?? null; +} /** * The run principal of a run: the `kind = 'workflow'` principal whose `refId` diff --git a/packages/db/src/sender-key-resolver.ts b/packages/db/src/sender-key-resolver.ts index 159cd624a..b55bf2caf 100644 --- a/packages/db/src/sender-key-resolver.ts +++ b/packages/db/src/sender-key-resolver.ts @@ -6,6 +6,10 @@ import { getLogger } from "@intx/log"; import type { DBExecutor } from "./client"; import type { PrincipalKeyStore } from "./principal-key-store"; +import { + loadRunPrincipalActiveKey, + runPrincipalHasActiveKey, +} from "./run-principal-key"; import { principal } from "./schema/principals"; import { tenant } from "./schema/tenants"; import { workflowDefinition } from "./schema/workflow-definitions"; @@ -31,9 +35,10 @@ const logger = getLogger(["db", "sender-key-resolver"]); * renders these ids as the `MailAcceptCoordinate` set the gate compares against * with no translation. * - * A run's `principalId` is nullable: a deployment anchor holds a signing key - * (recorded at deploy ack) before its first trigger links its run principal - * to the anchor, so a run that CAN sign mail may not yet carry a principal. + * A run's `principalId` is nullable: a deployment's run principal holds its + * signing key (recorded when its initialization completes) before the first + * trigger links that principal to the anchor, so a run that CAN sign mail may + * not yet carry a principal link. * The key still resolves for signature checks; `senderCoordinates` returns `null` for * such a sender, which the admission gate treats as the `unknown` outcome. A * user sender always resolves to a concrete principal, so its `principalId` is @@ -89,8 +94,9 @@ export function senderCoordinates( /** * Resolve the durable public key a signed mail sender must verify against, plus * the sender's admission coordinates, unioning the two existing durable-key - * sources: a run address (`run_@`) resolves to the run's - * `workflow_run.public_key` and its `(principal, asset, tenant)` ids; any + * sources: a run address (`run_@`) resolves to the active key of + * the run's principal, which the sidecar running the deployment minted, and + * its `(principal, asset, tenant)` ids; any * other address (`@`) is treated as a user sender and resolves * to that principal's hub-custodied key and its `(principal, tenant)` ids. The * sender is identified only from the passed address; the MIME `From` is never @@ -136,16 +142,19 @@ export async function resolveSenderKey( const { localPart, domain } = parsed; if (localPart.startsWith(RUN_PREFIX)) { - // A run sender resolves to the sidecar-minted key the hub recorded on the - // deployment anchor when the deployment's initialization completed. - // `workflow_run.address` is unique among the runs that set it, so there is - // at most one row. A row whose `public_key` is still null is a deployment - // whose initialization has not completed -- an expected state -- so it - // resolves to nothing rather than erroring, - // unlike the keyless-principal invariant break below. + // A run sender resolves to the sidecar-minted key the hub recorded as the + // active key of the run's principal when the deployment's initialization + // completed. `workflow_run.address` is unique among the runs that set it, + // so there is at most one row. A run whose principal holds no active key + // has not finished initializing, or its sidecar was replaced and the + // replacement has not recorded a key yet -- an expected state -- so it + // resolves to nothing rather than erroring, unlike the keyless-principal + // invariant break below. A retired key no longer resolves, so the hub + // vouches for no key for mail its replaced sidecar signed. The principal's + // status is not read: a terminal run's in-flight mail still verifies. const [row] = await db .select({ - publicKey: workflowRun.publicKey, + id: workflowRun.id, principalId: workflowRun.principalId, assetId: workflowDefinition.assetId, tenantId: workflowRun.tenantId, @@ -157,7 +166,9 @@ export async function resolveSenderKey( ) .where(eq(sql`lower(${workflowRun.address})`, normalized)) .limit(1); - if (row === undefined || row.publicKey === null) return null; + if (row === undefined) return null; + const publicKey = await loadRunPrincipalActiveKey(db, row.tenantId, row.id); + if (publicKey === null) return null; // `principalId` may be null: an acked anchor holds a signing key before its // first trigger links its run principal, so it CAN sign yet has no linked // principal. The key still resolves (the send path is unaffected); @@ -166,7 +177,7 @@ export async function resolveSenderKey( // has no asset. return { source: "run", - publicKey: row.publicKey, + publicKey, principalId: row.principalId, assetId: row.assetId, tenantId: row.tenantId, @@ -212,8 +223,9 @@ export type ResolvedSenderPrincipal = { * principal, tenant, and admission coordinates carry through. * * Returns `null` when there is no durable identity to bind: an unresolvable - * address, or a principal-less run (an acked anchor before its first trigger - * links its principal, for which `senderCoordinates` returns `null`). A `null` + * address, or a run with no principal link (an initialized anchor before its + * first trigger links its run principal, for which `senderCoordinates` + * returns `null`). A `null` * leaves the sender unadmitted rather than admitting a partial identity. The * strict resolver's throw on an ambiguous address or a keyless-principal * invariant break is preserved for the caller to catch, so a fault never @@ -375,7 +387,10 @@ export async function auditSenderKeys( // or was cancelled before ack (address set, key null, never signed). // Match "running" literally, NOT isLiveWorkflowRunStatus, which also // admits "deployed" and would re-open the pre-ack false positive. - or(isNotNull(workflowRun.publicKey), eq(workflowRun.status, "running")), + or( + runPrincipalHasActiveKey(workflowRun.tenantId, workflowRun.id), + eq(workflowRun.status, "running"), + ), ), ); for (const run of runs) { diff --git a/packages/db/src/sidecar-allocation-store.ts b/packages/db/src/sidecar-allocation-store.ts index 4cff78846..437d36ee7 100644 --- a/packages/db/src/sidecar-allocation-store.ts +++ b/packages/db/src/sidecar-allocation-store.ts @@ -22,8 +22,10 @@ import { import type { DB, DBExecutor } from "./client"; import { deactivateRunPrincipals } from "./principal-store"; import { + loadRunPrincipalActiveKey, recordRunPrincipalKey, retireRunPrincipalKey, + runPrincipalHasActiveKey, } from "./run-principal-key"; import { createWorkflowPendingProjectionStore } from "./workflow-pending-projection-store"; import { @@ -443,7 +445,7 @@ function reconnectDeadline( args: MarkSidecarConnectionLostArgs, beforeFirstDeploy: SQL, ) { - return sql`case when exists (select 1 from ${sidecar} where ${sidecar.id} = ${sidecarAllocation.sidecarId} and ${sidecar.reconnectWindowEndedAt} is not null) then ${deadlineTimestamp(args.now)} when exists (select 1 from ${workflowRun} where ${workflowRun.id} = ${sidecarAllocation.anchorRunId} and ${workflowRun.publicKey} is not null) then ${disconnectDeadline(args.now)} else ${beforeFirstDeploy} end`; + return sql`case when exists (select 1 from ${sidecar} where ${sidecar.id} = ${sidecarAllocation.sidecarId} and ${sidecar.reconnectWindowEndedAt} is not null) then ${deadlineTimestamp(args.now)} when ${runPrincipalHasActiveKey(sidecarAllocation.tenantId, sidecarAllocation.anchorRunId)} then ${disconnectDeadline(args.now)} else ${beforeFirstDeploy} end`; } function firstConnectDeadline(args: MarkSidecarConnectionLostArgs) { @@ -620,6 +622,7 @@ function buildSidecarAllocationStore( ): Promise { const [allocation] = await tx .select({ + tenantId: sidecarAllocation.tenantId, anchorRunId: sidecarAllocation.anchorRunId, initializationLeaseId: sidecarAllocation.initializationLeaseId, }) @@ -636,11 +639,13 @@ function buildSidecarAllocationStore( return false; // Read the key after acquiring the allocation lock, so a completion that // committed while we waited is visible even if its response was lost. - const [anchor] = await tx - .select({ publicKey: workflowRun.publicKey }) - .from(workflowRun) - .where(eq(workflowRun.id, allocation.anchorRunId)); - return anchor !== undefined && anchor.publicKey !== null; + return ( + (await loadRunPrincipalActiveKey( + tx, + allocation.tenantId, + allocation.anchorRunId, + )) !== null + ); } async function insertSidecarIdentity( @@ -840,7 +845,7 @@ function buildSidecarAllocationStore( // Preserve the disconnect window for an initialized deployment or // replacement cleanup, which has already cleared the anchor's key. // A first-connect deadline does not bound subsequent cleanup waiting. - connectDeadline: sql`case when ${sidecarAllocation.status} = 'replacing' or exists (select 1 from ${workflowRun} where ${workflowRun.id} = ${sidecarAllocation.anchorRunId} and ${workflowRun.publicKey} is not null) then ${sidecarAllocation.connectDeadline} else null end`, + connectDeadline: sql`case when ${sidecarAllocation.status} = 'replacing' or ${runPrincipalHasActiveKey(sidecarAllocation.tenantId, sidecarAllocation.anchorRunId)} then ${sidecarAllocation.connectDeadline} else null end`, // Placement and initialization retry diagnostics are not release causes. // Preserve a recorded workflow failure, or the reason supplied by release. failureCode: @@ -2273,7 +2278,7 @@ function buildSidecarAllocationStore( // completes; repeated hellos must not extend initialization forever. ...(connected ? { - connectDeadline: sql`case when ${sidecarAllocation.status} in ('releasing', 'replacing') or exists (select 1 from ${workflowRun} where ${workflowRun.id} = ${sidecarAllocation.anchorRunId} and ${workflowRun.publicKey} is not null) then null else ${sidecarAllocation.connectDeadline} end`, + connectDeadline: sql`case when ${sidecarAllocation.status} in ('releasing', 'replacing') or ${runPrincipalHasActiveKey(sidecarAllocation.tenantId, sidecarAllocation.anchorRunId)} then null else ${sidecarAllocation.connectDeadline} end`, } : {}), }) diff --git a/packages/hub-agent/src/sender-key-cache.test.ts b/packages/hub-agent/src/sender-key-cache.test.ts index 24c7b38b3..408aa9fb0 100644 --- a/packages/hub-agent/src/sender-key-cache.test.ts +++ b/packages/hub-agent/src/sender-key-cache.test.ts @@ -403,8 +403,8 @@ describe("SenderKeyCache", () => { await cache.put("run_job1@tenant.example", makeKey(9)); await cache.put("usr_bob@tenant.example", makeKey(13)); - // A run sender's key is the immutable workflow_run.public_key, so only the - // user senders are worth re-resolving on reconnect. + // A run sender's current key arrives with its next mail, so only the user + // senders are worth re-resolving on reconnect. expect([...cache.rotatableAddresses()].sort()).toEqual([ "usr_alice@tenant.example", "usr_bob@tenant.example", diff --git a/packages/hub-agent/src/sender-key-cache.ts b/packages/hub-agent/src/sender-key-cache.ts index aa4871168..b796247ef 100644 --- a/packages/hub-agent/src/sender-key-cache.ts +++ b/packages/hub-agent/src/sender-key-cache.ts @@ -145,10 +145,11 @@ export type SenderKeyCache = { * a user sender's hub principal key can rotate while the sidecar is * disconnected; and an address whose on-disk entry failed to load, because the * hub's re-push is what replaces the file the cache cannot read. Run addresses - * are excluded in both cases -- a run sender's key is the immutable - * `workflow_run.public_key`, the hub skips a reported run address whatever this - * returns, and a run sender's next grants barrier re-pushes its key anyway, - * which repairs a faulted file through `put`. This is a work-saving filter, not + * are excluded in both cases -- a run sender's key changes only when its + * sidecar is replaced, the hub skips a reported run address whatever this + * returns, and a run sender's next grants barrier re-pushes its current key + * with its mail, which repairs a faulted file through `put` and carries a + * replacement sidecar's key. This is a work-saving filter, not * a trust boundary -- the hub resolves every reported address on its own. */ rotatableAddresses(): string[]; diff --git a/packages/hub-api/src/routes/runs.ts b/packages/hub-api/src/routes/runs.ts index f577c7bb7..6cbc6b22c 100644 --- a/packages/hub-api/src/routes/runs.ts +++ b/packages/hub-api/src/routes/runs.ts @@ -16,6 +16,7 @@ import type { PrincipalKeyStore, RunTokenStore, } from "@intx/db"; +import { runPrincipalActiveKeySql } from "@intx/db"; import { getLogger } from "@intx/log"; import type { ConditionRegistry, GrantStore } from "@intx/types/authz"; import { extractPartByPath } from "@intx/mime"; @@ -142,6 +143,7 @@ const RUN_STATUSES_BY_VIEW_STATUS: Record< type RunListRow = { run: typeof workflowRun.$inferSelect; definitionName: string; + publicKey: string | null; }; // Shape a listed run into the routing record the run view expects, through the @@ -151,7 +153,7 @@ function runToRecord(row: RunListRow): RoutableRecord { if (row.run.address === null) { throw new Error(`run ${row.run.id} listed with a null address`); } - return runRowToRoutableRecord(row.run, row.run.address); + return runRowToRoutableRecord(row.run, row.run.address, row.publicKey); } export type CreateRunRoutesDeps = { @@ -332,6 +334,10 @@ export function createRunRoutes({ .select({ run: workflowRun, definitionName: workflowDefinition.name, + publicKey: runPrincipalActiveKeySql( + workflowRun.tenantId, + workflowRun.id, + ), }) .from(workflowRun) .innerJoin( diff --git a/packages/hub-api/src/routes/workflows.test.ts b/packages/hub-api/src/routes/workflows.test.ts index 57fe05333..4967ba075 100644 --- a/packages/hub-api/src/routes/workflows.test.ts +++ b/packages/hub-api/src/routes/workflows.test.ts @@ -332,8 +332,11 @@ type MockDBOpts = { bindings: { handle: string; credentialId: string; consumer: string }[]; hubConsumers?: string[]; }; - /** Null models an anchor whose initialization has not published a key. */ - publicKey?: string | null; + /** + * The active key of the anchor's run principal. Null models an anchor whose + * initialization has not recorded a key. + */ + runPrincipalKey?: string | null; initializationLeaseId?: string | null; /** * The deployment's recorded deployer. Absent means an active deployer; @@ -387,6 +390,19 @@ function createMockDB(opts: MockDBOpts) { .filter((record) => record.table === principalTable) .map((record) => record.values); } + // The initialization check reads the active key of the anchor's run + // principal. + if ( + table === principalTable && + fields !== undefined && + "publicKey" in fields + ) { + const key = + opts.runPrincipalKey !== undefined + ? opts.runPrincipalKey + : "recorded-key"; + return key === null ? [] : [{ publicKey: key }]; + } if (table === sidecarAllocationTable) { const status = opts.allocationStatus ?? "allocated"; return opts.allocationId === undefined @@ -460,6 +476,7 @@ function createMockDB(opts: MockDBOpts) { : [ { id: opts.deploymentRow.id, + tenantId: opts.deploymentRow.tenantId, allocationId: opts.allocationId ?? null, allocationStatus: opts.allocationId === undefined @@ -469,10 +486,6 @@ function createMockDB(opts: MockDBOpts) { opts.topLevelRunStatus ?? opts.anchorStatus ?? "running", cancellationRequestedAt: opts.cancellationRequestedAt ?? null, expiresAt: opts.expiresAt ?? null, - publicKey: - opts.publicKey !== undefined - ? opts.publicKey - : "recorded-key", credentialRefs: opts.credentialRefs !== undefined ? opts.credentialRefs @@ -3331,7 +3344,7 @@ describe("POST /workflows/:anchorRunId/mail", () => { deploymentRow, assetRow: workflowAssetRow, inserts, - publicKey: null, + runPrincipalKey: null, }, workflowJson: WORKFLOW_JSON_WITH_TOOLS, }); diff --git a/packages/hub-api/src/run-grant-materialization.test.ts b/packages/hub-api/src/run-grant-materialization.test.ts index 15325a584..40dc0a193 100644 --- a/packages/hub-api/src/run-grant-materialization.test.ts +++ b/packages/hub-api/src/run-grant-materialization.test.ts @@ -122,10 +122,11 @@ function mockDb(opts: { credentialRefs?: CredentialRefs; /** The anchor's legacy mail accept rule; absent means none. */ legacyMailAccept?: LegacyMailAcceptRule; - /** Null models an anchor whose initialization has not published a key. */ + /** The run principal's active key; null models a deployment whose + * initialization has not recorded one. */ publicKey?: string | null; /** - * Set while a deploy has cleared the key and left the previous refs. + * Set while a replacement has retired the key and left the previous refs. * Absent means the anchor has no allocation row. */ initializationLeaseId?: string | null; @@ -191,6 +192,18 @@ function mockDb(opts: { if (opts.snapshotReads) opts.snapshotReads.count += 1; return [{ snapshot: opts.grantSnapshot }]; } + // The run principal's active key: the deployment finished initializing + // when it has one. + if ( + table === principalTable && + joined && + fields !== undefined && + "publicKey" in fields + ) { + const publicKey = + opts.publicKey !== undefined ? opts.publicKey : "recorded-key"; + return publicKey === null ? [] : [{ publicKey }]; + } if (table === grantTable) { return opts.committedGrants === undefined ? insertedValues(grantTable).map((row) => ({ ...row, roleId: null })) @@ -233,10 +246,7 @@ function mockDb(opts: { expiresAt: null, cancellationRequestedAt: opts.lockedCancellationRequestedAt ?? null, - publicKey: - opts.publicKey !== undefined - ? opts.publicKey - : "recorded-key", + tenantId: opts.deploymentRow.tenantId, credentialRefs: opts.credentialRefs !== undefined ? opts.credentialRefs @@ -474,7 +484,7 @@ describe("grantRowsWithLockedCredentialResolution", () => { bindings: [], hubConsumers: ["action:a", "tool:@acme/t"], }, - publicKey: "recorded-key", + initialized: true, initializationLeaseId: null, }, args, @@ -497,7 +507,7 @@ describe("grantRowsWithLockedCredentialResolution", () => { expect( credentialResolutionUnrecorded({ credentialRefs: null, - publicKey: "recorded-key", + initialized: true, initializationLeaseId: null, }), ).toBe(false); @@ -527,7 +537,7 @@ describe("grantRowsWithLockedCredentialResolution", () => { [creator, stale], { credentialRefs: null, - publicKey: "recorded-key", + initialized: true, initializationLeaseId: null, }, args, @@ -551,7 +561,7 @@ describe("grantRowsWithLockedCredentialResolution", () => { expect( credentialResolutionUnrecorded({ credentialRefs: refs, - publicKey: null, + initialized: false, initializationLeaseId: null, }), ).toBe(false); @@ -559,7 +569,7 @@ describe("grantRowsWithLockedCredentialResolution", () => { [], { credentialRefs: refs, - publicKey: null, + initialized: false, initializationLeaseId: null, }, args, @@ -595,7 +605,7 @@ describe("grantRowsWithLockedCredentialResolution", () => { expect( credentialResolutionUnrecorded({ credentialRefs: refs, - publicKey: null, + initialized: false, initializationLeaseId: "lease-1", }), ).toBe(true); @@ -604,7 +614,7 @@ describe("grantRowsWithLockedCredentialResolution", () => { [], { credentialRefs: refs, - publicKey: null, + initialized: false, initializationLeaseId: "lease-1", }, args, diff --git a/packages/hub-api/src/run-grant-materialization.ts b/packages/hub-api/src/run-grant-materialization.ts index fa683fad9..216f7a06c 100644 --- a/packages/hub-api/src/run-grant-materialization.ts +++ b/packages/hub-api/src/run-grant-materialization.ts @@ -45,6 +45,7 @@ import type { import { createWorkflowRunStore, insertRunPrincipal, + loadRunPrincipalActiveKey, workflowRunExecutability, loadDeploymentGrantSnapshot, resolveSenderPrincipal, @@ -151,19 +152,20 @@ export function credentialUseBindings( /** * Credential resolution is unrecorded while initialization has not published - * it. That is an anchor with neither a public key nor credential refs, or an - * anchor whose allocation holds an initialization lease. The lease means a - * deploy has cleared the key and the refs still on the row are the previous - * attempt's. A key with null refs and no lease means initialization finished - * and there was no delivery. + * it. That is a deployment that has neither finished initializing (its run + * principal holds no key) nor recorded credential refs, or one whose + * allocation holds an initialization lease. The lease means a deploy has + * retired the key and the refs still on the row are the previous attempt's. + * A finished initialization with null refs and no lease means there was no + * delivery. */ export function credentialResolutionUnrecorded(resolution: { credentialRefs: WorkflowRunCredentialRefs | null; - publicKey: string | null; + initialized: boolean; initializationLeaseId: string | null; }): boolean { if (resolution.initializationLeaseId !== null) return true; - return resolution.credentialRefs === null && resolution.publicKey === null; + return resolution.credentialRefs === null && !resolution.initialized; } /** @@ -176,17 +178,18 @@ export async function lockAnchorCredentialResolution( anchorRunId: string, ): Promise<{ credentialRefs: WorkflowRunCredentialRefs | null; - publicKey: string | null; + initialized: boolean; initializationLeaseId: string | null; } | null> { // Lock the anchor only. The allocation lease is read afterwards and is not // locked: beginInitialization takes the allocation lock before the anchor, - // so locking it here would deadlock. The lease and the key are written in - // that same transaction, which cannot commit while this one holds the anchor. + // so locking it here would deadlock. The lease and the run principal's key + // are written in a transaction that holds the anchor lock, which cannot + // commit while this one holds the anchor. const [run] = await tx .select({ + tenantId: workflowRun.tenantId, credentialRefs: workflowRun.credentialRefs, - publicKey: workflowRun.publicKey, }) .from(workflowRun) .where( @@ -207,7 +210,8 @@ export async function lockAnchorCredentialResolution( .limit(1); return { credentialRefs: run.credentialRefs, - publicKey: run.publicKey, + initialized: + (await loadRunPrincipalActiveKey(tx, run.tenantId, anchorRunId)) !== null, initializationLeaseId: allocation === undefined ? null : allocation.initializationLeaseId, }; @@ -222,7 +226,7 @@ export function grantRowsWithLockedCredentialResolution( rows: readonly MaterializedGrantRow[], resolution: { credentialRefs: WorkflowRunCredentialRefs | null; - publicKey: string | null; + initialized: boolean; initializationLeaseId: string | null; }, args: { @@ -1610,7 +1614,7 @@ const correspondentLogger = getLogger(["hub-api", "correspondent-grant"]); * durable identity resolves through the same `resolveSenderPrincipal` the reply * resolves through. A local deployment that `resolveSenderPrincipal` cannot * resolve -- one that has not fired yet (the first fire links its principal to - * the anchor), or one whose signing key is cleared while its sidecar is + * the anchor), or one whose signing key is retired while its sidecar is * replaced -- resolves from its anchor row instead: the linked principal when * there is one, else `deriveRunPrincipalId(tenantId, runId)` with the runId * taken from the deployment address. Deploy creates the run principal with diff --git a/packages/hub-api/src/workflow-run-trigger.ts b/packages/hub-api/src/workflow-run-trigger.ts index fe1289f73..b3204c8fb 100644 --- a/packages/hub-api/src/workflow-run-trigger.ts +++ b/packages/hub-api/src/workflow-run-trigger.ts @@ -193,10 +193,11 @@ export function createWorkflowRunTrigger(deps: TriggerWorkflowRunDeps) { // and its From address (`${principal.refId}@${tenant.domain}`) must resolve // back to that same key when the signature is verified. Only a user // principal's address resolves to its hub principal key; a run principal's - // address resolves to the sidecar-minted `workflow_run.public_key`, and an - // agent principal's address has no resolution surface, so signing here as a - // non-user principal would guarantee a verification mismatch. The route - // layer only ever admits user principals to this path, so this is an + // address resolves to the sidecar-minted key the hub holds only the public + // half of, and an agent principal's address has no resolution surface, so + // signing here as a non-user principal would guarantee a verification + // mismatch. The route layer only ever admits user principals to this path, + // so this is an // invariant assertion, not a client-facing rejection: fail loud if a wiring // change ever lets another kind through, rather than silently emitting mail // that verification cannot resolve. Revisit this fence if the resolver diff --git a/packages/hub-sessions/src/hub-session-lookups.ts b/packages/hub-sessions/src/hub-session-lookups.ts index 5d1996c62..0a8ced5d6 100644 --- a/packages/hub-sessions/src/hub-session-lookups.ts +++ b/packages/hub-sessions/src/hub-session-lookups.ts @@ -10,6 +10,7 @@ import { eq, and, asc, inArray, isNotNull, isNull } from "drizzle-orm"; import type { DB } from "@intx/db"; import { createApprovalStore, + runPrincipalActiveKeySql, createSignalCorrelationStore, createWorkflowPendingProjectionStore, createWorkflowRunStore, @@ -636,7 +637,6 @@ export interface RoutableEndpoint { readonly id: string; readonly tenantId: string; readonly address: string; - readonly publicKey: string | null; /** * The endpoint's raw run status. Resolution is `endedAt`-filtered, so a * resolved endpoint is not necessarily live: a leaked run is deliberately @@ -668,7 +668,6 @@ export async function resolveRoutableAddress( .select({ id: workflowRun.id, tenantId: workflowRun.tenantId, - publicKey: workflowRun.publicKey, status: workflowRun.status, principalId: workflowRun.principalId, }) @@ -685,7 +684,6 @@ export async function resolveRoutableAddress( id: runRow.id, tenantId: runRow.tenantId, address, - publicKey: runRow.publicKey, status: runRow.status, sessionId: await resolveRunSessionId(db, runRow.principalId), }; @@ -795,17 +793,16 @@ export interface RoutableRecord { } /** - * Shape a run row and its already-resolved routing address into the run - * record. Callers decide whether the run resolves at all -- only a top-level - * run (`isTopLevelRun`) does -- and pass the address they have narrowed; this - * only maps the columns, including the run's `endedAt ?? createdAt` stand-in - * for the absent `updatedAt`. + * Shape a run row, its already-resolved routing address, and the active key + * of its run principal into the run record. Callers decide whether the run + * resolves at all -- only a top-level run (`isTopLevelRun`) does -- and pass + * the address they have narrowed; this only maps the columns, including the + * run's `endedAt ?? createdAt` stand-in for the absent `updatedAt`. */ export function runRowToRoutableRecord( run: { id: string; tenantId: string; - publicKey: string | null; status: string; createdAt: Date; endedAt: Date | null; @@ -818,6 +815,7 @@ export function runRowToRoutableRecord( infrastructureFailedAt: Date | null; }, address: string, + publicKey: string | null, ): RoutableRecord { // A failure the Hub decided waits on history still being reconciled, and is // dropped if that history ends the run first, so only a failed run whose @@ -828,7 +826,7 @@ export function runRowToRoutableRecord( id: run.id, tenantId: run.tenantId, address, - publicKey: run.publicKey, + publicKey, status: run.status, createdAt: run.createdAt, updatedAt: run.endedAt ?? run.createdAt, @@ -873,7 +871,7 @@ export async function findRoutableById( tenantId: workflowRun.tenantId, address: workflowRun.address, anchorRunId: workflowRun.anchorRunId, - publicKey: workflowRun.publicKey, + publicKey: runPrincipalActiveKeySql(workflowRun.tenantId, workflowRun.id), status: workflowRun.status, createdAt: workflowRun.createdAt, endedAt: workflowRun.endedAt, @@ -905,5 +903,6 @@ export async function findRoutableById( return runRowToRoutableRecord( { ...runRow, definitionId: runRow.definitionId }, runRow.address, + runRow.publicKey, ); } diff --git a/packages/hub-sessions/src/session-service.test.ts b/packages/hub-sessions/src/session-service.test.ts index ac8c9619c..084af4f69 100644 --- a/packages/hub-sessions/src/session-service.test.ts +++ b/packages/hub-sessions/src/session-service.test.ts @@ -509,6 +509,22 @@ describe("deployCodeSourcedWorkflow", () => { insert: inner.insert ?? (() => ({ values: async () => undefined })), }; } + // The read of the active key of an anchor's run principal: + // select().from(principal).innerJoin(principalKey).where().limit(). + function runPrincipalKeyRead(read: () => Promise) { + return () => ({ + from: () => ({ + innerJoin: () => ({ + where: () => ({ + limit: async () => { + const key = await read(); + return key === null ? [] : [{ publicKey: key }]; + }, + }), + }), + }), + }); + } // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- test stub: only the definition existence query, the anchor insert, and the success-path public-key update are exercised const CAPTURING_DB = { query: { @@ -842,16 +858,14 @@ describe("deployCodeSourcedWorkflow", () => { workflowDefinition: { findFirst: async () => ({ id: "definition" }), }, - workflowRun: { - findFirst: async () => { - if (holdRecoveryRead) { - recoveryRead.resolve(true); - await recoveryResponse.promise; - } - return { publicKey }; - }, - }, }, + select: runPrincipalKeyRead(async () => { + if (holdRecoveryRead) { + recoveryRead.resolve(true); + await recoveryResponse.promise; + } + return publicKey; + }), } as unknown as DB["db"]; const repoStore = createMockRepoStore(); repoStore.repoStore.resolveRef = async () => null; @@ -1013,11 +1027,7 @@ describe("deployCodeSourcedWorkflow", () => { const router = createMockRouter(); // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- DB stub answers only the anchor-key read recoverSenderDeploy performs const db = { - query: { - workflowRun: { - findFirst: async () => ({ publicKey: null }), - }, - }, + select: runPrincipalKeyRead(async () => null), } as unknown as DB["db"]; const allocation: SidecarAllocation = { id: TEST_TARGET.allocationId, @@ -1330,10 +1340,7 @@ describe("deployCodeSourcedWorkflow", () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- transaction double records the narrow initialization writes; real atomicity is covered by DB tests const db = { ...CAPTURING_DB, - query: { - ...CAPTURING_DB.query, - workflowRun: { findFirst: async () => ({ publicKey: null }) }, - }, + select: runPrincipalKeyRead(async () => null), transaction: async (run: (handle: typeof tx) => Promise) => { const result = await run(withRunPrincipalKeys(tx)); if (lostResponse === "reservation") diff --git a/packages/hub-sessions/src/session-service.ts b/packages/hub-sessions/src/session-service.ts index edb77523d..2ce1d2966 100644 --- a/packages/hub-sessions/src/session-service.ts +++ b/packages/hub-sessions/src/session-service.ts @@ -1,4 +1,4 @@ -import { and, eq, isNull } from "drizzle-orm"; +import { and, eq, not } from "drizzle-orm"; import { getLogger } from "@intx/log"; import { @@ -7,7 +7,9 @@ import { createWorkflowDefinitionArtifactStore, deactivateRunPrincipals, insertRunPrincipal, + loadRunPrincipalActiveKey, recordRunPrincipalKey, + runPrincipalHasActiveKey, resolveInferenceMaterials, type DB, type SidecarAllocation, @@ -943,7 +945,12 @@ export async function deployCodeSourcedWorkflow( eq(workflowRunTable.anchorRunId, args.anchorRunId), eq(workflowRunTable.tenantId, args.tenantId), eq(workflowRunTable.status, "deployed"), - isNull(workflowRunTable.publicKey), + not( + runPrincipalHasActiveKey( + workflowRunTable.tenantId, + workflowRunTable.id, + ), + ), ), ) .returning({ id: workflowRunTable.id }); @@ -982,7 +989,12 @@ export async function deployCodeSourcedWorkflow( eq(workflowRunTable.anchorRunId, args.anchorRunId), eq(workflowRunTable.tenantId, args.tenantId), eq(workflowRunTable.status, "deployed"), - isNull(workflowRunTable.publicKey), + not( + runPrincipalHasActiveKey( + workflowRunTable.tenantId, + workflowRunTable.id, + ), + ), ), ) .returning({ id: workflowRunTable.id }); @@ -1062,15 +1074,16 @@ export async function recoverSenderDeploy(args: { // Claiming the lease prevents the previous attempt from publishing. Its // marker and key now distinguish a committed initialization from failure, // even before the worker reconnects or the old response arrives. - const anchor = await args.db.query.workflowRun.findFirst({ - where: eq(workflowRunTable.id, allocation.anchorRunId), - columns: { publicKey: true }, - }); + const publicKey = await loadRunPrincipalActiveKey( + args.db, + allocation.tenantId, + allocation.anchorRunId, + ); reconciliation.signal.throwIfAborted(); args.sidecarRouter.noteSenderDeploySettled( { allocationId: allocation.id, generation: allocation.generation }, - anchor !== undefined && anchor.publicKey !== null - ? { recorded: anchor.publicKey } + publicKey !== null + ? { recorded: publicKey } : { failed: "Previous deployment initialization did not complete" }, ); } diff --git a/packages/hub-sessions/src/workflow-allocation-service.ts b/packages/hub-sessions/src/workflow-allocation-service.ts index 6c6aec1d4..4d0822e8f 100644 --- a/packages/hub-sessions/src/workflow-allocation-service.ts +++ b/packages/hub-sessions/src/workflow-allocation-service.ts @@ -6,6 +6,7 @@ import { createWorkflowProbeStore, createWorkflowRunLaunchSpecStore, insertRunPrincipal, + loadRunPrincipalActiveKey, resolveTenantSidecarCapabilityPolicies, resolveDeploymentLifecyclePolicy, canExecuteWorkflowRun, @@ -940,7 +941,6 @@ export function createWorkflowAllocationService({ const anchor = await db.query.workflowRun.findFirst({ where: eq(workflowRun.id, allocation.anchorRunId), columns: { - publicKey: true, definitionId: true, status: true, expiresAt: true, @@ -951,8 +951,16 @@ export function createWorkflowAllocationService({ throw new Error(`Allocation ${allocation.id} has no workflow anchor run`); } if (!canExecuteWorkflowRun(anchor, now())) return null; + // The run principal holds a key once this generation's first deploy + // completed. + const initialized = + (await loadRunPrincipalActiveKey( + db, + allocation.tenantId, + allocation.anchorRunId, + )) !== null; if (await allocationRouter.isAllocatedWorkflowActive(allocationTarget)) { - if (anchor.publicKey !== null) return null; + if (initialized) return null; throw new SessionLaunchError( new Error( `Allocation ${allocation.id} has an active workflow without a completed initialization key`, @@ -985,7 +993,7 @@ export function createWorkflowAllocationService({ } // The key commits only once the first deploy completed, so a connected // sidecar that did not report the deployment no longer holds it. - if (anchor.publicKey !== null) { + if (initialized) { throw new SidecarDeploymentMissingError( allocation.id, allocation.generation, diff --git a/packages/hub-sessions/src/workflow-lifecycle-service.ts b/packages/hub-sessions/src/workflow-lifecycle-service.ts index a920e4228..cb3952318 100644 --- a/packages/hub-sessions/src/workflow-lifecycle-service.ts +++ b/packages/hub-sessions/src/workflow-lifecycle-service.ts @@ -22,6 +22,7 @@ import { createWorkflowRunStore, createWorkflowRunDispatchStore, parseWorkflowRunRow, + runPrincipalHasActiveKey, workflowCapacityReleaseAt, SIDECAR_CLEANUP_RETRY_EXHAUSTED_FAILURE_CODE, SIDECAR_CLEANUP_DISCONNECT_TIMEOUT_FAILURE_CODE, @@ -744,7 +745,7 @@ export function createWorkflowLifecycleService({ eq(workflowRun.id, runId), eq(workflowRun.tenantId, tenantId), notInArray(workflowRun.status, [...liveWorkflowRunStatuses]), - isNotNull(workflowRun.publicKey), + runPrincipalHasActiveKey(workflowRun.tenantId, workflowRun.id), eq(sidecarAllocation.status, "allocated"), eq( sidecarAllocation.ensureAcceptedGeneration, @@ -1141,7 +1142,7 @@ export function createLifecycleSweepQueries( sidecarAllocation.anchorRunId, and( notInArray(workflowRun.status, [...liveWorkflowRunStatuses]), - isNotNull(workflowRun.publicKey), + runPrincipalHasActiveKey(workflowRun.tenantId, workflowRun.id), // Undated policies go through releaseDue, which derives the deadline. or( gt( diff --git a/packages/hub-sessions/src/ws/sidecar-handler.mail.test.ts b/packages/hub-sessions/src/ws/sidecar-handler.mail.test.ts index abc4a2810..a48af209a 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.mail.test.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.mail.test.ts @@ -764,9 +764,9 @@ describe("SidecarRouter allocation mail durability", () => { test("re-resolves and co-delivers a keyless run sender's key on reconnect replay", async () => { // A trigger tracked with NO captured senderIdentities and a RUN-address // sender is the gap: replaying it as-is would leave the recipient with no - // key and see the sender as unknown. A run's deployment key is immutable - // once acked, so re-resolving at replay is safe and co-delivers the key - // ahead of the mail. + // key and see the sender as unknown. A run's key changes only when its + // sidecar is replaced, so re-resolving at replay is safe and co-delivers + // the key ahead of the mail. const runSender = "run_peer@tenant.example"; const hexKey = "ee".repeat(32); const router = createAllocatedRouter({ diff --git a/packages/hub-sessions/src/ws/sidecar-handler.test.ts b/packages/hub-sessions/src/ws/sidecar-handler.test.ts index 1859c7764..88c437c23 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.test.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.test.ts @@ -468,7 +468,7 @@ describe("SidecarRouter allocation routing", () => { ); // The run address is filtered before resolution; only the user sender is // resolved and refreshed. The run address is never resolved and thus never - // evicted -- its key is the immutable workflow_run.public_key. + // evicted -- its key reaches a receiver with its next mail. expect(resolved).toEqual(["usr_bob@exclusive"]); const reconciled = ws.sent .map((raw): Record => JSON.parse(raw)) diff --git a/packages/hub-sessions/src/ws/sidecar-handler.ts b/packages/hub-sessions/src/ws/sidecar-handler.ts index 07c30f8bd..9cb77d7e5 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.ts @@ -1033,7 +1033,7 @@ export function createSidecarRouter( const pendingMailRetention = new Map>(); // sender address → pre-ack mail parked while the sender's public key is not // yet recorded. A run mints its signing keypair locally at the sidecar and can - // send mail before the hub has recorded its `public_key`; such mail resolves a + // send mail before the hub has recorded its public key; such mail resolves a // null sender key, so no key co-delivers and a strict recipient drops it as an // unknown sender. Each entry holds what a re-drive of `handleMailOutbound` // needs -- the raw message and its recipients -- plus a per-entry TTL timer. @@ -1350,9 +1350,13 @@ export function createSidecarRouter( // caches the key ahead of the mail; // - `undefined` when nothing must precede the mail. // - // The re-resolve is KIND-GATED to run-address senders only. A run's - // deployment key is immutable once acked, so the re-resolved key equals the - // signing-time key -- safe. A user (non-run) sender's key may have rotated + // The re-resolve is KIND-GATED to run-address senders only. A run's key + // changes only when its sidecar is replaced, so the re-resolve returns the + // signing-time key, except for mail the replaced sidecar signed and that is + // replayed after the replacement: that resolves to no key, or to the + // replacement's key, which fails the check. The hub never vouches for a + // retired key, so the mail is refused rather than admitted. A user (non-run) + // sender's key may have rotated // since it signed, so re-resolving would check the fixed signed bytes against // a newer key and turn a valid message into a false `invalid`; such a sender // lacking a captured key stays keyless (an honest `unknown`). An entry that @@ -1404,9 +1408,10 @@ export function createSidecarRouter( } // Best-effort re-resolve of a run sender's hub-held key at replay time. Only - // called for a run-address sender, whose deployment key is immutable once - // acked, so the current key equals the signing-time key. Returns null when no - // resolver is wired or the sender has no durable key. + // called for a run-address sender, whose key changes only when its sidecar is + // replaced; the hub vouches for no retired key, so the current key is the + // right one to co-deliver. Returns null when no resolver is wired or the + // sender has no durable key. // // This relies on `lookups.resolveSenderKey` being the BEST-EFFORT, // NEVER-THROWS resolver (the contract at sidecar-events.ts:274-279, wired to @@ -2954,9 +2959,10 @@ export function createSidecarRouter( void (async () => { for (const address of sendersToResync) { // The sidecar already reports only non-run senders, but do not trust - // the report: a run sender's key is the immutable - // workflow_run.public_key and is never refreshed or evicted, so skip - // it here too rather than couple correctness to the sidecar's filter. + // the report: a run sender's key reaches a receiver with that run's + // next mail, on its grants barrier, and is never refreshed or + // evicted here, so skip it here too rather than couple correctness + // to the sidecar's filter. if (isRunAddress(address)) continue; // Tri-state, deleted-vs-fault distinguished by the STRICT resolver: // - resolves to a key -> refresh the sidecar's cached key; @@ -4966,8 +4972,9 @@ export function createSidecarRouter( // // Resolve its key here, at dispatch (redelivery) time, from that persisted // address, to co-deliver on the run's grants barrier so the recipient - // caches the sender's current hub-held key. A run sender's deployment key - // is immutable once acked; a user sender's key rotating mid-flight would + // caches the sender's current hub-held key. A run sender's key changes + // only when its sidecar is replaced, and the hub vouches for no retired + // key; a user sender's key rotating mid-flight would // leave the fixed signed bytes checked against the new key, which the // recipient logs as unverifiable. Null when unresolvable (no resolver // wired, or the sender has no durable key). The lookup contract (see diff --git a/packages/hub-sessions/src/ws/sidecar-token-authenticator.test.ts b/packages/hub-sessions/src/ws/sidecar-token-authenticator.test.ts index e8e163167..04cabacf7 100644 --- a/packages/hub-sessions/src/ws/sidecar-token-authenticator.test.ts +++ b/packages/hub-sessions/src/ws/sidecar-token-authenticator.test.ts @@ -34,7 +34,8 @@ type MockDBOpts = { generation: number; } | null; anchorAddress?: string | null; - anchorPublicKey?: string | null; + // The active key of the anchor's run principal. + runPrincipalKey?: string | null; anchorStatus?: string; anchorCancellationRequestedAt?: Date; anchorFailureCode?: string; @@ -88,7 +89,6 @@ function createMockDB(opts: MockDBOpts): DB["db"] { ? undefined : { address: opts.anchorAddress, - publicKey: opts.anchorPublicKey ?? null, status: opts.anchorStatus ?? "running", expiresAt: null, endedAt: null, @@ -111,6 +111,20 @@ function createMockDB(opts: MockDBOpts): DB["db"] { ], }, }, + // The run principal key read: select().from().innerJoin().where().limit(). + select: () => ({ + from: () => ({ + innerJoin: () => ({ + where: () => ({ + limit: async () => + opts.runPrincipalKey === null || + opts.runPrincipalKey === undefined + ? [] + : [{ publicKey: opts.runPrincipalKey }], + }), + }), + }), + }), }; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- drizzle PgDatabase type cannot be structurally satisfied in tests return mock as unknown as DB["db"]; @@ -238,7 +252,7 @@ describe("createSidecarTokenAuthenticator", () => { } as const; const resolverWith = ( initializationLeaseId: string | null, - anchorPublicKey: string | null, + runPrincipalKey: string | null, anchorStatus = "running", ) => createSidecarCredentialResolver({ @@ -254,7 +268,7 @@ describe("createSidecarTokenAuthenticator", () => { initializationLeaseId, }, anchorAddress: "workflow@exclusive", - anchorPublicKey, + runPrincipalKey, anchorStatus, }), }); @@ -291,7 +305,7 @@ describe("createSidecarTokenAuthenticator", () => { } as const; const resolverWith = ( initializationLeaseId: string | null, - anchorPublicKey: string | null, + runPrincipalKey: string | null, anchorStatus: string, outcome: { cancelled?: boolean; @@ -312,7 +326,7 @@ describe("createSidecarTokenAuthenticator", () => { initializationLeaseId, }, anchorAddress: "workflow@exclusive", - anchorPublicKey, + runPrincipalKey, anchorStatus, ...(outcome.releaseAt !== undefined ? { anchorCapacityReleaseAt: outcome.releaseAt } diff --git a/packages/hub-sessions/src/ws/sidecar-token-authenticator.ts b/packages/hub-sessions/src/ws/sidecar-token-authenticator.ts index c26781ebe..f5243c852 100644 --- a/packages/hub-sessions/src/ws/sidecar-token-authenticator.ts +++ b/packages/hub-sessions/src/ws/sidecar-token-authenticator.ts @@ -1,6 +1,7 @@ import { and, eq, inArray, or } from "drizzle-orm"; import { sha256 } from "@intx/crypto"; import { + loadRunPrincipalActiveKey, workflowRunExecutability, workflowCapacityReleaseAt, type DB, @@ -199,7 +200,6 @@ export function createSidecarCredentialResolver({ const anchor = await db.query.workflowRun.findFirst({ columns: { address: true, - publicKey: true, status: true, expiresAt: true, cancellationRequestedAt: true, @@ -212,7 +212,13 @@ export function createSidecarCredentialResolver({ }); if (anchor?.address !== identity.workflowRunAddress) return false; if (!copyCheck) return true; - if (anchor.publicKey === null) return false; + const initialized = + (await loadRunPrincipalActiveKey( + db, + identity.tenantId, + identity.anchorRunId, + )) !== null; + if (!initialized) return false; // The Hub may have ended the run without the worker recording it, so the // run row, not the copy that reconnects, decides whether it is over. const ended = workflowRunExecutability(anchor) === "terminal"; diff --git a/tests/db/mail-accept-backfill.test.ts b/tests/db/mail-accept-backfill.test.ts index 61ae22782..1c0754aff 100644 --- a/tests/db/mail-accept-backfill.test.ts +++ b/tests/db/mail-accept-backfill.test.ts @@ -50,6 +50,7 @@ import { REPO_ROOT } from "@intx/test-harness/env"; import { seedAsset, seedPrincipal, + seedRunPrincipalKey, seedTenants, seedWorkflowRunLaunchSpec, } from "@intx/test-harness/seed"; @@ -250,7 +251,6 @@ describe.skipIf(!harnessDbEnvAvailable())( definitionId: MAIL_DEFINITION, address: WORKFLOW_ADDRESS, status: "running", - publicKey: "test-public-key", }, { id: SENDING_DEPLOYMENT, @@ -259,7 +259,6 @@ describe.skipIf(!harnessDbEnvAvailable())( definitionId: SENDING_DEFINITION, address: SENDING_ADDRESS, status: "deployed", - publicKey: "test-public-key", }, { id: MANUAL_DEPLOYMENT, @@ -268,7 +267,6 @@ describe.skipIf(!harnessDbEnvAvailable())( definitionId: MANUAL_DEFINITION, address: "run_manual@tenant.example", status: "deployed", - publicKey: "test-public-key", }, ]); for (const [anchorRunId, grantSnapshot] of [ @@ -284,6 +282,14 @@ describe.skipIf(!harnessDbEnvAvailable())( } }); + // Record the deployment's key, as its completed first deploy does, so its + // first mail can fire it. Only a test that fires the deployment calls + // this: the run principal it creates did not exist when the backfill + // shipped, and the backfill would give it an accept row. + async function initializeDeployment(): Promise { + await seedRunPrincipalKey(h.db, TENANT, DEPLOYMENT, "deployment-key"); + } + async function runBackfill(): Promise { for (const statement of BACKFILL_DATA_STATEMENTS) { await h.db.execute(sql.raw(statement)); @@ -347,7 +353,6 @@ describe.skipIf(!harnessDbEnvAvailable())( definitionId: args.definitionId ?? MAIL_DEFINITION, status: args.status, principalId: args.principalId, - publicKey: "test-public-key", }); } @@ -490,8 +495,13 @@ describe.skipIf(!harnessDbEnvAvailable())( definitionId: WAITING_DEFINITION, address: "run_waiting@tenant.example", status: "deployed", - publicKey: "test-public-key", }); + await seedRunPrincipalKey( + h.db, + TENANT, + WAITING_DEPLOYMENT, + "waiting-key", + ); await seedWorkflowRunLaunchSpec(h.db, { anchorRunId: WAITING_DEPLOYMENT, sourceAuthorityPrincipalId: CREATOR, @@ -611,6 +621,7 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("after the backfill a deployment that has not fired admits its first mail from its deployer", async () => { + await initializeDeployment(); const firstRun = ""; const before = await materialize(firstRun, DEPLOYER_SENDER); expect(before.outcome).toBe("rejected"); @@ -644,6 +655,7 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("a new deployment of the same definition keeps the older deployment's rule and refuses by default", async () => { + await initializeDeployment(); await runBackfill(); // A new deployment of the unchanged definition: a new anchor, a new @@ -657,8 +669,8 @@ describe.skipIf(!harnessDbEnvAvailable())( definitionId: MAIL_DEFINITION, address: "run_mail_new@tenant.example", status: "deployed", - publicKey: "test-public-key", }); + await seedRunPrincipalKey(h.db, TENANT, "run_mail_new", "mail-new-key"); await seedWorkflowRunLaunchSpec(h.db, { anchorRunId: "run_mail_new", sourceAuthorityPrincipalId: CREATOR, @@ -701,6 +713,12 @@ describe.skipIf(!harnessDbEnvAvailable())( .update(workflowRun) .set({ principalId: SENDER_PRINCIPAL, status: "running" }) .where(eq(workflowRun.id, SENDING_DEPLOYMENT)); + await seedRunPrincipalKey( + h.db, + TENANT, + SENDING_DEPLOYMENT, + "sending-key", + ); // A recipient that resolves to a durable principal. await seedPrincipal(h.db, { id: RECIPIENT_PRINCIPAL, @@ -716,8 +734,13 @@ describe.skipIf(!harnessDbEnvAvailable())( address: RECIPIENT_ADDRESS, status: "running", principalId: RECIPIENT_PRINCIPAL, - publicKey: "ab".repeat(32), }); + await seedRunPrincipalKey( + h.db, + TENANT, + RECIPIENT_RUN_ID, + "ab".repeat(32), + ); }); async function mintAndCount(): Promise { diff --git a/tests/db/mail-triggered-run-grants.test.ts b/tests/db/mail-triggered-run-grants.test.ts index 24b23c429..caabc0fde 100644 --- a/tests/db/mail-triggered-run-grants.test.ts +++ b/tests/db/mail-triggered-run-grants.test.ts @@ -13,7 +13,9 @@ import { createGrantStore, createPrincipalKeyStore, createRunTokenStore, + insertRunPrincipal, resolveSenderPrincipal, + retireRunPrincipalKey, } from "@intx/db"; import { grant, @@ -43,6 +45,7 @@ import { seedAsset, seedGrant, seedPrincipal, + seedRunPrincipalKey, seedTenants, seedWorkflowRunLaunchSpec, } from "@intx/test-harness/seed"; @@ -51,6 +54,8 @@ const TENANT = "tnt"; const ASSET = "ast"; const DEFINITION = "wfd_real"; const DEPLOYMENT = "run_real"; +// The run principal deploy creates for the deployment. +const DEPLOYMENT_PRINCIPAL = await deriveRunPrincipalId(TENANT, DEPLOYMENT); const WORKFLOW_ADDRESS = "run_real@tenant.example"; // The capability walk emits this grant for the workflow's mail trigger. A // snapshot without it belongs to a workflow that mail does not start. @@ -137,8 +142,8 @@ describe.skipIf(!harnessDbEnvAvailable())( definitionId: DEFINITION, address: WORKFLOW_ADDRESS, status: "running", - publicKey: "test-public-key", }); + await seedRunPrincipalKey(h.db, TENANT, DEPLOYMENT, "test-public-key"); // The creator holds the grant its creator-sourced requirement demands, // so the happy path resolves it. The rejection test overrides the // requirement to a resource the creator does NOT hold. @@ -202,8 +207,8 @@ describe.skipIf(!harnessDbEnvAvailable())( definitionId: DEFINITION, address: "run_second@tenant.example", status: "running", - publicKey: "test-public-key", }); + await seedRunPrincipalKey(h.db, TENANT, second, "second-public-key"); const writeSnapshot: GrantWalkSnapshot = { ...snapshot("secret:vault"), perStep: [ @@ -481,10 +486,7 @@ describe.skipIf(!harnessDbEnvAvailable())( test("does not commit grants before credential resolution is recorded", async () => { await seedFrozenSnapshot(snapshot("secret:vault")); - await h.db - .update(workflowRun) - .set({ publicKey: null }) - .where(eq(workflowRun.id, DEPLOYMENT)); + await retireRunPrincipalKey(h.db, TENANT, DEPLOYMENT); const runId = ""; const result = await materializeOnce(runId); @@ -769,20 +771,14 @@ describe.skipIf(!harnessDbEnvAvailable())( ], grantRequirements: [], }); - await seedPrincipal(h.db, { - id: "prn_http_run", - tenantId: TENANT, - kind: "workflow", - refId: DEPLOYMENT, - }); await h.db .update(workflowRun) - .set({ principalId: "prn_http_run" }) + .set({ principalId: DEPLOYMENT_PRINCIPAL }) .where(eq(workflowRun.id, DEPLOYMENT)); await seedGrant(h.db, { id: "grt_http_run_tool", tenantId: TENANT, - principalId: "prn_http_run", + principalId: DEPLOYMENT_PRINCIPAL, resource: "tool:mail_send", action: "invoke", effect: "allow", @@ -831,31 +827,32 @@ describe.skipIf(!harnessDbEnvAvailable())( if (result.outcome === "rejected") { expect(result.code).toBe("mail_admission_denied"); } - const runPrincipals = await h.db - .select({ id: principal.id }) - .from(principal) - .where(eq(principal.refId, DEPLOYMENT)); - expect(runPrincipals).toHaveLength(0); + // The refused fire leaves the principal deploy created unlinked and + // without grants. + const [anchor] = await h.db + .select({ principalId: workflowRun.principalId }) + .from(workflowRun) + .where(eq(workflowRun.id, DEPLOYMENT)); + expect(anchor?.principalId).toBeNull(); + const runGrants = await h.db + .select({ id: grant.id }) + .from(grant) + .where(eq(grant.principalId, DEPLOYMENT_PRINCIPAL)); + expect(runGrants).toHaveLength(0); }); test("admits mail to the run that its own trigger started", async () => { // The run's own trigger committed its principal and its accept rule: // here the caller that started it, the tenant's creator. await seedFrozenSnapshot(manualSnapshot); - await seedPrincipal(h.db, { - id: "prn_manual_run", - tenantId: TENANT, - kind: "workflow", - refId: DEPLOYMENT, - }); await h.db .update(workflowRun) - .set({ principalId: "prn_manual_run" }) + .set({ principalId: DEPLOYMENT_PRINCIPAL }) .where(eq(workflowRun.id, DEPLOYMENT)); await seedGrant(h.db, { id: "grt_manual_run_invoker", tenantId: TENANT, - principalId: "prn_manual_run", + principalId: DEPLOYMENT_PRINCIPAL, resource: mailAcceptResource("principal", CREATOR), action: "accept", effect: "allow", @@ -1270,10 +1267,7 @@ describe.skipIf(!harnessDbEnvAvailable())( principalId: "prn_committed_run", status: "running", }); - await tx - .update(workflowRun) - .set({ publicKey: null }) - .where(eq(workflowRun.id, DEPLOYMENT)); + await retireRunPrincipalKey(tx, TENANT, DEPLOYMENT); const [backend] = await tx.execute( sql`select pg_backend_pid() as pid`, ); @@ -1717,7 +1711,7 @@ describe.skipIf(!harnessDbEnvAvailable())( }); describe("createCorrespondentGrantMinter (send-seam)", () => { - const SENDER_PRINCIPAL = "prn_sender_run"; + const SENDER_PRINCIPAL = DEPLOYMENT_PRINCIPAL; const RECIPIENT_RUN_ID = "run_recipient"; const RECIPIENT_ADDRESS = "run_recipient@tenant.example"; const RECIPIENT_PRINCIPAL = "prn_recipient"; @@ -1749,12 +1743,6 @@ describe.skipIf(!harnessDbEnvAvailable())( status: "running" | "completed", overrides: { cancellationRequestedAt?: Date; expiresAt?: Date } = {}, ) { - await seedPrincipal(h.db, { - id: SENDER_PRINCIPAL, - tenantId: TENANT, - kind: "workflow", - refId: DEPLOYMENT, - }); await h.db .update(workflowRun) .set({ principalId: SENDER_PRINCIPAL, status, ...overrides }) @@ -1770,8 +1758,9 @@ describe.skipIf(!harnessDbEnvAvailable())( } // A recipient run that resolves to a durable principal: `resolveSenderKey` - // reads its `public_key`/`principal_id` off the run row, so the reply's - // coordinate the gate later matches is exactly this principal. + // reads its key off its run principal and its `principal_id` off the run + // row, so the reply's coordinate the gate later matches is exactly this + // principal. async function seedRecipientRun() { await seedPrincipal(h.db, { id: RECIPIENT_PRINCIPAL, @@ -1787,8 +1776,13 @@ describe.skipIf(!harnessDbEnvAvailable())( address: RECIPIENT_ADDRESS, status: "running", principalId: RECIPIENT_PRINCIPAL, - publicKey: "ab".repeat(32), }); + await seedRunPrincipalKey( + h.db, + TENANT, + RECIPIENT_RUN_ID, + "ab".repeat(32), + ); } function mint() { @@ -1998,15 +1992,12 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(reply.outcome).toBe("materialized"); }); - test("mints for a fired recipient whose signing key is cleared", async () => { + test("mints for a fired recipient whose signing key is retired", async () => { await seedFrozenSnapshot(correspondentSnapshot()); await makeSenderRun("running"); await seedRecipientRun(); - // A sidecar replacement clears the recorded key but keeps the principal. - await h.db - .update(workflowRun) - .set({ publicKey: null }) - .where(eq(workflowRun.id, RECIPIENT_RUN_ID)); + // A sidecar replacement retires the recorded key but keeps the principal. + await retireRunPrincipalKey(h.db, TENANT, RECIPIENT_RUN_ID); await mint()({ senderAddress: WORKFLOW_ADDRESS, @@ -2054,8 +2045,13 @@ describe.skipIf(!harnessDbEnvAvailable())( address: RECIPIENT2_ADDRESS, status: "running", principalId: RECIPIENT2_PRINCIPAL, - publicKey: "cd".repeat(32), }); + await seedRunPrincipalKey( + h.db, + TENANT, + RECIPIENT2_RUN_ID, + "cd".repeat(32), + ); await mint()({ senderAddress: WORKFLOW_ADDRESS, @@ -2191,15 +2187,16 @@ describe.skipIf(!harnessDbEnvAvailable())( const FRESH_RUN_ID = "run_fresh"; const FRESH_ADDRESS = "run_fresh@tenant.example"; - // A deployed, acked anchor with no principal, which the sending run - // deployed: its first fire mints the principal, and its own snapshot - // admits mail from its deployer, the invoker. - async function seedFreshRecipient( - overrides: { - cancellationRequestedAt?: Date; - publicKey?: string | null; - } = {}, - ) { + // A deployed, acked anchor that the sending run deployed: its run + // principal exists from deploy, its first fire links it, and its own + // snapshot admits mail from its deployer, the invoker. + async function seedFreshRecipient({ + publicKey = "ef".repeat(32), + ...overrides + }: { + cancellationRequestedAt?: Date; + publicKey?: string | null; + } = {}) { await h.db.insert(workflowDefinition).values({ id: FRESH_DEFINITION, tenantId: TENANT, @@ -2213,9 +2210,18 @@ describe.skipIf(!harnessDbEnvAvailable())( definitionId: FRESH_DEFINITION, address: FRESH_ADDRESS, status: "deployed", - publicKey: "ef".repeat(32), ...overrides, }); + // Deploy creates the run principal; the key arrives with the ack. + await insertRunPrincipal(h.db, { + id: await deriveRunPrincipalId(TENANT, FRESH_RUN_ID), + tenantId: TENANT, + runId: FRESH_RUN_ID, + createdAt: new Date(), + }); + if (publicKey !== null) { + await seedRunPrincipalKey(h.db, TENANT, FRESH_RUN_ID, publicKey); + } await seedWorkflowRunLaunchSpec(h.db, { anchorRunId: FRESH_RUN_ID, sourceAuthorityPrincipalId: SENDER_PRINCIPAL, diff --git a/tests/db/run-principal-key.test.ts b/tests/db/run-principal-key.test.ts index 3c9150b5b..b1cc9c157 100644 --- a/tests/db/run-principal-key.test.ts +++ b/tests/db/run-principal-key.test.ts @@ -8,14 +8,24 @@ import { } from "bun:test"; import { asc, eq } from "drizzle-orm"; -import { recordRunPrincipalKey, retireRunPrincipalKey } from "@intx/db"; -import { principalKey } from "@intx/db/schema"; +import { + recordRunPrincipalKey, + retireRunPrincipalKey, + runPrincipalActiveKeySql, + runPrincipalHasActiveKey, +} from "@intx/db"; +import { principalKey, workflowDefinition, workflowRun } from "@intx/db/schema"; import { createTestDb, harnessDbEnvAvailable, type TestDb, } from "@intx/test-harness/db-harness"; -import { seedPrincipal, seedTenants } from "@intx/test-harness/seed"; +import { + seedPrincipal, + seedRunPrincipalKey, + seedTenants, + seedWorkflowRun, +} from "@intx/test-harness/seed"; const TENANT = "tnt_rpk"; const RUN_ID = "run_rpk"; @@ -83,6 +93,54 @@ describe.skipIf(!harnessDbEnvAvailable())( ]); }); + test("a query over many runs reads each run's own active key", async () => { + const OTHER_TENANT = "tnt_rpk_other"; + await seedTenants(h.db, [{ id: OTHER_TENANT }]); + for (const id of [RUN_ID, "run_retired", "run_unkeyed", "run_foreign"]) { + await seedWorkflowRun(h.db, { id, tenantId: TENANT, anchorRunId: id }); + } + await recordRunPrincipalKey(h.db, TENANT, RUN_ID, FIRST_KEY); + await seedRunPrincipalKey(h.db, TENANT, "run_retired", REPLACEMENT_KEY); + await retireRunPrincipalKey(h.db, TENANT, "run_retired"); + // A run principal with the same run id in another tenant is not the + // run's principal. + await seedRunPrincipalKey( + h.db, + OTHER_TENANT, + "run_foreign", + "cc".repeat(32), + ); + + // Joined, as the run list reads runs, so the subqueries' outer columns + // are qualified against more than one table in scope. + const rows = await h.db + .select({ + id: workflowRun.id, + publicKey: runPrincipalActiveKeySql( + workflowRun.tenantId, + workflowRun.id, + ), + initialized: runPrincipalHasActiveKey( + workflowRun.tenantId, + workflowRun.id, + ), + }) + .from(workflowRun) + .innerJoin( + workflowDefinition, + eq(workflowDefinition.id, workflowRun.definitionId), + ) + .where(eq(workflowRun.tenantId, TENANT)) + .orderBy(asc(workflowRun.id)); + + expect(rows).toEqual([ + { id: "run_foreign", publicKey: null, initialized: false }, + { id: "run_retired", publicKey: null, initialized: false }, + { id: RUN_ID, publicKey: FIRST_KEY, initialized: true }, + { id: "run_unkeyed", publicKey: null, initialized: false }, + ]); + }); + test("a run with no run principal cannot record a key and has none to retire", async () => { await expect( recordRunPrincipalKey(h.db, TENANT, "run_missing", FIRST_KEY), diff --git a/tests/db/sender-key-resolver.test.ts b/tests/db/sender-key-resolver.test.ts index ba1a5f518..96c4d7eeb 100644 --- a/tests/db/sender-key-resolver.test.ts +++ b/tests/db/sender-key-resolver.test.ts @@ -10,8 +10,10 @@ import { import { auditSenderKeys, createPrincipalKeyStore, + recordRunPrincipalKey, resolveSenderKey, resolveFrameSenderKey, + retireRunPrincipalKey, senderCoordinates, type PrincipalKeyStore, } from "@intx/db"; @@ -189,8 +191,8 @@ describe.skipIf(!harnessDbEnvAvailable())("resolveSenderKey (real DB)", () => { test("returns null for a run whose deploy has not been acked yet", async () => { await seedTenant("tnt_preack", "preack.localhost"); const address = "run_preack01@preack.localhost"; - // A deployed-but-not-yet-acked anchor carries an address with a null - // public_key; that is the expected pre-ack state, not an error. + // A deployed-but-not-yet-acked anchor carries an address, and its run + // principal holds no key; that is the expected pre-ack state, not an error. await seedWorkflowRun(h.db, { id: "run_preack01", tenantId: "tnt_preack", @@ -202,6 +204,29 @@ describe.skipIf(!harnessDbEnvAvailable())("resolveSenderKey (real DB)", () => { expect(await resolveSenderKey(h.db, store, address)).toBeNull(); }); + test("resolves a run to the key its replacement sidecar recorded, not the retired one", async () => { + await seedTenant("tnt_rotate", "rotate.localhost"); + const address = "run_rotate01@rotate.localhost"; + await seedWorkflowRun(h.db, { + id: "run_rotate01", + tenantId: "tnt_rotate", + address, + publicKey: RUN_PUBLIC_KEY, + status: "running", + }); + + // Replacing the sidecar retires the key the old sidecar held. + await retireRunPrincipalKey(h.db, "tnt_rotate", "run_rotate01"); + expect(await resolveSenderKey(h.db, store, address)).toBeNull(); + + const rotated = "cd".repeat(32); + await recordRunPrincipalKey(h.db, "tnt_rotate", "run_rotate01", rotated); + expect(await resolveSenderKey(h.db, store, address)).toMatchObject({ + source: "run", + publicKey: rotated, + }); + }); + test("returns null for an unknown run address", async () => { expect( await resolveSenderKey(h.db, store, "run_missing@nowhere.localhost"), diff --git a/tests/db/sidecar-allocation-store.test.ts b/tests/db/sidecar-allocation-store.test.ts index 3402388fe..118800918 100644 --- a/tests/db/sidecar-allocation-store.test.ts +++ b/tests/db/sidecar-allocation-store.test.ts @@ -75,6 +75,16 @@ describe.skipIf(!harnessDbEnvAvailable())( await h.close(); }); + // Record the key a completed initialization leaves: on the anchor and as + // the active key of its run principal. + async function recordAnchorKey(publicKey: string) { + await h.db + .update(workflowRun) + .set({ publicKey }) + .where(eq(workflowRun.id, ANCHOR_RUN_ID)); + await recordPublicKey(h.db, ANCHOR_RUN_PRINCIPAL_ID, publicKey); + } + // The keys of the anchor's run principal, oldest first. async function runPrincipalKeys() { return h.db @@ -670,10 +680,7 @@ describe.skipIf(!harnessDbEnvAvailable())( const id = "alloc-cleanup-disconnected"; const store = await bindFirstGeneration(id, ANCHOR_RUN_ID); await store.markAllocated({ allocationId: id, generation: 1 }); - await h.db - .update(workflowRun) - .set({ publicKey: "committed-key" }) - .where(eq(workflowRun.id, ANCHOR_RUN_ID)); + await recordAnchorKey("committed-key"); const at = new Date(); const disconnected = await store.markConnectionLost({ allocationId: id, @@ -1687,10 +1694,7 @@ describe.skipIf(!harnessDbEnvAvailable())( }), ).toMatchObject({ status: "running" }); - await h.db - .update(workflowRun) - .set({ publicKey: "restored-key" }) - .where(eq(workflowRun.id, ANCHOR_RUN_ID)); + await recordAnchorKey("restored-key"); restored = false; const healthy = createSidecarAllocationReconciler(options); await healthy.initialize(); @@ -4407,7 +4411,11 @@ describe.skipIf(!harnessDbEnvAvailable())( const locked = Promise.withResolvers(); const unlock = Promise.withResolvers(); const blocker = h.db.transaction(async (tx) => { - await tx.execute(sql`lock table workflow_run in access exclusive mode`); + // Recovery reads the run principal's key, so blocking that table + // holds each recovery read open. + await tx.execute( + sql`lock table principal_key in access exclusive mode`, + ); locked.resolve(true); await unlock.promise; }); @@ -5248,10 +5256,7 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(deploying?.connectDeadline).toEqual(firstConnectDeadline); expect(deploying?.nextAttemptAt).toEqual(firstConnectDeadline); - await h.db - .update(workflowRun) - .set({ publicKey: "committed-key" }) - .where(eq(workflowRun.id, ANCHOR_RUN_ID)); + await recordAnchorKey("committed-key"); const deadline = new Date(lostAt.getTime() + 120_000); const disconnected = await store.markConnectionLost({ allocationId: "alloc-reconnect", @@ -5329,10 +5334,7 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(kept?.connectDeadline).toEqual(fresh); // Once its first deploy completed, it gets its whole disconnect limit. - await h.db - .update(workflowRun) - .set({ publicKey: "committed-key" }) - .where(eq(workflowRun.id, ANCHOR_RUN_ID)); + await recordAnchorKey("committed-key"); const running = await store.scheduleReconnectAfterHubStart({ ...target, now: startedAt, @@ -5376,10 +5378,7 @@ describe.skipIf(!harnessDbEnvAvailable())( allocationId: "alloc-repair", generation: 1, }); - await h.db - .update(workflowRun) - .set({ publicKey: "committed-key" }) - .where(eq(workflowRun.id, ANCHOR_RUN_ID)); + await recordAnchorKey("committed-key"); const repairedAt = new Date(); const deadline = new Date(repairedAt.getTime() + 120_000); diff --git a/tests/db/sidecar-token-authenticator.test.ts b/tests/db/sidecar-token-authenticator.test.ts index c7a95c207..dd0fd0b77 100644 --- a/tests/db/sidecar-token-authenticator.test.ts +++ b/tests/db/sidecar-token-authenticator.test.ts @@ -9,6 +9,7 @@ import { import { sha256 } from "@intx/crypto"; import { + retireRunPrincipalKey, SIDECAR_CLEANUP_RETRY_EXHAUSTED_FAILURE_CODE, SIDECAR_CLEANUP_DISCONNECT_TIMEOUT_FAILURE_CODE, } from "@intx/db"; @@ -30,6 +31,7 @@ import { } from "@intx/test-harness/db-harness"; import { seedAsset, + seedRunPrincipalKey, seedTenants, seedWorkflowRun, } from "@intx/test-harness/seed"; @@ -64,6 +66,17 @@ describe.skipIf(!harnessDbEnvAvailable())( await seedTenants(h.db, [{ id: TENANT_ID }]); }); + // Record the key the deployment's sidecar minted, as a completed first + // deploy does. A key is unique, so each deployment gets its own. + async function recordAnchorKey(anchorRunId: string): Promise { + await seedRunPrincipalKey( + h.db, + TENANT_ID, + anchorRunId, + `key-${anchorRunId}`, + ); + } + // Seed a sidecar identity the same way provisioning does: store the // SHA-256 digest of the token as the `bytea` hash. `url` is not read // by the authenticator; a placeholder satisfies its NOT NULL. @@ -423,10 +436,7 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(await resolver.isCurrent(binding, "routing")).toBe(true); expect(await resolver.isCurrent(binding, "reclaim")).toBe(false); - await h.db - .update(workflowRun) - .set({ publicKey: "public-key" }) - .where(eq(workflowRun.id, binding.anchorRunId)); + await recordAnchorKey(binding.anchorRunId); expect(await resolver.isCurrent(binding, "reclaim")).toBe(true); await h.db @@ -441,10 +451,7 @@ describe.skipIf(!harnessDbEnvAvailable())( const resolver = createSidecarCredentialResolver({ db: h.db }); const allowed = () => resolver.isCurrent(binding, "retention-transition"); expect(await allowed()).toBe(false); - await h.db - .update(workflowRun) - .set({ publicKey: "public-key" }) - .where(eq(workflowRun.id, binding.anchorRunId)); + await recordAnchorKey(binding.anchorRunId); expect(await allowed()).toBe(false); await h.db .update(workflowRun) @@ -481,19 +488,19 @@ describe.skipIf(!harnessDbEnvAvailable())( .update(sidecarAllocation) .set({ initializationLeaseId: null }) .where(eq(sidecarAllocation.id, binding.allocationId)); - await h.db - .update(workflowRun) - .set({ publicKey: null }) - .where(eq(workflowRun.id, binding.anchorRunId)); + await h.db.transaction((tx) => + retireRunPrincipalKey(tx, TENANT_ID, binding.anchorRunId), + ); expect(await allowed()).toBe(false); }); test("retains instead of reclaiming a copy of a run that ended on its own", async () => { const binding = await seedSidecar({ id: "sc-ended", token: "t" }); const resolver = createSidecarCredentialResolver({ db: h.db }); + await recordAnchorKey(binding.anchorRunId); await h.db .update(workflowRun) - .set({ publicKey: "public-key", status: "failed", endedAt: new Date() }) + .set({ status: "failed", endedAt: new Date() }) .where(eq(workflowRun.id, binding.anchorRunId)); expect(await resolver.isCurrent(binding, "reclaim")).toBe(false); // Its copy stays, unrouted, until the Hub releases the deployment. @@ -513,10 +520,10 @@ describe.skipIf(!harnessDbEnvAvailable())( db: h.db, now: () => now, }); + await recordAnchorKey(binding.anchorRunId); await h.db .update(workflowRun) .set({ - publicKey: "public-key", status: "completed", endedAt, lifecyclePolicy: { capacityRetention: { completed: "1m" } }, @@ -539,9 +546,10 @@ describe.skipIf(!harnessDbEnvAvailable())( test("retains a Hub-ended copy only once its run is terminal", async () => { const binding = await seedSidecar({ id: "sc-cancelled", token: "t" }); const resolver = createSidecarCredentialResolver({ db: h.db }); + await recordAnchorKey(binding.anchorRunId); await h.db .update(workflowRun) - .set({ publicKey: "public-key", cancellationRequestedAt: new Date() }) + .set({ cancellationRequestedAt: new Date() }) .where(eq(workflowRun.id, binding.anchorRunId)); // A run still cancelling keeps its copy, which records the cancel. expect(await resolver.isCurrent(binding, "reclaim")).toBe(true); @@ -555,10 +563,10 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(await resolver.isCurrent(binding, "retention")).toBe(true); const failed = await seedSidecar({ id: "sc-failed", token: "t2" }); + await recordAnchorKey(failed.anchorRunId); await h.db .update(workflowRun) .set({ - publicKey: "public-key", failureCode: "sidecar_deployment_stopped", infrastructureFailedAt: new Date(), }) diff --git a/tests/db/workflow-allocation-service.test.ts b/tests/db/workflow-allocation-service.test.ts index dda299cef..d3d99907f 100644 --- a/tests/db/workflow-allocation-service.test.ts +++ b/tests/db/workflow-allocation-service.test.ts @@ -69,6 +69,7 @@ import { seedModelProvider, seedPrincipal, seedProvider, + seedRunPrincipalKey, seedTenants, seedWorkflowRun, } from "@intx/test-harness/seed"; @@ -1192,10 +1193,12 @@ describe.skipIf(!harnessDbEnvAvailable())( h.db, ).findByAnchorRunId(prepared.anchorRunId); if (allocation === null) throw new Error("expected adopted allocation"); - await h.db - .update(workflowRun) - .set({ publicKey: "committed-key" }) - .where(eq(workflowRun.id, prepared.anchorRunId)); + await seedRunPrincipalKey( + h.db, + TENANT_ID, + prepared.anchorRunId, + "committed-key", + ); const reconciliation = { signal: new AbortController().signal, leaseId: "initialization-test", diff --git a/tests/db/workflow-lifecycle-service.test.ts b/tests/db/workflow-lifecycle-service.test.ts index fe2acdf82..5394cfa5f 100644 --- a/tests/db/workflow-lifecycle-service.test.ts +++ b/tests/db/workflow-lifecycle-service.test.ts @@ -17,6 +17,7 @@ import { } from "@intx/test-harness/db-harness"; import { seedPrincipal, + seedRunPrincipalKey, seedTenants, seedRunToken, } from "@intx/test-harness/seed"; @@ -200,6 +201,12 @@ describe.skipIf(!harnessDbEnvAvailable())( events = [{ seq: 1, type, body: { type, at: current.toISOString() } }]; } + // Record the key the deployment's sidecar minted, as a completed first + // deploy does. A key is unique, so each deployment gets its own. + async function recordRunKey(id: string): Promise { + await seedRunPrincipalKey(h.db, tenantId, id, `key-${id}`); + } + // A receive whose run-status projection failed leaves this row behind. async function leavePending( anchorRunId = runId, @@ -525,13 +532,13 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("a terminal copy without a retention deadline transfers once and keeps its allocation", async () => { + await recordRunKey(runId); await h.db .update(workflowRun) .set({ status: "completed", endedAt: current, lifecyclePolicy: null, - publicKey: "ab".repeat(32), }) .where(eq(workflowRun.id, runId)); let kept = false; @@ -589,10 +596,10 @@ describe.skipIf(!harnessDbEnvAvailable())( definitionId: "definition", status: "completed", endedAt: current, - publicKey: "ab".repeat(32), address: `${id}@example.test`, createdAt, }); + await recordRunKey(id); await h.db.insert(sidecarAllocation).values({ ...template, id: retainedAllocationId, @@ -698,13 +705,13 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("a kept copy remains sweep work until its history handoff completes", async () => { + await recordRunKey(runId); await h.db .update(workflowRun) .set({ status: "completed", endedAt: current, lifecyclePolicy: null, - publicKey: "ab".repeat(32), }) .where(eq(workflowRun.id, runId)); let recovered = false; @@ -739,13 +746,13 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("unreceived retained history does not extend the release deadline", async () => { + await recordRunKey(runId); await h.db .update(workflowRun) .set({ status: "failed", endedAt: current, lifecyclePolicy: { capacityRetention: { failed: "1m" } }, - publicKey: "ab".repeat(32), }) .where(eq(workflowRun.id, runId)); let requests = 0; @@ -775,12 +782,12 @@ describe.skipIf(!harnessDbEnvAvailable())( test.each(["completed", "failed", "cancelled"] as const)( "retention overflow releases a %s copy without changing its outcome", async (status) => { + await recordRunKey(runId); await h.db .update(workflowRun) .set({ status, endedAt: current, - publicKey: "ab".repeat(32), failureCode: status === "failed" ? "original_failure" : null, lifecyclePolicy: { capacityRetention: { [status]: "1d" } }, }) @@ -817,6 +824,7 @@ describe.skipIf(!harnessDbEnvAvailable())( test.each(["saved", "derived"] as const)( "a %s release deadline that is already due never reserves a kept slot", async (deadline) => { + await recordRunKey(runId); await h.db .update(workflowRun) .set({ @@ -824,7 +832,6 @@ describe.skipIf(!harnessDbEnvAvailable())( endedAt: new Date( current.getTime() - (deadline === "saved" ? 30_000 : 60_000), ), - publicKey: "ab".repeat(32), lifecyclePolicy: { capacityRetention: { completed: "1m" } }, capacityReleaseAt: deadline === "saved" ? current : null, }) @@ -855,9 +862,10 @@ describe.skipIf(!harnessDbEnvAvailable())( ); test("retention stops a terminal copy before history recovery but overflow cleanup waits for accepted history", async () => { + await recordRunKey(runId); await h.db .update(workflowRun) - .set({ status: "failed", endedAt: current, publicKey: "ab".repeat(32) }) + .set({ status: "failed", endedAt: current }) .where(eq(workflowRun.id, runId)); await leavePending(); let refused = false; @@ -911,12 +919,12 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("refused retention still expires at its policy deadline when accepted history cannot be recovered", async () => { + await recordRunKey(runId); await h.db .update(workflowRun) .set({ status: "failed", endedAt: current, - publicKey: "ab".repeat(32), lifecyclePolicy: { capacityRetention: { failed: "1m" } }, }) .where(eq(workflowRun.id, runId)); @@ -954,13 +962,13 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("an unconfirmed retain keeps its reservation until retained inventory confirms it without another request", async () => { + await recordRunKey(runId); await h.db .update(workflowRun) .set({ status: "completed", endedAt: current, lifecyclePolicy: null, - publicKey: "ab".repeat(32), }) .where(eq(workflowRun.id, runId)); // This copy occupies the last active slot until its retained proof arrives. @@ -1056,12 +1064,12 @@ describe.skipIf(!harnessDbEnvAvailable())( test.each(["none", "other status", "future"] as const)( "a confirmed kept copy with %s retention work is not revisited", async (policy) => { + await recordRunKey(runId); await h.db .update(workflowRun) .set({ status: "completed", endedAt: current, - publicKey: "ab".repeat(32), lifecyclePolicy: policy === "none" ? null @@ -1084,12 +1092,12 @@ describe.skipIf(!harnessDbEnvAvailable())( test("kept copies still recover accepted history and release when their deadline arrives", async () => { const deadline = new Date(current.getTime() + 60_000); + await recordRunKey(runId); await h.db .update(workflowRun) .set({ status: "completed", endedAt: current, - publicKey: "ab".repeat(32), lifecyclePolicy: null, capacityReleaseAt: deadline, }) @@ -1111,12 +1119,12 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("retention waits quietly while disconnected and resumes on a current connection", async () => { + await recordRunKey(runId); await h.db .update(workflowRun) .set({ status: "completed", endedAt: current, - publicKey: "ab".repeat(32), lifecyclePolicy: null, }) .where(eq(workflowRun.id, runId)); @@ -1154,12 +1162,12 @@ describe.skipIf(!harnessDbEnvAvailable())( }); test("retention scheduling ignores a candidate for an older generation", async () => { + await recordRunKey(runId); await h.db .update(workflowRun) .set({ status: "completed", endedAt: current, - publicKey: "ab".repeat(32), lifecyclePolicy: null, }) .where(eq(workflowRun.id, runId)); diff --git a/tests/hub-api/find-routable-by-id.test.ts b/tests/hub-api/find-routable-by-id.test.ts index 988d4a179..786a93d6a 100644 --- a/tests/hub-api/find-routable-by-id.test.ts +++ b/tests/hub-api/find-routable-by-id.test.ts @@ -12,8 +12,10 @@ import { harnessDbEnvAvailable, type TestDb, } from "@intx/test-harness/db-harness"; +import { retireRunPrincipalKey } from "@intx/db"; import { seedPrincipal, + seedRunPrincipalKey, seedTenants, seedWorkflowRun, } from "@intx/test-harness/seed"; @@ -70,6 +72,30 @@ describe.skipIf(!harnessDbEnvAvailable())("findRoutableById (real DB)", () => { expect(record?.updatedAt).toEqual(new Date("2026-01-01T00:00:00Z")); }); + test("reports the active key of the run's principal", async () => { + await seedBase(); + await seedWorkflowRun(h.db, { + id: "run_keyed", + tenantId: "tnt_root", + anchorRunId: "run_keyed", + address: "run_keyed@root.example", + }); + expect( + (await findRoutableById(h.db, "run_keyed", "tnt_root"))?.publicKey, + ).toBeNull(); + + await seedRunPrincipalKey(h.db, "tnt_root", "run_keyed", "aa".repeat(32)); + expect( + (await findRoutableById(h.db, "run_keyed", "tnt_root"))?.publicKey, + ).toBe("aa".repeat(32)); + + // A sidecar replacement retires the key. + await retireRunPrincipalKey(h.db, "tnt_root", "run_keyed"); + expect( + (await findRoutableById(h.db, "run_keyed", "tnt_root"))?.publicKey, + ).toBeNull(); + }); + test("a terminal run reports endedAt as its updatedAt", async () => { await seedBase(); await h.db.insert(workflowDefinition).values({ diff --git a/tests/hub-api/resolve-routable-address.test.ts b/tests/hub-api/resolve-routable-address.test.ts index fe7c11361..c574959ab 100644 --- a/tests/hub-api/resolve-routable-address.test.ts +++ b/tests/hub-api/resolve-routable-address.test.ts @@ -67,7 +67,6 @@ describe.skipIf(!harnessDbEnvAvailable())( principalId: "prn_run", address: "run_folded@root.example", status: "running", - publicKey: "pk-run", }); const endpoint = await resolveRoutableAddress( @@ -76,7 +75,6 @@ describe.skipIf(!harnessDbEnvAvailable())( ); expect(endpoint?.id).toBe("run_folded"); expect(endpoint?.tenantId).toBe("tnt_root"); - expect(endpoint?.publicKey).toBe("pk-run"); expect(endpoint?.sessionId).toBe("ses_r"); }); diff --git a/tests/hub-api/trigger-delegator-race.test.ts b/tests/hub-api/trigger-delegator-race.test.ts index 38f736281..a2fbebaad 100644 --- a/tests/hub-api/trigger-delegator-race.test.ts +++ b/tests/hub-api/trigger-delegator-race.test.ts @@ -34,6 +34,7 @@ import { seedAsset, seedGrant, seedPrincipal, + seedRunPrincipalKey, seedWorkflowRun, seedWorkflowRunLaunchSpec, } from "@intx/test-harness/seed"; @@ -342,10 +343,7 @@ describe.skipIf(!harnessDbEnvAvailable())( }); // The deploy ack recorded the anchor's initialization. - await h.db - .update(workflowRun) - .set({ publicKey: "ab".repeat(32) }) - .where(eq(workflowRun.id, ANCHOR)); + await seedRunPrincipalKey(h.db, tenantId, ANCHOR, "ab".repeat(32)); await trigger(app, tenantId); diff --git a/tests/lib/seed.ts b/tests/lib/seed.ts index 3688e528c..cca005969 100644 --- a/tests/lib/seed.ts +++ b/tests/lib/seed.ts @@ -402,20 +402,32 @@ export async function seedWorkflowRun( ...(r.createdAt !== undefined ? { createdAt: r.createdAt } : {}), ...(r.endedAt !== undefined ? { endedAt: r.endedAt } : {}), }); - // A recorded key also lives on the run principal, as a deploy records it. - // The run principal is the one a test already seeded for the run, or the - // one deploy would have created. if (r.publicKey != null) { - await insertRunPrincipal(db, { - id: await deriveRunPrincipalId(r.tenantId, r.id), - tenantId: r.tenantId, - runId: r.id, - createdAt: r.createdAt ?? new Date(), - }); - await recordRunPrincipalKey(db, r.tenantId, r.id, r.publicKey); + await seedRunPrincipalKey(db, r.tenantId, r.id, r.publicKey, r.createdAt); } } +/** + * Make `publicKey` the active key of the run's principal, as a deploy records + * it. The run principal is the one a test already seeded for the run, or the + * one deploy would have created. + */ +export async function seedRunPrincipalKey( + db: Db, + tenantId: string, + runId: string, + publicKey: string, + createdAt: Date = new Date(), +): Promise { + await insertRunPrincipal(db, { + id: await deriveRunPrincipalId(tenantId, runId), + tenantId, + runId, + createdAt, + }); + await recordRunPrincipalKey(db, tenantId, runId, publicKey); +} + export type SeedWorkflowRunLaunchSpec = { anchorRunId: string; sourceAuthorityPrincipalId: string; diff --git a/tests/workflow-deploy/mail-federation-derived-grants-roundtrip.test.ts b/tests/workflow-deploy/mail-federation-derived-grants-roundtrip.test.ts index bd772a5d2..be4427df8 100644 --- a/tests/workflow-deploy/mail-federation-derived-grants-roundtrip.test.ts +++ b/tests/workflow-deploy/mail-federation-derived-grants-roundtrip.test.ts @@ -91,6 +91,7 @@ import { seedGrant, seedPrincipal, seedPrincipalKey, + seedRunPrincipalKey, seedWorkflowRunLaunchSpec, tenantSlugFromId, } from "@intx/test-harness/seed"; @@ -361,11 +362,16 @@ describe.skipIf(!harnessDbEnvAvailable())( address, // The deploy-time birth state; the first mail trigger flips it running. status: "deployed", - // A recorded key with no credential refs is a finished initialization - // that delivered nothing. The first grant commit refuses an anchor - // that has recorded neither. - publicKey: "test-public-key", }); + // A recorded key with no credential refs is a finished initialization + // that delivered nothing. The first grant commit refuses an anchor that + // has recorded neither. + await seedRunPrincipalKey( + h.db, + TENANT_ID, + opts.anchorRunId, + `key-${opts.anchorRunId}`, + ); // Freeze the deploy-approved grant-walk snapshot into the deployment's // launch spec: the trigger route materializes the run's grants from it, // including the creator-sourced requirement resolved against the asset @@ -505,8 +511,9 @@ describe.skipIf(!harnessDbEnvAvailable())( const body = ErrorBody.assert(await res.json()); expect(body.error.code).toBe("insufficient_grants"); - // Nothing committed: no run principal, no run row, no grant rows on a - // run principal. The route rejects before `commitRunGrants` runs. + // Nothing committed: the run principal deploy created stays unlinked and + // holds no grant rows, and no run row exists. The route rejects before + // `commitRunGrants` runs. const runPrincipals = await h.db .select() .from(principalTable) @@ -516,7 +523,17 @@ describe.skipIf(!harnessDbEnvAvailable())( eq(principalTable.kind, "workflow"), ), ); - expect(runPrincipals).toHaveLength(0); + expect(runPrincipals.map((p) => p.refId)).toEqual([anchorRunId]); + const [anchor] = await h.db + .select({ principalId: workflowRunTable.principalId }) + .from(workflowRunTable) + .where(eq(workflowRunTable.id, anchorRunId)); + expect(anchor?.principalId).toBeNull(); + const runPrincipalGrants = await h.db + .select() + .from(grantTable) + .where(eq(grantTable.principalId, runPrincipals[0]?.id ?? "")); + expect(runPrincipalGrants).toHaveLength(0); // Only the deployment's seeded anchor run (id == anchorRunId) exists; // the rejected trigger committed no child run of its own. From cad2cfe2e649e29ab0cf39424760fcaa77292514 Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 17:25:48 -0700 Subject: [PATCH 11/14] Drop the run key copy from the deployment anchor --- docs/IMPLEMENTATION.md | 4 +- .../0122_drop_workflow_run_public_key.sql | 37 + .../db/migrations/meta/0122_snapshot.json | 5001 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/src/parse-row.test.ts | 1 - packages/db/src/run-principal-key.ts | 32 +- packages/db/src/schema/workflow-run.ts | 5 +- packages/db/src/sidecar-allocation-store.ts | 49 +- .../hub-sessions/src/session-service.test.ts | 192 +- packages/hub-sessions/src/session-service.ts | 89 +- .../src/sidecar-allocation/contracts.ts | 10 +- .../hub-sessions/src/ws/sidecar-handler.ts | 4 +- tests/db/anchor-before-frame.test.ts | 27 +- tests/db/mail-triggered-run-grants.test.ts | 2 +- ...on-0121-run-principal-sidecar-keys.test.ts | 29 +- ...-0122-drop-workflow-run-public-key.test.ts | 242 + tests/db/run-principal-key.test.ts | 99 +- tests/db/sidecar-allocation-store.test.ts | 101 +- .../db/sidecar-lifecycle-integration.test.ts | 4 +- tests/lib/seed.ts | 11 +- .../multistep-conversation-failover.test.ts | 26 +- .../walking-skeleton.e2e.test.ts | 6 +- 22 files changed, 5705 insertions(+), 273 deletions(-) create mode 100644 packages/db/migrations/0122_drop_workflow_run_public_key.sql create mode 100644 packages/db/migrations/meta/0122_snapshot.json create mode 100644 tests/db/migration-0122-drop-workflow-run-public-key.test.ts diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 7fda19ac8..aec479f41 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -369,7 +369,7 @@ The reconciler owns acknowledged undeploys for releasing and replacing deploymen A non-retryable destroy rejection moves a `releasing` or `replacing` allocation to terminal `destroy_failed`. The Hub records the failure, keeps a cleanup-only binding, and stops reconciliation. It fails remaining live runs and unsettled dispatches unless deployment removal is confirmed and cancellation is pending; then lifecycle reconciliation settles cancellation and dispatches after recovering accepted history. The allocation retains its sidecar id and external reference for operator recovery, but cannot dispatch work or create a replacement. Its copy occupies an active slot until removal or retained storage is confirmed. Reconnect preserves inventory access without an untracked hello undeploy or a new cleanup attempt. Cleanup must preserve other work on a shared sidecar. The deployment list reports `destroy_failed` explicitly. -Every replacement increments `generation`. The generation is persisted before the provisioner is called and appears in the ensure/destroy request and the sidecar's authenticated identity. Provisioners must make ensure/destroy idempotent and reject a generation older than one they have already observed. The Hub separately records `ensure_accepted_generation`; registration may occur while capacity is converging, but readiness and routing require the exact generation to be accepted. A reconnecting sidecar takes a deployment's route back only once the deployment's first deploy completed: its anchor's key is committed and no initialization is in flight. Releasing a deployment on a sidecar that hosts other work does not destroy the sidecar, so the Hub undeploys a copy whose deploy is still uncertain rather than routing to it. A copy whose run has ended takes no route either. The lifecycle service stops and transfers it under the [retained-capacity policy](SIDECAR_PLACEMENT.md#active-and-retained-capacity). A deferred failure whose run is still live remains ineligible for reclaim and is undeployed. +Every replacement increments `generation`. The generation is persisted before the provisioner is called and appears in the ensure/destroy request and the sidecar's authenticated identity. Provisioners must make ensure/destroy idempotent and reject a generation older than one they have already observed. The Hub separately records `ensure_accepted_generation`; registration may occur while capacity is converging, but readiness and routing require the exact generation to be accepted. A reconnecting sidecar takes a deployment's route back only once the deployment's first deploy completed: its run principal holds the deployment's key and no initialization is in flight. Releasing a deployment on a sidecar that hosts other work does not destroy the sidecar, so the Hub undeploys a copy whose deploy is still uncertain rather than routing to it. A copy whose run has ended takes no route either. The lifecycle service stops and transfers it under the [retained-capacity policy](SIDECAR_PLACEMENT.md#active-and-retained-capacity). A deferred failure whose run is still live remains ineligible for reclaim and is undeployed. The Hub never persists raw sidecar bearer tokens; it stores only their hashes. A sidecar connection grants cleanup while it hosts a `releasing`, `replacing`, or `destroy_failed` allocation, including a permanent provider rejection. A disconnect timeout resumes cleanup on reconnect; an exhausted retry budget remains terminal. A cleanup binding permits registration, reporting existing retained-copy proofs, and cleanup. It grants no readiness, routing, reclaim, or permission to request new retention. A sidecar with neither current work nor cleanup obligations stays connected, idle, after the Hub asks it to undeploy what its hello reports, so its provisioner can place new work on it. If the Hub restarts during a probe, startup marks its temporary capacity for release; background reconciliation performs and retries destruction. If it restarts after binding an allocation generation but before recording ensure acceptance, it treats that outcome as uncertain, fences the generation, destroys it idempotently, and mints a replacement identity. On Hub startup, interrupted probes are durably marked for cleanup before active allocation fences are rebuilt; no provider call is awaited during this initialization. @@ -427,7 +427,7 @@ The allocation reconciler uses database leases and retry timestamps rather than Before sending an allocated deploy, the Hub commits an `initialization_lease_id` on the allocation under the current accepted generation and unexpired reconciliation lease. Restoration and preparation happen before this reservation. The router rechecks cancellation and the exact socket after the reservation commits. A crash between reservation and send can conservatively require cleanup even if no frame reached the worker. A proven unsent failure of a confirmed reservation clears its own initialization marker, even after cancellation or loss of its reconciliation lease, provided the exact marker, allocation status, anchor identity, and accepted generation still match. If reservation confirmation is lost, the caller cannot prove its reservation and leaves the marker for conservative cleanup. Cancellation, disconnects, retries, and lease changes otherwise preserve the marker; a stale clear cannot remove a newer attempt or affect another generation. -An allocated supervisor's deploy acknowledgement does not publish its public key immediately. The Hub publishes the key and credential references and clears the exact initialization marker in one allocation-locked transaction, rechecking lease expiry before committing the mutations. A later reconciliation with an outstanding marker fences and releases the generation before checking connectivity or restoring history. That cleanup requires the exact observed marker to remain present when the generation advances. A concurrent proven-unsent rollback instead requeues ordinary initialization checks; absence of the marker alone does not publish readiness. Readiness and dispatch require the marker to be absent, while bootstrap Git uploads remain permitted. Release consumes the marker atomically with the generation advance. A publication error whose transaction actually committed must not release the completed worker: cleanup revalidates the marker and key after acquiring the allocation lock, and completed initialization re-enters the normal callback to finish dispatch requeue and readiness. An active allocated supervisor without a completed initialization key remains uncertain and must be fenced and released, never redeployed in place. +An allocated supervisor's deploy acknowledgement does not publish its public key immediately. The Hub records the key as the active key of the deployment's run principal, stores the credential references, and clears the exact initialization marker in one allocation-locked transaction, rechecking lease expiry before committing the mutations. A later reconciliation with an outstanding marker fences and releases the generation before checking connectivity or restoring history. That cleanup requires the exact observed marker to remain present when the generation advances. A concurrent proven-unsent rollback instead requeues ordinary initialization checks; absence of the marker alone does not publish readiness. Readiness and dispatch require the marker to be absent, while bootstrap Git uploads remain permitted. Release consumes the marker atomically with the generation advance. A publication error whose transaction actually committed must not release the completed worker: cleanup revalidates the marker and key after acquiring the allocation lock, and completed initialization re-enters the normal callback to finish dispatch requeue and readiness. An active allocated supervisor without a completed initialization key remains uncertain and must be fenced and released, never redeployed in place. Mail sent before initialization publishes its signing key is deferred under the allocation generation and reconciliation attempt. Cancellation leaves that mail pending because the key transaction may already have committed. A confirmed unsent rollback fails queued mail. A new reconciliation resolves the previous attempt's marker and key before waiting for a connection. A completed initialization releases its queued mail and a rolled-back one fails it; an outstanding marker leaves mail pending until fencing confirms failure, because the prior caller may still complete a proven-unsent rollback. A lost rollback response is resolved on a later claim, which finds neither a marker nor a key. A durable generation advance also clears an unfinished sender attempt, including when the cleanup transaction's response was lost. Late callbacks can settle only their own attempt. Deferred delivery carries the confirmed signing key so a newer attempt cannot capture the queued message or change its verification key. The Hub completes asynchronous delivery preparation before sending the sender key and mail consecutively on the same socket. The existing deferred-mail TTL remains the fallback if recovery cannot resolve the outcome. diff --git a/packages/db/migrations/0122_drop_workflow_run_public_key.sql b/packages/db/migrations/0122_drop_workflow_run_public_key.sql new file mode 100644 index 000000000..0611192d7 --- /dev/null +++ b/packages/db/migrations/0122_drop_workflow_run_public_key.sql @@ -0,0 +1,37 @@ +-- Drop the deployment anchor's copy of its sidecar key. The run principal's +-- active key is the deployment's key, and the hub writes the two together, so +-- they agree. Refuse to drop a column that disagrees with its run principal +-- rather than lose the key it holds: a key the run principal does not hold +-- as its active key, or no key where the run principal holds an active one. +-- The check and the drop run in one statement under the lock the drop needs +-- on the run table. A writer of a run's key locks the deployment's anchor row +-- before it writes the key, so this lock also keeps another writer from +-- changing a key between the check and the drop. A hub transaction can still +-- deadlock with the migration's locks, here or in an earlier migration that +-- runs in the same transaction; the migration then rolls back whole and can +-- run again. +DO $$ +BEGIN + LOCK TABLE "workflow_run" IN ACCESS EXCLUSIVE MODE; + IF EXISTS ( + SELECT 1 FROM "workflow_run" r + WHERE r."public_key" IS NOT NULL AND NOT EXISTS ( + SELECT 1 FROM "principal" p + JOIN "principal_key" k ON k."principal_id" = p."id" AND k."status" = 'active' + WHERE p."tenant_id" = r."tenant_id" AND p."kind" = 'workflow' AND p."ref_id" = r."id" + AND k."public_key" = r."public_key" + ) + ) THEN + RAISE EXCEPTION 'a deployment anchor holds a key its run principal does not hold as active'; + END IF; + IF EXISTS ( + SELECT 1 FROM "workflow_run" r + JOIN "principal" p + ON p."tenant_id" = r."tenant_id" AND p."kind" = 'workflow' AND p."ref_id" = r."id" + JOIN "principal_key" k ON k."principal_id" = p."id" AND k."status" = 'active' + WHERE r."anchor_run_id" = r."id" AND r."public_key" IS NULL + ) THEN + RAISE EXCEPTION 'a deployment anchor without a key has a run principal with an active key'; + END IF; + ALTER TABLE "workflow_run" DROP COLUMN "public_key"; +END $$; diff --git a/packages/db/migrations/meta/0122_snapshot.json b/packages/db/migrations/meta/0122_snapshot.json new file mode 100644 index 000000000..dc6ceb947 --- /dev/null +++ b/packages/db/migrations/meta/0122_snapshot.json @@ -0,0 +1,5001 @@ +{ + "id": "6be781d3-bd69-4af1-bdb6-620b6abf9fce", + "prevId": "7e5f6f90-f79a-4bec-8972-dd7adcad714f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_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 + }, + "id_token": { + "name": "id_token", + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "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, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_trust": { + "name": "federation_trust", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_tenant_id": { + "name": "target_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federation_trust_tenant_id_tenant_id_fk": { + "name": "federation_trust_tenant_id_tenant_id_fk", + "tableFrom": "federation_trust", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_trust_target_tenant_id_tenant_id_fk": { + "name": "federation_trust_target_tenant_id_tenant_id_fk", + "tableFrom": "federation_trust", + "tableTo": "tenant", + "columnsFrom": ["target_tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federation_trust_tenant_id_target_tenant_id_unique": { + "name": "federation_trust_tenant_id_target_tenant_id_unique", + "nullsNotDistinct": false, + "columns": ["tenant_id", "target_tenant_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenant": { + "name": "tenant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "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": { + "tenant_domain_lower_idx": { + "name": "tenant_domain_lower_idx", + "columns": [ + { + "expression": "lower(\"domain\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tenant_parent_id_tenant_id_fk": { + "name": "tenant_parent_id_tenant_id_fk", + "tableFrom": "tenant", + "tableTo": "tenant", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tenant_slug_unique": { + "name": "tenant_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": { + "tenant_slug_dns_label_check": { + "name": "tenant_slug_dns_label_check", + "value": "\"tenant\".\"slug\" ~ '^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$' and length(\"tenant\".\"slug\") <= 63" + } + }, + "isRLSEnabled": false + }, + "public.principal": { + "name": "principal", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref_id": { + "name": "ref_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "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": { + "principal_tenant_id_tenant_id_fk": { + "name": "principal_tenant_id_tenant_id_fk", + "tableFrom": "principal", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "principal_tenant_id_kind_ref_id_unique": { + "name": "principal_tenant_id_kind_ref_id_unique", + "nullsNotDistinct": false, + "columns": ["tenant_id", "kind", "ref_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_key": { + "name": "principal_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "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": { + "principal_key_one_active": { + "name": "principal_key_one_active", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"principal_key\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_key_principal_id_principal_id_fk": { + "name": "principal_key_principal_id_principal_id_fk", + "tableFrom": "principal_key", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "principal_key_public_key_unique": { + "name": "principal_key_public_key_unique", + "nullsNotDistinct": false, + "columns": ["public_key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_role": { + "name": "agent_role", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "agent_role_agent_id_workflow_definition_id_fk": { + "name": "agent_role_agent_id_workflow_definition_id_fk", + "tableFrom": "agent_role", + "tableTo": "workflow_definition", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_role_role_id_role_id_fk": { + "name": "agent_role_role_id_role_id_fk", + "tableFrom": "agent_role", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_role_agent_id_role_id_pk": { + "name": "agent_role_agent_id_role_id_pk", + "columns": ["agent_id", "role_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_role": { + "name": "principal_role", + "schema": "", + "columns": { + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "principal_role_principal_id_principal_id_fk": { + "name": "principal_role_principal_id_principal_id_fk", + "tableFrom": "principal_role", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "principal_role_role_id_role_id_fk": { + "name": "principal_role_role_id_role_id_fk", + "tableFrom": "principal_role", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "principal_role_principal_id_role_id_pk": { + "name": "principal_role_principal_id_role_id_pk", + "columns": ["principal_id", "role_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "role_tenant_id_tenant_id_fk": { + "name": "role_tenant_id_tenant_id_fk", + "tableFrom": "role", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.grant": { + "name": "grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "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": { + "grant_invoker_allow_principal_idx": { + "name": "grant_invoker_allow_principal_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"grant\".\"origin\" = 'invoker' and \"grant\".\"effect\" = 'allow'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "grant_tenant_id_tenant_id_fk": { + "name": "grant_tenant_id_tenant_id_fk", + "tableFrom": "grant", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "grant_role_id_role_id_fk": { + "name": "grant_role_id_role_id_fk", + "tableFrom": "grant", + "tableTo": "role", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "grant_principal_id_principal_id_fk": { + "name": "grant_principal_id_principal_id_fk", + "tableFrom": "grant", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "grant_target_exactly_one": { + "name": "grant_target_exactly_one", + "value": "num_nonnulls(\"grant\".\"principal_id\", \"grant\".\"role_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.approval": { + "name": "approval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_address": { + "name": "agent_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_definition": { + "name": "tool_definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "tool_arguments": { + "name": "tool_arguments", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "timeout_at": { + "name": "timeout_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "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": { + "approval_tenant_status_idx": { + "name": "approval_tenant_status_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_anchor_run_idx": { + "name": "approval_anchor_run_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_run_idx": { + "name": "approval_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_tenant_id_tenant_id_fk": { + "name": "approval_tenant_id_tenant_id_fk", + "tableFrom": "approval", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "approval_anchor_run_id_workflow_run_id_fk": { + "name": "approval_anchor_run_id_workflow_run_id_fk", + "tableFrom": "approval", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "approval_run_id_workflow_run_id_fk": { + "name": "approval_run_id_workflow_run_id_fk", + "tableFrom": "approval", + "tableTo": "workflow_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "approval_correlation_id_unique": { + "name": "approval_correlation_id_unique", + "nullsNotDistinct": false, + "columns": ["correlation_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signal_correlation": { + "name": "signal_correlation", + "schema": "", + "columns": { + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_address": { + "name": "agent_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_name": { + "name": "signal_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_id": { + "name": "signal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "signal_correlation_tenant_id_tenant_id_fk": { + "name": "signal_correlation_tenant_id_tenant_id_fk", + "tableFrom": "signal_correlation", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "signal_correlation_anchor_run_id_workflow_run_id_fk": { + "name": "signal_correlation_anchor_run_id_workflow_run_id_fk", + "tableFrom": "signal_correlation", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "signal_correlation_run_id_workflow_run_id_fk": { + "name": "signal_correlation_run_id_workflow_run_id_fk", + "tableFrom": "signal_correlation", + "tableTo": "workflow_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider": { + "name": "provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin": { + "name": "plugin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_base_url": { + "name": "api_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_info_url": { + "name": "user_info_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "provider_tenant_id_tenant_id_fk": { + "name": "provider_tenant_id_tenant_id_fk", + "tableFrom": "provider", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_tenant_name": { + "name": "provider_tenant_name", + "nullsNotDistinct": false, + "columns": ["tenant_id", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "default_scopes": { + "name": "default_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "oauth_client_tenant_id_tenant_id_fk": { + "name": "oauth_client_tenant_id_tenant_id_fk", + "tableFrom": "oauth_client", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_client_provider_id_provider_id_fk": { + "name": "oauth_client_provider_id_provider_id_fk", + "tableFrom": "oauth_client", + "tableTo": "provider", + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_tenant_provider": { + "name": "oauth_client_tenant_provider", + "nullsNotDistinct": false, + "columns": ["tenant_id", "provider_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_version": { + "name": "secret_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "refresh_secret": { + "name": "refresh_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "metadata": { + "name": "metadata", + "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": {}, + "foreignKeys": { + "credential_tenant_id_tenant_id_fk": { + "name": "credential_tenant_id_tenant_id_fk", + "tableFrom": "credential", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_principal_id_principal_id_fk": { + "name": "credential_principal_id_principal_id_fk", + "tableFrom": "credential", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "credential_provider_id_provider_id_fk": { + "name": "credential_provider_id_provider_id_fk", + "tableFrom": "credential", + "tableTo": "provider", + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_oauth_client_id_oauth_client_id_fk": { + "name": "credential_oauth_client_id_oauth_client_id_fk", + "tableFrom": "credential", + "tableTo": "oauth_client", + "columnsFrom": ["oauth_client_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_tenant_name": { + "name": "credential_tenant_name", + "nullsNotDistinct": false, + "columns": ["tenant_id", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.asset": { + "name": "asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_principal_id": { + "name": "creator_principal_id", + "type": "text", + "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": {}, + "foreignKeys": { + "asset_tenant_id_tenant_id_fk": { + "name": "asset_tenant_id_tenant_id_fk", + "tableFrom": "asset", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "asset_creator_principal_id_principal_id_fk": { + "name": "asset_creator_principal_id_principal_id_fk", + "tableFrom": "asset", + "tableTo": "principal", + "columnsFrom": ["creator_principal_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "asset_tenant_kind_name": { + "name": "asset_tenant_kind_name", + "nullsNotDistinct": false, + "columns": ["tenant_id", "kind", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transaction": { + "name": "transaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "wallet_id": { + "name": "wallet_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_id": { + "name": "sender_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "transaction_wallet_id_wallet_id_fk": { + "name": "transaction_wallet_id_wallet_id_fk", + "tableFrom": "transaction", + "tableTo": "wallet", + "columnsFrom": ["wallet_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transaction_run_id_workflow_run_id_fk": { + "name": "transaction_run_id_workflow_run_id_fk", + "tableFrom": "transaction", + "tableTo": "workflow_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wallet": { + "name": "wallet", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backend_type": { + "name": "backend_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "config": { + "name": "config", + "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": {}, + "foreignKeys": { + "wallet_tenant_id_tenant_id_fk": { + "name": "wallet_tenant_id_tenant_id_fk", + "tableFrom": "wallet", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.offering": { + "name": "offering", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing": { + "name": "pricing", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "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": {}, + "foreignKeys": { + "offering_agent_id_workflow_definition_id_fk": { + "name": "offering_agent_id_workflow_definition_id_fk", + "tableFrom": "offering", + "tableTo": "workflow_definition", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "offering_tenant_id_tenant_id_fk": { + "name": "offering_tenant_id_tenant_id_fk", + "tableFrom": "offering", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model": { + "name": "model", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_name": { + "name": "canonical_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_tenant_id_tenant_id_fk": { + "name": "model_tenant_id_tenant_id_fk", + "tableFrom": "model", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_tenant_canonical_name": { + "name": "model_tenant_canonical_name", + "nullsNotDistinct": false, + "columns": ["tenant_id", "canonical_name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_offering": { + "name": "model_offering", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deployment_tags": { + "name": "deployment_tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "capabilities": { + "name": "capabilities", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "quirks": { + "name": "quirks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_offering_tenant_id_tenant_id_fk": { + "name": "model_offering_tenant_id_tenant_id_fk", + "tableFrom": "model_offering", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_offering_model_id_model_id_fk": { + "name": "model_offering_model_id_model_id_fk", + "tableFrom": "model_offering", + "tableTo": "model", + "columnsFrom": ["model_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_offering_provider_id_model_provider_id_fk": { + "name": "model_offering_provider_id_model_provider_id_fk", + "tableFrom": "model_offering", + "tableTo": "model_provider", + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_offering_tenant_model_provider": { + "name": "model_offering_tenant_model_provider", + "nullsNotDistinct": false, + "columns": ["tenant_id", "model_id", "provider_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_pricing": { + "name": "model_pricing", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "offering_id": { + "name": "offering_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_token_price": { + "name": "input_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_token_price": { + "name": "output_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_read_token_price": { + "name": "cache_read_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_write_token_price": { + "name": "cache_write_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thinking_token_price": { + "name": "thinking_token_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_request_fee": { + "name": "per_request_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_image_fee": { + "name": "per_image_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "per_audio_fee": { + "name": "per_audio_fee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effective_from": { + "name": "effective_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "model_pricing_tenant_id_tenant_id_fk": { + "name": "model_pricing_tenant_id_tenant_id_fk", + "tableFrom": "model_pricing", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_pricing_offering_id_model_offering_id_fk": { + "name": "model_pricing_offering_id_model_offering_id_fk", + "tableFrom": "model_pricing", + "tableTo": "model_offering", + "columnsFrom": ["offering_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_pricing_offering_currency_effective_from": { + "name": "model_pricing_offering_currency_effective_from", + "nullsNotDistinct": false, + "columns": ["offering_id", "currency", "effective_from"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_provider": { + "name": "model_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin": { + "name": "plugin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wallet_id": { + "name": "wallet_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "model_provider_tenant_id_tenant_id_fk": { + "name": "model_provider_tenant_id_tenant_id_fk", + "tableFrom": "model_provider", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "model_provider_credential_id_credential_id_fk": { + "name": "model_provider_credential_id_credential_id_fk", + "tableFrom": "model_provider", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "model_provider_wallet_id_wallet_id_fk": { + "name": "model_provider_wallet_id_wallet_id_fk", + "tableFrom": "model_provider", + "tableTo": "wallet", + "columnsFrom": ["wallet_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_provider_tenant_name": { + "name": "model_provider_tenant_name", + "nullsNotDistinct": false, + "columns": ["tenant_id", "name"] + } + }, + "policies": {}, + "checkConstraints": { + "model_provider_auth_xor": { + "name": "model_provider_auth_xor", + "value": "(\"model_provider\".\"credential_id\" is not null) <> (\"model_provider\".\"wallet_id\" is not null)" + } + }, + "isRLSEnabled": false + }, + "public.sidecar": { + "name": "sidecar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reconnect_window_ended_at": { + "name": "reconnect_window_ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'online'" + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sidecar_token_hash_sha256_unique": { + "name": "sidecar_token_hash_sha256_unique", + "nullsNotDistinct": false, + "columns": ["token_hash_sha256"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sidecar_allocation": { + "name": "sidecar_allocation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ensure_accepted_generation": { + "name": "ensure_accepted_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconciliation_lease_id": { + "name": "reconciliation_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reconciliation_lease_expires_at": { + "name": "reconciliation_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "initialization_lease_id": { + "name": "initialization_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ensure_attempts": { + "name": "ensure_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "destroy_attempts": { + "name": "destroy_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deployment_cleanup_confirmed": { + "name": "deployment_cleanup_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_hold_released": { + "name": "provider_hold_released", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "connect_deadline": { + "name": "connect_deadline", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "max_disconnected_ms": { + "name": "max_disconnected_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "first_deploy_failed_at": { + "name": "first_deploy_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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": { + "sidecar_allocation_anchor_run_idx": { + "name": "sidecar_allocation_anchor_run_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sidecar_allocation_sidecar_idx": { + "name": "sidecar_allocation_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sidecar_allocation_reconciliation_idx": { + "name": "sidecar_allocation_reconciliation_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing', 'releasing') and \"sidecar_allocation\".\"next_attempt_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "sidecar_allocation_dispatchable_anchor_idx": { + "name": "sidecar_allocation_dispatchable_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sidecar_allocation_anchor_run_id_workflow_run_id_fk": { + "name": "sidecar_allocation_anchor_run_id_workflow_run_id_fk", + "tableFrom": "sidecar_allocation", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sidecar_allocation_tenant_id_tenant_id_fk": { + "name": "sidecar_allocation_tenant_id_tenant_id_fk", + "tableFrom": "sidecar_allocation", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "sidecar_allocation_sidecar_id_sidecar_id_fk": { + "name": "sidecar_allocation_sidecar_id_sidecar_id_fk", + "tableFrom": "sidecar_allocation", + "tableTo": "sidecar", + "columnsFrom": ["sidecar_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "sidecar_allocation_status_check": { + "name": "sidecar_allocation_status_check", + "value": "\"sidecar_allocation\".\"status\" in ('pending', 'provisioning', 'allocated', 'replacing', 'releasing', 'destroy_failed', 'released', 'failed')" + }, + "sidecar_allocation_generation_check": { + "name": "sidecar_allocation_generation_check", + "value": "\"sidecar_allocation\".\"generation\" >= 0" + }, + "sidecar_allocation_max_disconnected_check": { + "name": "sidecar_allocation_max_disconnected_check", + "value": "\"sidecar_allocation\".\"max_disconnected_ms\" > 0" + }, + "sidecar_allocation_accepted_generation_check": { + "name": "sidecar_allocation_accepted_generation_check", + "value": "\"sidecar_allocation\".\"ensure_accepted_generation\" is null or \"sidecar_allocation\".\"ensure_accepted_generation\" <= \"sidecar_allocation\".\"generation\"" + }, + "sidecar_allocation_initialization_check": { + "name": "sidecar_allocation_initialization_check", + "value": "\"sidecar_allocation\".\"initialization_lease_id\" is null or (\"sidecar_allocation\".\"status\" = 'allocated' and \"sidecar_allocation\".\"ensure_accepted_generation\" is not null and \"sidecar_allocation\".\"ensure_accepted_generation\" = \"sidecar_allocation\".\"generation\")" + } + }, + "isRLSEnabled": false + }, + "public.agent_session": { + "name": "agent_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_session_tenant_id_tenant_id_fk": { + "name": "agent_session_tenant_id_tenant_id_fk", + "tableFrom": "agent_session", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_session_agent_id_workflow_definition_id_fk": { + "name": "agent_session_agent_id_workflow_definition_id_fk", + "tableFrom": "agent_session", + "tableTo": "workflow_definition", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "agent_session_principal_id_principal_id_fk": { + "name": "agent_session_principal_id_principal_id_fk", + "tableFrom": "agent_session", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_asset": { + "name": "session_asset", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mount_path": { + "name": "mount_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_pack_sha": { + "name": "asset_pack_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_commit_sha": { + "name": "source_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "materialized_at": { + "name": "materialized_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_asset_pack_sha_idx": { + "name": "session_asset_pack_sha_idx", + "columns": [ + { + "expression": "asset_pack_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "session_asset_instance_id_mount_path_pk": { + "name": "session_asset_instance_id_mount_path_pk", + "columns": ["instance_id", "mount_path"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inference_turn": { + "name": "inference_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inference_turn_instance_id_started_at_idx": { + "name": "inference_turn_instance_id_started_at_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inference_turn_session_id_agent_session_id_fk": { + "name": "inference_turn_session_id_agent_session_id_fk", + "tableFrom": "inference_turn", + "tableTo": "agent_session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inference_turn_tenant_id_tenant_id_fk": { + "name": "inference_turn_tenant_id_tenant_id_fk", + "tableFrom": "inference_turn", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_mail": { + "name": "session_mail", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "raw": { + "name": "raw", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_mail_instance_id_created_at_idx": { + "name": "session_mail_instance_id_created_at_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_mail_session_id_created_at_idx": { + "name": "session_mail_session_id_created_at_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_mail_session_id_agent_session_id_fk": { + "name": "session_mail_session_id_agent_session_id_fk", + "tableFrom": "session_mail", + "tableTo": "agent_session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_mail_tenant_id_tenant_id_fk": { + "name": "session_mail_tenant_id_tenant_id_fk", + "tableFrom": "session_mail", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.turn_part": { + "name": "turn_part", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "turn_part_turn_id_inference_turn_id_fk": { + "name": "turn_part_turn_id_inference_turn_id_fk", + "tableFrom": "turn_part", + "tableTo": "inference_turn", + "columnsFrom": ["turn_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "turn_part_session_id_agent_session_id_fk": { + "name": "turn_part_session_id_agent_session_id_fk", + "tableFrom": "turn_part", + "tableTo": "agent_session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_token": { + "name": "git_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref_pattern": { + "name": "ref_pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actions": { + "name": "actions", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "git_token_user_id_name_active_idx": { + "name": "git_token_user_id_name_active_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"git_token\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "git_token_tenant_id_tenant_id_fk": { + "name": "git_token_tenant_id_tenant_id_fk", + "tableFrom": "git_token", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "git_token_user_id_user_id_fk": { + "name": "git_token_user_id_user_id_fk", + "tableFrom": "git_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_token_principal_id_principal_id_fk": { + "name": "git_token_principal_id_principal_id_fk", + "tableFrom": "git_token", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_token_token_hash_sha256_unique": { + "name": "git_token_token_hash_sha256_unique", + "nullsNotDistinct": false, + "columns": ["token_hash_sha256"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.run_token": { + "name": "run_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash_sha256": { + "name": "token_hash_sha256", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "sealed_token": { + "name": "sealed_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "run_token_principal_consumer_idx": { + "name": "run_token_principal_consumer_idx", + "columns": [ + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "run_token_principal_id_principal_id_fk": { + "name": "run_token_principal_id_principal_id_fk", + "tableFrom": "run_token", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "run_token_token_hash_sha256_unique": { + "name": "run_token_token_hash_sha256_unique", + "nullsNotDistinct": false, + "columns": ["token_hash_sha256"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_definition": { + "name": "workflow_definition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creator_principal_id": { + "name": "creator_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wire_hash": { + "name": "wire_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_requirements": { + "name": "grant_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "model_requirements": { + "name": "model_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_bindings": { + "name": "credential_bindings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lifecycle_policy": { + "name": "lifecycle_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'deployed'" + }, + "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": { + "workflow_definition_tenant_idx": { + "name": "workflow_definition_tenant_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_definition_asset_wire_hash_idx": { + "name": "workflow_definition_asset_wire_hash_idx", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "wire_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_definition_tenant_id_tenant_id_fk": { + "name": "workflow_definition_tenant_id_tenant_id_fk", + "tableFrom": "workflow_definition", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_definition_creator_principal_id_principal_id_fk": { + "name": "workflow_definition_creator_principal_id_principal_id_fk", + "tableFrom": "workflow_definition", + "tableTo": "principal", + "columnsFrom": ["creator_principal_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_definition_asset_id_asset_id_fk": { + "name": "workflow_definition_asset_id_asset_id_fk", + "tableFrom": "workflow_definition", + "tableTo": "asset", + "columnsFrom": ["asset_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_definition_version": { + "name": "workflow_definition_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "approved_wire_hash": { + "name": "approved_wire_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_snapshot": { + "name": "grant_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_definition_version_definition_idx": { + "name": "workflow_definition_version_definition_idx", + "columns": [ + { + "expression": "definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_definition_version_definition_id_workflow_definition_id_fk": { + "name": "workflow_definition_version_definition_id_workflow_definition_id_fk", + "tableFrom": "workflow_definition_version", + "tableTo": "workflow_definition", + "columnsFrom": ["definition_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workflow_definition_version_definition_version": { + "name": "workflow_definition_version_definition_version", + "nullsNotDistinct": false, + "columns": ["definition_id", "version"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run": { + "name": "workflow_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition_id": { + "name": "definition_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kernel_id": { + "name": "kernel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_preferences": { + "name": "model_preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_refs": { + "name": "credential_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lifecycle_policy": { + "name": "lifecycle_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "operator_cancellation_requested_at": { + "name": "operator_cancellation_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_deadline": { + "name": "cancellation_deadline", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity_release_at": { + "name": "capacity_release_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "infrastructure_failed_at": { + "name": "infrastructure_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "legacy_mail_accept": { + "name": "legacy_mail_accept", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_definition_idx": { + "name": "workflow_run_definition_idx", + "columns": [ + { + "expression": "definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_address_idx": { + "name": "workflow_run_address_idx", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_run\".\"address\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_anchor_idx": { + "name": "workflow_run_anchor_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run\".\"id\" = \"workflow_run\".\"anchor_run_id\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_live_anchor_idx": { + "name": "workflow_run_live_anchor_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run\".\"id\" = \"workflow_run\".\"anchor_run_id\" and \"workflow_run\".\"status\" in ('deployed', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_infrastructure_failed_idx": { + "name": "workflow_run_infrastructure_failed_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run\".\"infrastructure_failed_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_sidecar_idx": { + "name": "workflow_run_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run\".\"sidecar_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_run_definition_id_workflow_definition_id_fk": { + "name": "workflow_run_definition_id_workflow_definition_id_fk", + "tableFrom": "workflow_run", + "tableTo": "workflow_definition", + "columnsFrom": ["definition_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_tenant_id_tenant_id_fk": { + "name": "workflow_run_tenant_id_tenant_id_fk", + "tableFrom": "workflow_run", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_principal_id_principal_id_fk": { + "name": "workflow_run_principal_id_principal_id_fk", + "tableFrom": "workflow_run", + "tableTo": "principal", + "columnsFrom": ["principal_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_run_sidecar_id_sidecar_id_fk": { + "name": "workflow_run_sidecar_id_sidecar_id_fk", + "tableFrom": "workflow_run", + "tableTo": "sidecar", + "columnsFrom": ["sidecar_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_run_legacy_mail_accept_check": { + "name": "workflow_run_legacy_mail_accept_check", + "value": "\"workflow_run\".\"legacy_mail_accept\" is null or \"workflow_run\".\"legacy_mail_accept\" in ('invoker-correspondent', 'correspondent')" + } + }, + "isRLSEnabled": false + }, + "public.workflow_run_dispatch": { + "name": "workflow_run_dispatch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'mail'" + }, + "sender_address": { + "name": "sender_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_message": { + "name": "raw_message", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "step_grants": { + "name": "step_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "acknowledged_generation": { + "name": "acknowledged_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "delivery_lease_id": { + "name": "delivery_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_lease_expires_at": { + "name": "delivery_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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()" + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_dispatch_anchor_message_idx": { + "name": "workflow_run_dispatch_anchor_message_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_dispatch_delivery_idx": { + "name": "workflow_run_dispatch_delivery_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run_dispatch\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_dispatch_anchor_status_idx": { + "name": "workflow_run_dispatch_anchor_status_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_dispatch_unresolved_anchor_idx": { + "name": "workflow_run_dispatch_unresolved_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_run_dispatch\".\"status\" in ('pending', 'acknowledged') or (\"workflow_run_dispatch\".\"status\" = 'abandoned' and \"workflow_run_dispatch\".\"next_attempt_at\" is not null)", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_run_dispatch_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_dispatch_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_dispatch", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_run_dispatch_status_check": { + "name": "workflow_run_dispatch_status_check", + "value": "\"workflow_run_dispatch\".\"status\" in ('pending', 'acknowledged', 'abandoned', 'settled', 'failed')" + }, + "workflow_run_dispatch_kind_check": { + "name": "workflow_run_dispatch_kind_check", + "value": "\"workflow_run_dispatch\".\"kind\" in ('mail', 'signal')" + }, + "workflow_run_dispatch_attempt_count_check": { + "name": "workflow_run_dispatch_attempt_count_check", + "value": "\"workflow_run_dispatch\".\"attempt_count\" >= 0" + }, + "workflow_run_dispatch_acknowledged_generation_check": { + "name": "workflow_run_dispatch_acknowledged_generation_check", + "value": "\"workflow_run_dispatch\".\"acknowledged_generation\" is null or \"workflow_run_dispatch\".\"acknowledged_generation\" >= 0" + }, + "workflow_run_dispatch_acknowledged_state_check": { + "name": "workflow_run_dispatch_acknowledged_state_check", + "value": "\"workflow_run_dispatch\".\"status\" <> 'acknowledged' or \"workflow_run_dispatch\".\"acknowledged_generation\" is not null" + }, + "workflow_run_dispatch_pending_schedule_check": { + "name": "workflow_run_dispatch_pending_schedule_check", + "value": "\"workflow_run_dispatch\".\"status\" <> 'pending' or \"workflow_run_dispatch\".\"next_attempt_at\" is not null" + }, + "workflow_run_dispatch_mail_sender_check": { + "name": "workflow_run_dispatch_mail_sender_check", + "value": "\"workflow_run_dispatch\".\"kind\" <> 'mail' or \"workflow_run_dispatch\".\"status\" in ('abandoned', 'settled', 'failed') or \"workflow_run_dispatch\".\"sender_address\" is not null" + } + }, + "isRLSEnabled": false + }, + "public.workflow_pending_projection": { + "name": "workflow_pending_projection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "history_required": { + "name": "history_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "workflow_pending_projection_anchor_idx": { + "name": "workflow_pending_projection_anchor_idx", + "columns": [ + { + "expression": "anchor_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_pending_projection_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_pending_projection_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_pending_projection", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run_launch_spec": { + "name": "workflow_run_launch_spec", + "schema": "", + "columns": { + "anchor_run_id": { + "name": "anchor_run_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_domain": { + "name": "deployment_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_authority_principal_id": { + "name": "source_authority_principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "frozen_approval_bundle": { + "name": "frozen_approval_bundle", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "definition_artifact_id": { + "name": "definition_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_snapshot": { + "name": "grant_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_offering_ids": { + "name": "source_offering_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "default_source_offering_id": { + "name": "default_source_offering_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_package_pins": { + "name": "tool_package_pins", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_launch_spec_anchor_run_id_workflow_run_id_fk": { + "name": "workflow_run_launch_spec_anchor_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_launch_spec", + "tableTo": "workflow_run", + "columnsFrom": ["anchor_run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_run_launch_spec_source_authority_principal_id_principal_id_fk": { + "name": "workflow_run_launch_spec_source_authority_principal_id_principal_id_fk", + "tableFrom": "workflow_run_launch_spec", + "tableTo": "principal", + "columnsFrom": ["source_authority_principal_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workflow_run_launch_spec_definition_artifact_id_workflow_definition_artifact_id_fk": { + "name": "workflow_run_launch_spec_definition_artifact_id_workflow_definition_artifact_id_fk", + "tableFrom": "workflow_run_launch_spec", + "tableTo": "workflow_definition_artifact", + "columnsFrom": ["definition_artifact_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_run_execution": { + "name": "workflow_run_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_run_execution_run_id_id_idx": { + "name": "workflow_run_execution_run_id_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_run_execution_status_idx": { + "name": "workflow_run_execution_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_run_execution_run_id_workflow_run_id_fk": { + "name": "workflow_run_execution_run_id_workflow_run_id_fk", + "tableFrom": "workflow_run_execution", + "tableTo": "workflow_run", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_probe": { + "name": "workflow_probe", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "definition_asset_id": { + "name": "definition_asset_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entry": { + "name": "entry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pin": { + "name": "pin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "provisioner_id": { + "name": "provisioner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioner_api_version": { + "name": "provisioner_api_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provisioner_binding_fingerprint": { + "name": "provisioner_binding_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sidecar_id": { + "name": "sidecar_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_manifest_digest": { + "name": "adapter_manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_message": { + "name": "failure_message", + "type": "text", + "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": { + "workflow_probe_active_idx": { + "name": "workflow_probe_active_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_probe\".\"status\" in ('pending', 'provisioning', 'probing', 'releasing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_probe_sidecar_idx": { + "name": "workflow_probe_sidecar_idx", + "columns": [ + { + "expression": "sidecar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_probe_tenant_id_tenant_id_fk": { + "name": "workflow_probe_tenant_id_tenant_id_fk", + "tableFrom": "workflow_probe", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workflow_probe_definition_asset_id_asset_id_fk": { + "name": "workflow_probe_definition_asset_id_asset_id_fk", + "tableFrom": "workflow_probe", + "tableTo": "asset", + "columnsFrom": ["definition_asset_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workflow_probe_sidecar_id_sidecar_id_fk": { + "name": "workflow_probe_sidecar_id_sidecar_id_fk", + "tableFrom": "workflow_probe", + "tableTo": "sidecar", + "columnsFrom": ["sidecar_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_probe_status_check": { + "name": "workflow_probe_status_check", + "value": "\"workflow_probe\".\"status\" in ('pending', 'provisioning', 'probing', 'releasing', 'succeeded', 'failed')" + }, + "workflow_probe_succeeded_result_check": { + "name": "workflow_probe_succeeded_result_check", + "value": "\"workflow_probe\".\"status\" <> 'succeeded' or \"workflow_probe\".\"result\" is not null" + }, + "workflow_probe_generation_check": { + "name": "workflow_probe_generation_check", + "value": "\"workflow_probe\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_definition_artifact": { + "name": "workflow_definition_artifact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_definition_artifact_revision_idx": { + "name": "workflow_definition_artifact_revision_idx", + "columns": [ + { + "expression": "revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_definition_artifact_revision_id_workflow_definition_revision_id_fk": { + "name": "workflow_definition_artifact_revision_id_workflow_definition_revision_id_fk", + "tableFrom": "workflow_definition_artifact", + "tableTo": "workflow_definition_revision", + "columnsFrom": ["revision_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_definition_artifact_state_check": { + "name": "workflow_definition_artifact_state_check", + "value": "(\"workflow_definition_artifact\".\"result\" is null and \"workflow_definition_artifact\".\"lease_owner\" is not null and \"workflow_definition_artifact\".\"lease_expires_at\" is not null) or (\"workflow_definition_artifact\".\"result\" is not null and \"workflow_definition_artifact\".\"lease_owner\" is null and \"workflow_definition_artifact\".\"lease_expires_at\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.workflow_definition_revision": { + "name": "workflow_definition_revision", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "asset_commit_sha": { + "name": "asset_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entry": { + "name": "entry", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "closure": { + "name": "closure", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_definition_revision_tenant_id_tenant_id_fk": { + "name": "workflow_definition_revision_tenant_id_tenant_id_fk", + "tableFrom": "workflow_definition_revision", + "tableTo": "tenant", + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workflow_definition_revision_asset_id_asset_id_fk": { + "name": "workflow_definition_revision_asset_id_asset_id_fk", + "tableFrom": "workflow_definition_revision", + "tableTo": "asset", + "columnsFrom": ["asset_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 862cd22e9..890b3f7a7 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -848,6 +848,13 @@ "when": 1793842006891, "tag": "0121_run_principal_sidecar_keys", "breakpoints": true + }, + { + "idx": 122, + "version": "7", + "when": 1793842023286, + "tag": "0122_drop_workflow_run_public_key", + "breakpoints": true } ] } diff --git a/packages/db/src/parse-row.test.ts b/packages/db/src/parse-row.test.ts index 5a7689676..d29609ea1 100644 --- a/packages/db/src/parse-row.test.ts +++ b/packages/db/src/parse-row.test.ts @@ -307,7 +307,6 @@ function makeWorkflowRunRow( principalId: null, status: "running", address: null, - publicKey: null, sidecarId: null, kernelId: null, modelPreferences: null, diff --git a/packages/db/src/run-principal-key.ts b/packages/db/src/run-principal-key.ts index 9e757e67c..986d49385 100644 --- a/packages/db/src/run-principal-key.ts +++ b/packages/db/src/run-principal-key.ts @@ -11,6 +11,7 @@ import type { DBExecutor } from "./client"; import { recordPublicKey, retireActiveKey } from "./principal-key-store"; import { principal } from "./schema/principals"; import { principalKey } from "./schema/principal-keys"; +import { workflowRun } from "./schema/workflow-run"; // A column of the enclosing query, named with its table. Drizzle renders a // bare column reference unqualified in a single-table statement, and inside @@ -111,28 +112,47 @@ async function loadRunPrincipalId( } /** - * Make `publicKey`, which the sidecar running the run minted, the active key - * of the run's principal. The sidecar holds the private key. The caller holds - * the run's anchor row lock, which serializes every writer of a run's key. - * Throws when the run has no run principal. + * Make `publicKey`, which the sidecar running the deployment minted, the + * active key of the deployment's run principal. The sidecar holds the private + * key. Locks the deployment's anchor row first: every writer of a run's key + * and every reader that must not race one holds that lock. Returns false when + * `runId` names no deployment anchor in the tenant, and throws when the + * anchor has no run principal. Call it in a transaction. */ export async function recordRunPrincipalKey( tx: DBExecutor, tenantId: string, runId: string, publicKey: string, -): Promise { +): Promise { + // FOR NO KEY UPDATE, as an update of a non-key column would take: it + // conflicts with the other anchor locks but not with the key-share locks + // that inserts referencing the anchor take. + const [anchor] = await tx + .select({ id: workflowRun.id }) + .from(workflowRun) + .where( + and( + eq(workflowRun.id, runId), + eq(workflowRun.anchorRunId, runId), + eq(workflowRun.tenantId, tenantId), + ), + ) + .for("no key update"); + if (anchor === undefined) return false; const principalId = await loadRunPrincipalId(tx, tenantId, runId); if (principalId === null) { throw new Error(`Run ${runId} has no run principal to hold its key`); } await recordPublicKey(tx, principalId, publicKey); + return true; } /** * Retire the active key of the run's principal, so the key a replaced sidecar * held no longer answers for the run. A run with no run principal, or with no - * active key, is left as it is. + * active key, is left as it is. The caller holds the deployment's anchor row + * lock. */ export async function retireRunPrincipalKey( tx: DBExecutor, diff --git a/packages/db/src/schema/workflow-run.ts b/packages/db/src/schema/workflow-run.ts index 62ad8ccce..5f08c0907 100644 --- a/packages/db/src/schema/workflow-run.ts +++ b/packages/db/src/schema/workflow-run.ts @@ -114,13 +114,12 @@ export const workflowRun = pgTable( // Runtime bindings. `address` is a run's routing endpoint -- a deployment's // anchor run carries a workflow-derived address; nullable because an // address-less child run routes via its deployment, but unique among the - // runs that set it (the partial unique index below). `publicKey` is - // persisted when the sidecar acks the deploy. No code populates + // runs that set it (the partial unique index below). A deployment's + // signing key lives on its run principal, not here. No code populates // `sidecarId`/`kernelId`, and no code writes `modelPreferences`: only // the retired folded launch route wrote it, and readers parse rows it // left behind. address: text("address"), - publicKey: text("public_key"), sidecarId: text("sidecar_id").references(() => sidecar.id, { onDelete: "set null", }), diff --git a/packages/db/src/sidecar-allocation-store.ts b/packages/db/src/sidecar-allocation-store.ts index 437d36ee7..ae1220fa1 100644 --- a/packages/db/src/sidecar-allocation-store.ts +++ b/packages/db/src/sidecar-allocation-store.ts @@ -569,32 +569,19 @@ function buildSidecarAllocationStore( args.signal.throwIfAborted(); if (allocation === undefined) return false; if (completion !== undefined) { - const [anchor] = await tx - .update(workflowRun) - .set({ - publicKey: completion.publicKey, - ...(completion.credentialRefs !== undefined - ? { credentialRefs: completion.credentialRefs } - : {}), - }) - .where( - and( - eq(workflowRun.id, args.anchorRunId), - eq(workflowRun.anchorRunId, args.anchorRunId), - eq(workflowRun.tenantId, args.tenantId), - ), - ) - .returning({ id: workflowRun.id }); - if (anchor === undefined) - throw new Error("Initialization anchor is missing"); - // The anchor update above holds the anchor row lock, which serializes - // this with every other writer of the run's key. - await recordRunPrincipalKey( + const recorded = await recordRunPrincipalKey( tx, args.tenantId, args.anchorRunId, completion.publicKey, ); + if (!recorded) throw new Error("Initialization anchor is missing"); + if (completion.credentialRefs !== undefined) { + await tx + .update(workflowRun) + .set({ credentialRefs: completion.credentialRefs }) + .where(eq(workflowRun.id, args.anchorRunId)); + } } // Check the lease again: ownership can expire while the transaction holds // the allocation lock, such as while a completion waits on the anchor @@ -843,7 +830,7 @@ function buildSidecarAllocationStore( reconciliationLeaseId: null, reconciliationLeaseExpiresAt: null, // Preserve the disconnect window for an initialized deployment or - // replacement cleanup, which has already cleared the anchor's key. + // replacement cleanup, which has already retired the run's key. // A first-connect deadline does not bound subsequent cleanup waiting. connectDeadline: sql`case when ${sidecarAllocation.status} = 'replacing' or ${runPrincipalHasActiveKey(sidecarAllocation.tenantId, sidecarAllocation.anchorRunId)} then ${sidecarAllocation.connectDeadline} else null end`, // Placement and initialization retry diagnostics are not release causes. @@ -1541,16 +1528,18 @@ function buildSidecarAllocationStore( .returning(); if (updated === undefined) return null; - // The anchor public key is durable proof that this allocation - // generation completed restore and deployment. Clear the key and - // the credential refs with the generation advance so a replacement - // cannot mistake stale local sidecar state for a restored - // workflow. Leftover refs are the previous attempt, and a first - // grant commit would freeze them. The replacement sidecar mints a - // new key, so the run principal's key retires with it. + // The run principal's active key is durable proof that this + // allocation generation completed restore and deployment. Retire + // the key and clear the credential refs with the generation advance + // so a replacement cannot mistake stale local sidecar state for a + // restored workflow. Leftover refs are the previous attempt, and a + // first grant commit would freeze them. The replacement records the + // key its sidecar reports, which is the retired key again when it + // reuses the replaced sidecar's disk. The refs update holds the + // anchor row lock that retiring the key requires. await tx .update(workflowRun) - .set({ publicKey: null, credentialRefs: null }) + .set({ credentialRefs: null }) .where(eq(workflowRun.id, updated.anchorRunId)); await retireRunPrincipalKey(tx, updated.tenantId, updated.anchorRunId); diff --git a/packages/hub-sessions/src/session-service.test.ts b/packages/hub-sessions/src/session-service.test.ts index 084af4f69..6f5a633e7 100644 --- a/packages/hub-sessions/src/session-service.test.ts +++ b/packages/hub-sessions/src/session-service.test.ts @@ -477,36 +477,76 @@ describe("deployCodeSourcedWorkflow", () => { const SOURCE = { kind: "registry", registry: "npm" } as const; const TENANT = "tnt_test"; // These unit tests assert FRAME logic plus the SHAPE of the anchor - // workflow_run row the composed entrypoint writes (status, self-ref, born - // null-key) and its post-ack public-key stamp -- not its persistence (the + // workflow_run row the composed entrypoint writes (status, self-ref) and + // the key its ack records on the run principal -- not its persistence (the // real anchor-in-a-live-DB proof, including the prepare-before-deploy ordering, // lives in tests/db/workflow-allocation-service.test.ts). A capturing db - // records the anchor insert and the success-path public-key update, and + // records the anchor insert and the success-path key record, and // answers the persisted-definition // guard's existence query with a matching row; the fail-path tests throw // before reaching the insert. let capturedAnchorRow: Record | undefined; let capturedAnchorUpdate: Record | undefined; - - // Recording a deployment's key reads its run principal and the principal's - // keys, then inserts the key. Answer those reads with the run principal and - // no keys, and pass every other read to the wrapped double. - function withRunPrincipalKeys(handle: T): T { + // The key the success path records on the run principal. + let capturedRunKey: unknown; + + // Recording a deployment's key locks its anchor (the only FOR NO KEY UPDATE + // read of a run), reads its run principal and the principal's keys, then + // inserts the key. Answer those reads with the anchor, the run principal and + // no keys, report each recorded key to `onRecordKey`, and pass every other + // call to the wrapped double. + function withRunPrincipalKeys( + handle: T, + onRecordKey?: (publicKey: unknown) => void, + ): T { const inner = handle as { - select?: () => { from: (table: unknown) => unknown }; - insert?: unknown; + select?: () => { + from: (table: unknown) => { + where?: (...args: unknown[]) => Record; + }; + }; + insert?: (table: unknown) => unknown; }; + function anchorLock(table: unknown) { + return { + where: (...args: unknown[]) => { + const read = + inner + .select?.() + .from(table) + .where?.(...args) ?? {}; + const lock = read["for"]; + return Object.assign(read, { + for: (mode: unknown, ...rest: unknown[]) => + mode === "no key update" + ? Promise.resolve([{ id: ANCHOR_RUN_ID }]) + : typeof lock === "function" + ? lock(mode, ...rest) + : Promise.resolve([]), + }); + }, + }; + } return { ...handle, select: () => ({ from: (table: unknown) => - table === principalTable - ? { where: () => ({ limit: async () => [{ id: "prn_run" }] }) } - : table === principalKeyTable - ? { where: () => ({ limit: async () => [] }) } - : inner.select?.().from(table), + table === workflowRunTable + ? anchorLock(table) + : table === principalTable + ? { where: () => ({ limit: async () => [{ id: "prn_run" }] }) } + : table === principalKeyTable + ? { where: () => ({ limit: async () => [] }) } + : inner.select?.().from(table), }), - insert: inner.insert ?? (() => ({ values: async () => undefined })), + insert: (table: unknown) => + table === principalKeyTable + ? { + values: async (values: { publicKey: unknown }) => { + onRecordKey?.(values.publicKey); + }, + } + : (inner.insert?.(table) ?? { values: async () => undefined }), }; } // The read of the active key of an anchor's run principal: @@ -585,7 +625,11 @@ describe("deployCodeSourcedWorkflow", () => { }, }), transaction: (run: (tx: unknown) => Promise) => - run(withRunPrincipalKeys(CAPTURING_DB)), + run( + withRunPrincipalKeys(CAPTURING_DB, (publicKey) => { + capturedRunKey = publicKey; + }), + ), } as unknown as DB["db"]; async function capturePreparedDeployError( @@ -797,14 +841,6 @@ describe("deployCodeSourcedWorkflow", () => { const result = await run( withRunPrincipalKeys( transactionHandle((values) => { - if ("publicKey" in values) { - if ( - values.publicKey !== null && - typeof values.publicKey !== "string" - ) - throw new Error("Invalid key write"); - nextKey = values.publicKey; - } if ("initializationLeaseId" in values) { if ( values.initializationLeaseId !== null && @@ -814,6 +850,10 @@ describe("deployCodeSourcedWorkflow", () => { nextMarker = values.initializationLeaseId; } }), + (key) => { + if (typeof key !== "string") throw new Error("Invalid key"); + nextKey = key; + }, ), ); if (transactions === 1 || scenario !== "rolled back publication") { @@ -1100,7 +1140,11 @@ describe("deployCodeSourcedWorkflow", () => { const db = { ...CAPTURING_DB, transaction: async (run: (handle: typeof tx) => Promise) => - run(withRunPrincipalKeys(tx)), + run( + withRunPrincipalKeys(tx, (publicKey) => + writes.push({ publicKey }), + ), + ), } as unknown as DB["db"]; const allocationRouter = createMockAllocationRouter(); allocationRouter.sendAgentDeployToAllocation = async ( @@ -1136,6 +1180,7 @@ describe("deployCodeSourcedWorkflow", () => { ]); if (!approval.ok) throw new Error("expected approval"); capturedAnchorUpdate = undefined; + capturedRunKey = undefined; const result = await service .deployPreparedCodeSourcedWorkflow({ tenantId: TENANT, @@ -1166,6 +1211,7 @@ describe("deployCodeSourcedWorkflow", () => { expect(writes).toEqual([]); // A rejected reservation must not clear an existing same-token marker. expect(capturedAnchorUpdate).toBeUndefined(); + expect(capturedRunKey).toBeUndefined(); } }); } @@ -1205,7 +1251,11 @@ describe("deployCodeSourcedWorkflow", () => { const db = { ...CAPTURING_DB, transaction: async (run: (handle: typeof tx) => Promise) => - run(withRunPrincipalKeys(tx)), + run( + withRunPrincipalKeys(tx, (publicKey) => + writes.push({ publicKey }), + ), + ), select: () => ({ from: () => ({ where: async () => { @@ -1264,11 +1314,12 @@ describe("deployCodeSourcedWorkflow", () => { expect(writesAtRecheck).toBeDefined(); expect( writesAtRecheck?.some( - (values) => - values.publicKey === "completed-key" && - "credentialRefs" in values, + (values) => values.publicKey === "completed-key", ), ).toBe(true); + expect( + writesAtRecheck?.some((values) => "credentialRefs" in values), + ).toBe(true); const updates = router.calls.filter( (call) => call.method === "sendCredentialsUpdate", @@ -1522,14 +1573,14 @@ describe("deployCodeSourcedWorkflow", () => { }), }), update: () => ({ - set: (values: Record) => ({ - where: () => ({ - returning: async () => { - writes.push(values); - return [{ id: ANCHOR_RUN_ID }]; - }, - }), - }), + set: (values: Record) => { + writes.push(values); + return { + where: () => ({ + returning: async () => [{ id: ANCHOR_RUN_ID }], + }), + }; + }, }), }; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- a transaction double over the capturing DB; allocation-store DB tests cover the SQL guards @@ -1681,6 +1732,7 @@ describe("deployCodeSourcedWorkflow", () => { "inference.source:anthropic:mock-model", ]); capturedAnchorUpdate = undefined; + capturedRunKey = undefined; const error = await service .deployPreparedCodeSourcedWorkflow({ @@ -1703,6 +1755,7 @@ describe("deployCodeSourcedWorkflow", () => { expect(error).toBe(cancelled); expect(allocationRouter.calls).toEqual([]); expect(capturedAnchorUpdate).toBeUndefined(); + expect(capturedRunKey).toBeUndefined(); }); test("a cancelled deploy cannot publish its late key or declare definitive failure", async () => { @@ -1734,6 +1787,7 @@ describe("deployCodeSourcedWorkflow", () => { "inference.source:anthropic:mock-model", ]); capturedAnchorUpdate = undefined; + capturedRunKey = undefined; const error = await service .deployPreparedCodeSourcedWorkflow({ @@ -1760,6 +1814,7 @@ describe("deployCodeSourcedWorkflow", () => { expect(error.leakedAgent).toBe(true); expect(error.cause).toBe(cancelled); expect(capturedAnchorUpdate).toBeUndefined(); + expect(capturedRunKey).toBeUndefined(); expect(sidecarRouter.calls.map((call) => call.method)).toEqual([ "noteSenderDeployStarted", ]); @@ -1844,6 +1899,7 @@ describe("deployCodeSourcedWorkflow", () => { }); test("builds a self-consistent source-ref frame that binds to the gate's frozen hash and inert projection", async () => { + capturedRunKey = undefined; const mockRouter = createMockRouter(); const sentWorkflows: Parameters[2][] = []; @@ -1897,18 +1953,64 @@ describe("deployCodeSourcedWorkflow", () => { // The anchor workflow_run row is born "deployed" (live but pre-trigger) and // self-referential (anchorRunId === id); its address is the run address - // derived from the anchor run id. It is inserted with a NULL public key -- - // the row must exist before the frame, but the key is only known from the - // ack -- and the key is stamped by a follow-up update once the ack returns. + // derived from the anchor run id. The row must exist before the frame, but + // the key is only known from the ack, so the key is recorded on the run + // principal once the ack returns. expect(capturedAnchorRow).toBeDefined(); expect(capturedAnchorRow?.status).toBe("deployed"); expect(capturedAnchorRow?.id).toBe(ANCHOR_RUN_ID); expect(capturedAnchorRow?.anchorRunId).toBe(ANCHOR_RUN_ID); expect(capturedAnchorRow?.address).toBe(DEPLOY_ADDRESS); - expect(capturedAnchorRow?.publicKey).toBeNull(); - expect(capturedAnchorUpdate).toEqual({ - publicKey: "ed25519-supervisor-pubkey", - }); + expect(capturedRunKey).toBe("ed25519-supervisor-pubkey"); + }); + + test("a deploy whose acked key cannot be recorded fails as a possibly leaked launch", async () => { + const mockRouter = createMockRouter(); + mockRouter.sendAgentDeploy = (() => + Promise.resolve({ + publicKey: "ed25519-supervisor-pubkey", + })) as TestSidecarRouter["sendAgentDeploy"]; + // The anchor insert commits; the key record after the ack fails. + let transactions = 0; + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- the capturing double with a failing second transaction + const db = { + ...CAPTURING_DB, + transaction: (run: (tx: unknown) => Promise) => { + transactions += 1; + if (transactions > 1) { + return Promise.reject(new Error("connection lost")); + } + return run(withRunPrincipalKeys(CAPTURING_DB)); + }, + } as unknown as DB["db"]; + + const { deployCodeSourcedWorkflow } = await import("./session-service"); + const { approval, projection, closure } = await makeApproveOutput([ + "tool:read", + ]); + if (!approval.ok) throw new Error("expected approval"); + + const error = await deployCodeSourcedWorkflow({ + sidecarAllocationRouter: createMockAllocationRouter(mockRouter), + allocationTarget: { allocationId: "alloc-test", generation: 1 }, + agentAddress: DEPLOY_ADDRESS, + config: CONFIG, + sources: SOURCES, + approved: { approval, projection, closure }, + source: SOURCE, + db, + tenantId: TENANT, + anchorRunId: ANCHOR_RUN_ID, + deploymentDomain: DEPLOYMENT_DOMAIN, + credentialCipher: createNoopCredentialCipher(), + }).catch((cause: unknown) => cause); + + expect(error).toBeInstanceOf(SessionLaunchError); + if (!(error instanceof SessionLaunchError)) { + throw new Error("expected SessionLaunchError"); + } + expect(error.leakedAgent).toBe(true); + expect(error.message).toBe("connection lost"); }); test("delivers no credential for a top-level step that cannot invoke inference", async () => { diff --git a/packages/hub-sessions/src/session-service.ts b/packages/hub-sessions/src/session-service.ts index 2ce1d2966..814cb226d 100644 --- a/packages/hub-sessions/src/session-service.ts +++ b/packages/hub-sessions/src/session-service.ts @@ -667,10 +667,11 @@ async function prepareSourceRefDeploy( /** * Prepare then emit the source-ref deploy frame, for the prepared provisioned * path whose anchor `workflow_run` row already exists (inserted at prepare - * time). It emits the frame but does NOT touch the anchor row: the caller - * (`deployPreparedCodeSourcedWorkflow`) stamps the acked key under the - * allocation-ownership lock. A tagged `DeployFrameFailure` is converted to the - * `SessionLaunchError` disposition the allocation reconciler consumes, while + * time). It emits the frame but does NOT record the deployment: the caller + * (`deployPreparedCodeSourcedWorkflow`) records the acked key on the run + * principal under the allocation-ownership lock. A tagged `DeployFrameFailure` + * is converted to the `SessionLaunchError` disposition the allocation + * reconciler consumes, while * untagged preparation errors remain safe same-generation retries. The ordinary * path does NOT use this wrapper -- it must interleave the anchor INSERT between * prepare and emit, so it drives `prepareSourceRefDeploy` and @@ -845,10 +846,11 @@ async function emitSourceRefDeployFrame( * A direct allocation-bound composition entrypoint for tests and low-level * callers: prepare, INSERT the deployment's anchor `workflow_run` row, THEN emit * the source-ref frame. The anchor row is the deployment's first-class record - * that owns its routing address and public key. Run-grant materialization keys - * off this row (address + live status), so WITHOUT it no per-run grants (tool, - * capability, OR credential) ever materialize for a source-ref deployment. Born - * "deployed" (live but pre-trigger) with a null public key: the first trigger's + * that owns its routing address; its run principal holds its public key. + * Run-grant materialization keys off this row (address + live status), so + * WITHOUT it no per-run grants (tool, capability, OR credential) ever + * materialize for a source-ref deployment. Born + * "deployed" (live but pre-trigger) with no key yet: the first trigger's * materialization flips it to "running" via `anchorWithPrincipal`'s guarded * update, which a row born "running" would skip. Its `anchorRunId` equals its * own id, so the anchor references itself. The deployer read grant is deferred @@ -900,7 +902,6 @@ export async function deployCodeSourcedWorkflow( anchorRunId: args.anchorRunId, definitionId, address: args.agentAddress, - publicKey: null, status: "deployed", createdAt, // Persist the non-secret shape of the delivery so the reconnect resync @@ -931,7 +932,7 @@ export async function deployCodeSourcedWorkflow( } catch (cause) { if (isDeployFrameFailure(cause) && cause.frameSent === false) { // Positive proof the frame never reached the wire: nothing spawned, so - // fully roll the anchor back. The guard (`deployed`, null key) is a + // fully roll the anchor back. The guard (`deployed`, no key) is a // tripwire on the `frameSent: false` contract -- a 0-row delete means the // row advanced or vanished, so the contract lied and a child may be live; // surface that loudly and refuse to claim it is safe to roll back. The @@ -968,7 +969,7 @@ export async function deployCodeSourcedWorkflow( return rows; }); if (deleted.length === 0) { - logger.error`anchor-before-frame rollback found no deployed/null-key row for ${args.anchorRunId} after a frameSent:false failure; the never-sent contract was violated and a child may be live`; + logger.error`anchor-before-frame rollback found no deployed row without a key for ${args.anchorRunId} after a frameSent:false failure; the never-sent contract was violated and a child may be live`; throw new SessionLaunchError(cause, true); } throw new SessionLaunchError(cause, false); @@ -1021,36 +1022,23 @@ export async function deployCodeSourcedWorkflow( throw new SessionLaunchError(cause, true); } - // Emit succeeded: stamp the acked key. No status guard -- the key is a fact - // regardless of whether the pack-ack race already flipped the row to - // "running", and skipping the stamp there would strand a live run with a null - // key. A 0-row update is an anomaly (nothing should remove a deployed anchor - // on the success path), but the deploy succeeded, so log it rather than - // failing a live run. The update holds the anchor row lock, which - // serializes recording the key on the run principal. - const stamped = await args.db.transaction(async (tx) => { - const rows = await tx - .update(workflowRunTable) - .set({ publicKey }) - .where( - and( - eq(workflowRunTable.id, args.anchorRunId), - eq(workflowRunTable.anchorRunId, args.anchorRunId), - eq(workflowRunTable.tenantId, args.tenantId), - ), - ) - .returning({ id: workflowRunTable.id }); - if (rows.length > 0) { - await recordRunPrincipalKey( - tx, - args.tenantId, - args.anchorRunId, - publicKey, - ); - } - return rows; - }); - if (stamped.length === 0) { + // Emit succeeded: record the acked key on the run principal. No status guard + // -- the key is a fact regardless of whether the pack-ack race already + // flipped the row to "running", and skipping it there would strand a live + // run with no key. A missing anchor is an anomaly (nothing should remove a + // deployed anchor on the success path), but the deploy succeeded, so log it + // rather than failing a live run. Any other failure to record the key leaves + // a live agent whose key the hub does not hold, so it fails as a launch that + // may have leaked its agent. + let stamped: boolean; + try { + stamped = await args.db.transaction((tx) => + recordRunPrincipalKey(tx, args.tenantId, args.anchorRunId, publicKey), + ); + } catch (err) { + throw new SessionLaunchError(err, true); + } + if (!stamped) { logger.error`anchor-before-frame: anchor ${args.anchorRunId} vanished before its public key could be stamped on a successful deploy`; } @@ -1338,16 +1326,17 @@ export function createSessionService( } /** - * Update a prepared anchor run's `publicKey` under the allocation-ownership - * lock. The anchor row was inserted at prepare time; this stamps the - * supervisor key returned by the deploy ack, but only while the allocation + * Record a prepared deployment's key on its run principal under the + * allocation-ownership lock. The anchor row and run principal were inserted + * at prepare time; this records the supervisor key returned by the deploy + * ack, but only while the allocation * still names this exact accepted generation and unexpired reconciliation * lease for this anchor. Lost ownership or cancellation fails closed as a * leaked-agent `SessionLaunchError` -- the deploy already reached the sidecar, * so the caller must treat the sidecar agent as possibly live. Used by the * `deployPreparedCodeSourcedWorkflow` prepared path. */ - async function updateAnchorPublicKeyUnderAllocationLock(args: { + async function recordRunKeyUnderAllocationLock(args: { tenantId: string; anchorRunId: string; allocationTarget: AllocatedSidecarTarget; @@ -1356,9 +1345,7 @@ export function createSessionService( credentialRefs?: WorkflowRunCredentialRefs; }): Promise { if (db === undefined) { - throw new Error( - "updateAnchorPublicKeyUnderAllocationLock requires a db handle", - ); + throw new Error("recordRunKeyUnderAllocationLock requires a db handle"); } const dbHandle = db; try { @@ -1472,7 +1459,7 @@ export function createSessionService( try { // Bracket the allocated pre-ack window: mark the sender's key-record as // mid-flight before the deploy emit so a run that sends mail before its - // anchor key is committed parks rather than delivering keyless. The settle + // key is committed parks rather than delivering keyless. The settle // follows durable completion; cancellation leaves the outcome to recovery. if (source.kind === "asset") { if (resolveAttachment === null) { @@ -1491,7 +1478,7 @@ export function createSessionService( ); } - await updateAnchorPublicKeyUnderAllocationLock({ + await recordRunKeyUnderAllocationLock({ tenantId: params.tenantId, anchorRunId: params.anchorRunId, allocationTarget: params.allocationTarget, @@ -1502,7 +1489,7 @@ export function createSessionService( : {}), }); - // The anchor's public key is now durable. Wake any mail the run parked + // The run principal's key is now durable. Wake any mail the run parked // while pre-ack so it delivers with the sender key co-delivered, closing // the window where a run sends before its key is recorded. The write above // happens-before this settle, so a re-drive resolves the recorded key. diff --git a/packages/hub-sessions/src/sidecar-allocation/contracts.ts b/packages/hub-sessions/src/sidecar-allocation/contracts.ts index c84479d84..9c7eb2918 100644 --- a/packages/hub-sessions/src/sidecar-allocation/contracts.ts +++ b/packages/hub-sessions/src/sidecar-allocation/contracts.ts @@ -209,11 +209,11 @@ export type SidecarCredentials = { /** * `reclaim` also requires the deployment's first deploy to have completed: its - * anchor's key is committed and no initialization is in flight. A copy whose - * deploy is still uncertain is undeployed instead, since releasing it on a - * sidecar that hosts other work does not stop the sidecar. It further requires - * the anchor run not to be terminal: a copy of a run that has ended is not - * routed, even when its own history never recorded the end. + * run principal holds the deployment's key and no initialization is in flight. + * A copy whose deploy is still uncertain is undeployed instead, since + * releasing it on a sidecar that hosts other work does not stop the sidecar. It + * further requires the anchor run not to be terminal: a copy of a run that has + * ended is not routed, even when its own history never recorded the end. * * `retention` permits retaining an initialized terminal copy while its capacity * retention period remains open. `retention-transition` validates terminal diff --git a/packages/hub-sessions/src/ws/sidecar-handler.ts b/packages/hub-sessions/src/ws/sidecar-handler.ts index 9cb77d7e5..863c2e817 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.ts @@ -387,7 +387,7 @@ export type SidecarRouter = { ): void; /** * Mark a run-address sender's ALLOCATED deploy as mid-flight, before the deploy - * emit and its anchor-key update. An allocated run records its key later than + * emit and its key record. An allocated run records its key later than * the deploy ack clears `pendingDeploys`, so this marker covers the allocated * pre-ack window that `pendingDeploys` alone under-covers. `noteSenderDeploySettled` * clears it only when the durable outcome is known. Cancellation leaves it @@ -961,7 +961,7 @@ export function createSidecarRouter( >(); // Run addresses whose ALLOCATED deploy is mid-flight -- key-record has been // started but not yet committed. pendingDeploys clears at the deploy ack, but an - // allocated run's key is recorded LATER by session-service's anchor-key update, + // allocated run's key is recorded LATER, on its run principal, by session-service, // so pendingDeploys alone under-covers the allocated pre-ack window. session- // service brackets this marker across its deploy try/catch: set before the // deploy emit, cleared by noteSenderDeploySettled on record or failure. diff --git a/tests/db/anchor-before-frame.test.ts b/tests/db/anchor-before-frame.test.ts index f8dd2c034..86b512a1c 100644 --- a/tests/db/anchor-before-frame.test.ts +++ b/tests/db/anchor-before-frame.test.ts @@ -36,7 +36,10 @@ import { eq } from "drizzle-orm"; import { defineAgent } from "@intx/agent"; import { DeploymentRejectedError } from "@intx/types/sidecar"; import { createNoopCredentialCipher, generateKeyPair } from "@intx/crypto"; -import { createSidecarAllocationStore } from "@intx/db"; +import { + createSidecarAllocationStore, + runPrincipalActiveKeySql, +} from "@intx/db"; import { principal, sidecarAllocation, @@ -295,7 +298,10 @@ describe.skipIf(!harnessDbEnvAvailable())( const [row] = await h.db .select({ status: workflowRun.status, - publicKey: workflowRun.publicKey, + publicKey: runPrincipalActiveKeySql( + workflowRun.tenantId, + workflowRun.id, + ), }) .from(workflowRun) .where(eq(workflowRun.id, ANCHOR_RUN_ID)) @@ -380,11 +386,14 @@ describe.skipIf(!harnessDbEnvAvailable())( // did not exist yet, so the gate returned path_violation. expect(probeAccepted).toBe(true); - // The successful ack stamped the key onto the now-live anchor. + // The successful ack recorded the key on the anchor's run principal. const [row] = await h.db .select({ status: workflowRun.status, - publicKey: workflowRun.publicKey, + publicKey: runPrincipalActiveKeySql( + workflowRun.tenantId, + workflowRun.id, + ), }) .from(workflowRun) .where(eq(workflowRun.id, ANCHOR_RUN_ID)) @@ -612,11 +621,11 @@ describe.skipIf(!harnessDbEnvAvailable())( expect((await readAnchor())?.status).toBe("running"); }); - test("receiveWorkflowRunPack accepts a deployed anchor whose public key is still null", async () => { - // Pins the gate's publicKey-agnosticism: the anchor-before-frame window is - // a live "deployed" row with a null key, and the gate must accept a pack - // against it. A future "require publicKey on the anchor" change turns this - // red rather than silently re-breaking deploy bootstrap. + test("receiveWorkflowRunPack accepts a deployed anchor that has no key yet", async () => { + // Pins the gate's key-agnosticism: the anchor-before-frame window is a + // live "deployed" row whose run principal has no key yet, and the gate + // must accept a pack against it. A future "require a key" change turns + // this red rather than silently re-breaking deploy bootstrap. await seedWorkflowRun(h.db, { id: ANCHOR_RUN_ID, anchorRunId: ANCHOR_RUN_ID, diff --git a/tests/db/mail-triggered-run-grants.test.ts b/tests/db/mail-triggered-run-grants.test.ts index caabc0fde..101013e72 100644 --- a/tests/db/mail-triggered-run-grants.test.ts +++ b/tests/db/mail-triggered-run-grants.test.ts @@ -1232,7 +1232,7 @@ describe.skipIf(!harnessDbEnvAvailable())( }); // The blocker plays the winner: under the anchor lock it commits the // run principal with the winner's accept-grant and the run row linked - // to it, and it clears the anchor's key so credential resolution is + // to it, and it retires the deployment's key so credential resolution is // not ready afterwards. The loser must be gated as a delivery to the // live run, not parked. const locked = Promise.withResolvers(); diff --git a/tests/db/migration-0121-run-principal-sidecar-keys.test.ts b/tests/db/migration-0121-run-principal-sidecar-keys.test.ts index f3be22438..88fe9dd2c 100644 --- a/tests/db/migration-0121-run-principal-sidecar-keys.test.ts +++ b/tests/db/migration-0121-run-principal-sidecar-keys.test.ts @@ -15,7 +15,7 @@ import { import { asc, eq, sql } from "drizzle-orm"; import { createPrincipalKeyStore, recordPublicKey } from "@intx/db"; -import { principalKey, workflowRun } from "@intx/db/schema"; +import { principalKey } from "@intx/db/schema"; import { createNoopCredentialCipher } from "@intx/crypto"; import { createTestDb, @@ -51,9 +51,21 @@ describe.skipIf(!harnessDbEnvAvailable())( }); beforeEach(async () => { await h.reset(); + // A later migration drops the anchor's key column; restore it to the + // shape this migration reads. + await h.db.execute( + sql`alter table workflow_run add column if not exists public_key text`, + ); await seedTenants(h.db, [{ id: TENANT }]); }); + // The key the deploy ack recorded on the anchor before this migration. + async function setAnchorKey(runId: string, publicKey: string | null) { + await h.db.execute( + sql`update workflow_run set public_key = ${publicKey} where id = ${runId}`, + ); + } + // An anchor run and the run principal deploy created, with the key the // deploy ack recorded only on the anchor, as before the migration. async function seedAnchor( @@ -67,10 +79,7 @@ describe.skipIf(!harnessDbEnvAvailable())( tenantId: TENANT, status, }); - await h.db - .update(workflowRun) - .set({ publicKey }) - .where(eq(workflowRun.id, runId)); + await setAnchorKey(runId, publicKey); const principalId = `prn_${runId}`; await seedPrincipal(h.db, { id: principalId, @@ -183,10 +192,7 @@ describe.skipIf(!harnessDbEnvAvailable())( anchorRunId: "run_orphan", tenantId: TENANT, }); - await h.db - .update(workflowRun) - .set({ publicKey: KEY_A }) - .where(eq(workflowRun.id, "run_orphan")); + await setAnchorKey("run_orphan", KEY_A); await expect(replay()).rejects.toThrow(/has no run principal/); }); @@ -198,10 +204,7 @@ describe.skipIf(!harnessDbEnvAvailable())( anchorRunId: "run_parent", tenantId: TENANT, }); - await h.db - .update(workflowRun) - .set({ publicKey: KEY_A }) - .where(eq(workflowRun.id, "run_child")); + await setAnchorKey("run_child", KEY_A); await expect(replay()).rejects.toThrow(/not a deployment anchor/); }); diff --git a/tests/db/migration-0122-drop-workflow-run-public-key.test.ts b/tests/db/migration-0122-drop-workflow-run-public-key.test.ts new file mode 100644 index 000000000..e82e94d3c --- /dev/null +++ b/tests/db/migration-0122-drop-workflow-run-public-key.test.ts @@ -0,0 +1,242 @@ +// Migration 0122 drops the deployment anchor's copy of its key once it agrees +// with the run principal, so this replays it over seeded rows. + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; +import { asc, eq, sql } from "drizzle-orm"; + +import { + loadRunPrincipalActiveKey, + recordPublicKey, + recordRunPrincipalKey, + retireRunPrincipalKey, +} from "@intx/db"; +import { principalKey } from "@intx/db/schema"; +import { + createTestDb, + harnessDbEnvAvailable, + type TestDb, +} from "@intx/test-harness/db-harness"; +import { + seedPrincipal, + seedTenants, + seedWorkflowRun, +} from "@intx/test-harness/seed"; + +const MIGRATION_SQL = readFileSync( + join( + import.meta.dir, + "../../packages/db/migrations/0122_drop_workflow_run_public_key.sql", + ), + "utf8", +); +const TENANT = "tnt-migration-0122"; +const KEY_A = "aa".repeat(32); +const KEY_B = "bb".repeat(32); + +describe.skipIf(!harnessDbEnvAvailable())( + "migration 0122 drop workflow_run public_key", + () => { + let h: TestDb; + beforeAll(async () => { + h = await createTestDb(); + }); + afterAll(async () => { + await h.close(); + }); + beforeEach(async () => { + await h.reset(); + // Restore the column this migration drops. + await h.db.execute( + sql`alter table workflow_run add column if not exists public_key text`, + ); + await seedTenants(h.db, [{ id: TENANT }]); + }); + + // An anchor run and its run principal, with `anchorKey` on the anchor's + // column and `principalKey` recorded on the run principal. + async function seedAnchor( + runId: string, + keys: { anchorKey: string | null; principalKey: string | null }, + ): Promise { + await seedWorkflowRun(h.db, { + id: runId, + anchorRunId: runId, + tenantId: TENANT, + }); + await h.db.execute( + sql`update workflow_run set public_key = ${keys.anchorKey} where id = ${runId}`, + ); + const principalId = `prn_${runId}`; + await seedPrincipal(h.db, { + id: principalId, + tenantId: TENANT, + kind: "workflow", + refId: runId, + status: "active", + }); + if (keys.principalKey !== null) { + await recordPublicKey(h.db, principalId, keys.principalKey); + } + return principalId; + } + + async function keysOf(principalId: string) { + return h.db + .select({ + publicKey: principalKey.publicKey, + status: principalKey.status, + }) + .from(principalKey) + .where(eq(principalKey.principalId, principalId)) + .orderBy(asc(principalKey.createdAt)); + } + + async function columnExists(): Promise { + const rows = await h.db.execute( + sql`select 1 from information_schema.columns where table_schema = current_schema() and table_name = 'workflow_run' and column_name = 'public_key'`, + ); + return rows.length > 0; + } + + // The query builder returns a thenable; adopt it so a rejection reaches + // the assertion. + function replay(): Promise { + return Promise.resolve(h.db.execute(sql.raw(MIGRATION_SQL))); + } + + async function waitForBlocked(blockerPid: number) { + // Postgres exposes lock waits through its activity view only. + for (;;) { + const waiting = await h.db.execute( + sql`select pid from pg_stat_activity where ${blockerPid} = any(pg_blocking_pids(pid))`, + ); + if (waiting.length > 0) return; + await new Promise((resolve) => setImmediate(resolve)); + } + } + + test("waits for a hub transaction that read a run and then records a key", async () => { + await seedAnchor("run_live", { anchorKey: null, principalKey: null }); + const readDone = Promise.withResolvers(); + const write = Promise.withResolvers(); + // A hub transaction that reads a run and then records its key, as an + // initialization completion does. + const hub = h.db.transaction(async (tx) => { + await tx.execute(sql`select 1 from workflow_run`); + const [backend] = await tx.execute(sql`select pg_backend_pid() as pid`); + const pid = backend?.["pid"]; + if (typeof pid !== "number") throw new Error("Missing backend PID"); + readDone.resolve(pid); + await write.promise; + await tx.execute( + sql`update workflow_run set public_key = ${KEY_A} where id = 'run_live'`, + ); + return recordRunPrincipalKey(tx, TENANT, "run_live", KEY_A); + }); + void hub.catch(readDone.reject); + const hubPid = await readDone.promise; + + const migration = replay(); + await waitForBlocked(hubPid); + write.resolve(undefined); + + expect(await hub).toBe(true); + await migration; + expect(await columnExists()).toBe(false); + expect(await loadRunPrincipalActiveKey(h.db, TENANT, "run_live")).toBe( + KEY_A, + ); + }); + + test("does not wait for a hub transaction that changed a principal and then reads a run", async () => { + await seedAnchor("run_live", { anchorKey: null, principalKey: null }); + const changed = Promise.withResolvers(); + const read = Promise.withResolvers(); + // A hub transaction that changes a principal and then reads runs, as a + // principal status change re-derives the grants of the runs it delegated. + const hub = h.db.transaction(async (tx) => { + await tx.execute( + sql`update principal set status = 'active' where id = 'prn_run_live'`, + ); + changed.resolve(undefined); + await read.promise; + await tx.execute(sql`select 1 from workflow_run`); + }); + void hub.catch(changed.reject); + await changed.promise; + + await replay(); + read.resolve(undefined); + await hub; + expect(await columnExists()).toBe(false); + }); + + test("drops the column when every anchor agrees with its run principal", async () => { + const keyed = await seedAnchor("run_keyed", { + anchorKey: KEY_A, + principalKey: KEY_A, + }); + const unkeyed = await seedAnchor("run_unkeyed", { + anchorKey: null, + principalKey: null, + }); + // A replaced sidecar's key, retired on both. + const replaced = await seedAnchor("run_replaced", { + anchorKey: null, + principalKey: KEY_B, + }); + await retireRunPrincipalKey(h.db, TENANT, "run_replaced"); + + await replay(); + + expect(await columnExists()).toBe(false); + expect(await keysOf(keyed)).toEqual([ + { publicKey: KEY_A, status: "active" }, + ]); + expect(await keysOf(unkeyed)).toEqual([]); + expect(await keysOf(replaced)).toEqual([ + { publicKey: KEY_B, status: "retired" }, + ]); + }); + + test("refuses an anchor key its run principal does not hold as active", async () => { + await seedAnchor("run_drifted", { + anchorKey: KEY_A, + principalKey: KEY_B, + }); + + await expect(replay()).rejects.toThrow(/does not hold as active/); + expect(await columnExists()).toBe(true); + }); + + test("refuses an anchor key its run principal holds only as retired", async () => { + await seedAnchor("run_retired", { + anchorKey: KEY_A, + principalKey: KEY_A, + }); + await retireRunPrincipalKey(h.db, TENANT, "run_retired"); + + await expect(replay()).rejects.toThrow(/does not hold as active/); + expect(await columnExists()).toBe(true); + }); + + test("refuses an anchor with no key whose run principal holds an active key", async () => { + await seedAnchor("run_cleared", { anchorKey: null, principalKey: KEY_A }); + + await expect(replay()).rejects.toThrow( + /without a key has a run principal/, + ); + expect(await columnExists()).toBe(true); + }); + }, +); diff --git a/tests/db/run-principal-key.test.ts b/tests/db/run-principal-key.test.ts index b1cc9c157..0139ff6fc 100644 --- a/tests/db/run-principal-key.test.ts +++ b/tests/db/run-principal-key.test.ts @@ -6,9 +6,11 @@ import { expect, test, } from "bun:test"; -import { asc, eq } from "drizzle-orm"; +import { and, asc, eq, sql } from "drizzle-orm"; import { + loadRunPrincipalActiveKey, + recordPublicKey, recordRunPrincipalKey, retireRunPrincipalKey, runPrincipalActiveKeySql, @@ -52,6 +54,11 @@ describe.skipIf(!harnessDbEnvAvailable())( beforeEach(async () => { await h.reset(); await seedTenants(h.db, [{ id: TENANT }]); + await seedWorkflowRun(h.db, { + id: RUN_ID, + tenantId: TENANT, + anchorRunId: RUN_ID, + }); await seedPrincipal(h.db, { id: RUN_PRINCIPAL, tenantId: TENANT, @@ -72,6 +79,53 @@ describe.skipIf(!harnessDbEnvAvailable())( .orderBy(asc(principalKey.publicKey)); } + async function waitForBlocked(blockerPid: number) { + // Postgres exposes lock waits through its activity view only. + for (;;) { + const waiting = await h.db.execute( + sql`select pid from pg_stat_activity where ${blockerPid} = any(pg_blocking_pids(pid))`, + ); + if (waiting.length > 0) return; + await new Promise((resolve) => setImmediate(resolve)); + } + } + + test("recording a key waits for a transaction that holds the deployment anchor", async () => { + const locked = Promise.withResolvers(); + const release = Promise.withResolvers(); + // A reader that must not race a key writer, as the first grant commit + // locks the anchor and then reads whether the deployment has a key. + const reader = h.db.transaction(async (tx) => { + await tx + .select({ id: workflowRun.id }) + .from(workflowRun) + .where( + and(eq(workflowRun.id, RUN_ID), eq(workflowRun.tenantId, TENANT)), + ) + .for("update"); + const [backend] = await tx.execute(sql`select pg_backend_pid() as pid`); + const pid = backend?.["pid"]; + if (typeof pid !== "number") throw new Error("Missing backend PID"); + locked.resolve(pid); + await release.promise; + return loadRunPrincipalActiveKey(tx, TENANT, RUN_ID); + }); + void reader.catch(locked.reject); + const readerPid = await locked.promise; + + const writer = h.db.transaction((tx) => + recordRunPrincipalKey(tx, TENANT, RUN_ID, FIRST_KEY), + ); + await waitForBlocked(readerPid); + release.resolve(undefined); + + expect(await reader).toBeNull(); + expect(await writer).toBe(true); + expect(await keys()).toEqual([ + { publicKey: FIRST_KEY, status: "active" }, + ]); + }); + test("a replacement retires the key and records its own", async () => { await recordRunPrincipalKey(h.db, TENANT, RUN_ID, FIRST_KEY); await retireRunPrincipalKey(h.db, TENANT, RUN_ID); @@ -96,7 +150,7 @@ describe.skipIf(!harnessDbEnvAvailable())( test("a query over many runs reads each run's own active key", async () => { const OTHER_TENANT = "tnt_rpk_other"; await seedTenants(h.db, [{ id: OTHER_TENANT }]); - for (const id of [RUN_ID, "run_retired", "run_unkeyed", "run_foreign"]) { + for (const id of ["run_retired", "run_unkeyed", "run_foreign"]) { await seedWorkflowRun(h.db, { id, tenantId: TENANT, anchorRunId: id }); } await recordRunPrincipalKey(h.db, TENANT, RUN_ID, FIRST_KEY); @@ -104,12 +158,14 @@ describe.skipIf(!harnessDbEnvAvailable())( await retireRunPrincipalKey(h.db, TENANT, "run_retired"); // A run principal with the same run id in another tenant is not the // run's principal. - await seedRunPrincipalKey( - h.db, - OTHER_TENANT, - "run_foreign", - "cc".repeat(32), - ); + await seedPrincipal(h.db, { + id: "prn_foreign", + tenantId: OTHER_TENANT, + kind: "workflow", + refId: "run_foreign", + status: "active", + }); + await recordPublicKey(h.db, "prn_foreign", "cc".repeat(32)); // Joined, as the run list reads runs, so the subqueries' outer columns // are qualified against more than one table in scope. @@ -141,12 +197,31 @@ describe.skipIf(!harnessDbEnvAvailable())( ]); }); - test("a run with no run principal cannot record a key and has none to retire", async () => { - await expect( - recordRunPrincipalKey(h.db, TENANT, "run_missing", FIRST_KEY), - ).rejects.toThrow(/has no run principal/); + test("a run that is not a deployment anchor records no key and has none to retire", async () => { + expect( + await recordRunPrincipalKey(h.db, TENANT, "run_missing", FIRST_KEY), + ).toBe(false); + await seedWorkflowRun(h.db, { + id: "run_child", + tenantId: TENANT, + anchorRunId: RUN_ID, + }); + expect( + await recordRunPrincipalKey(h.db, TENANT, "run_child", FIRST_KEY), + ).toBe(false); await retireRunPrincipalKey(h.db, TENANT, "run_missing"); expect(await keys()).toEqual([]); }); + + test("a deployment anchor with no run principal cannot record a key", async () => { + await seedWorkflowRun(h.db, { + id: "run_orphan", + tenantId: TENANT, + anchorRunId: "run_orphan", + }); + await expect( + recordRunPrincipalKey(h.db, TENANT, "run_orphan", FIRST_KEY), + ).rejects.toThrow(/has no run principal/); + }); }, ); diff --git a/tests/db/sidecar-allocation-store.test.ts b/tests/db/sidecar-allocation-store.test.ts index 118800918..270482929 100644 --- a/tests/db/sidecar-allocation-store.test.ts +++ b/tests/db/sidecar-allocation-store.test.ts @@ -29,6 +29,7 @@ import { SIDECAR_CLEANUP_PREPARATION_TIMEOUT_FAILURE_CODE, SIDECAR_CLEANUP_OPERATOR_RELEASED_FAILURE_CODE, SidecarReuseRejectedError, + loadRunPrincipalActiveKey, recordPublicKey, type RetainedSidecarIncarnation, } from "@intx/db"; @@ -75,16 +76,17 @@ describe.skipIf(!harnessDbEnvAvailable())( await h.close(); }); - // Record the key a completed initialization leaves: on the anchor and as - // the active key of its run principal. + // Record the key a completed initialization leaves: the active key of the + // anchor's run principal. async function recordAnchorKey(publicKey: string) { - await h.db - .update(workflowRun) - .set({ publicKey }) - .where(eq(workflowRun.id, ANCHOR_RUN_ID)); await recordPublicKey(h.db, ANCHOR_RUN_PRINCIPAL_ID, publicKey); } + // The active key of the anchor's run principal, or null. + async function anchorKey() { + return loadRunPrincipalActiveKey(h.db, TENANT_ID, ANCHOR_RUN_ID); + } + // The keys of the anchor's run principal, oldest first. async function runPrincipalKeys() { return h.db @@ -1100,11 +1102,7 @@ describe.skipIf(!harnessDbEnvAvailable())( const deadline = parked?.connectDeadline; if (deadline === undefined) throw new Error("Missing cleanup deadline"); expect(parked?.status).toBe("replacing"); - expect( - await h.db.query.workflowRun.findFirst({ - where: eq(workflowRun.id, ANCHOR_RUN_ID), - }), - ).toMatchObject({ publicKey: null }); + expect(await anchorKey()).toBeNull(); state.now = new Date(deadline.getTime() - 1); const releasing = await store.beginRelease({ @@ -1344,7 +1342,8 @@ describe.skipIf(!harnessDbEnvAvailable())( await h.db.query.workflowRun.findFirst({ where: eq(workflowRun.id, ANCHOR_RUN_ID), }), - ).toMatchObject({ status: "running", publicKey: "restored-key" }); + ).toMatchObject({ status: "running" }); + expect(await anchorKey()).toBe("restored-key"); connected = false; const disconnected = await store.markConnectionLost({ allocationId: current.id, @@ -1365,11 +1364,7 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(replacing?.nextAttemptAt?.getTime()).toBe( state.now.getTime() + 1_000, ); - expect( - await h.db.query.workflowRun.findFirst({ - where: eq(workflowRun.id, ANCHOR_RUN_ID), - }), - ).toMatchObject({ publicKey: null }); + expect(await anchorKey()).toBeNull(); connected = true; } const replacing = await store.findById(target.allocationId); @@ -3452,13 +3447,7 @@ describe.skipIf(!harnessDbEnvAvailable())( expect((await store.findById(allocation.id))?.initializationLeaseId).toBe( leaseId, ); - expect( - ( - await h.db.query.workflowRun.findFirst({ - where: eq(workflowRun.id, ANCHOR_RUN_ID), - }) - )?.publicKey, - ).toBeNull(); + expect(await anchorKey()).toBeNull(); expect( await store.markConnectionReady({ allocationId: allocation.id, @@ -3485,7 +3474,8 @@ describe.skipIf(!harnessDbEnvAvailable())( await h.db.query.workflowRun.findFirst({ where: eq(workflowRun.id, ANCHOR_RUN_ID), }), - ).toMatchObject({ publicKey: "new-key", credentialRefs }); + ).toMatchObject({ credentialRefs }); + expect(await anchorKey()).toBe("new-key"); // The same completion makes the key the run principal's, public-only. expect(await runPrincipalKeys()).toEqual([ { publicKey: "new-key", status: "active" }, @@ -3515,13 +3505,7 @@ describe.skipIf(!harnessDbEnvAvailable())( }), ).rejects.toThrow(/belongs to principal/); - expect( - ( - await h.db.query.workflowRun.findFirst({ - where: eq(workflowRun.id, ANCHOR_RUN_ID), - }) - )?.publicKey, - ).toBeNull(); + expect(await anchorKey()).toBeNull(); expect(await runPrincipalKeys()).toEqual([]); expect((await store.findById(allocation.id))?.initializationLeaseId).toBe( leaseId, @@ -3624,11 +3608,7 @@ describe.skipIf(!harnessDbEnvAvailable())( status: "allocated", generation: allocation.generation, }); - expect( - await h.db.query.workflowRun.findFirst({ - where: eq(workflowRun.id, ANCHOR_RUN_ID), - }), - ).toMatchObject({ publicKey: "new-key" }); + expect(await anchorKey()).toBe("new-key"); }); test("unsent rollback clears its marker after lease loss without overwriting a later completion", async () => { @@ -3673,11 +3653,7 @@ describe.skipIf(!harnessDbEnvAvailable())( await store.completeInitialization({ ...next, publicKey: "new-key" }), ).toBe(true); expect(await store.clearUnsentInitialization(rollback)).toBe(false); - expect( - await h.db.query.workflowRun.findFirst({ - where: eq(workflowRun.id, ANCHOR_RUN_ID), - }), - ).toMatchObject({ publicKey: "new-key" }); + expect(await anchorKey()).toBe("new-key"); }); for (const nextAttempt of [false, true]) { @@ -3726,7 +3702,8 @@ describe.skipIf(!harnessDbEnvAvailable())( await h.db.query.workflowRun.findFirst({ where: eq(workflowRun.id, ANCHOR_RUN_ID), }), - ).toMatchObject({ status: "running", publicKey: null }); + ).toMatchObject({ status: "running" }); + expect(await anchorKey()).toBeNull(); if (nextAttempt) { const currentCleanup = { ...cleanup, @@ -3873,13 +3850,7 @@ describe.skipIf(!harnessDbEnvAvailable())( status: "allocated", generation: allocation.generation, }); - expect( - ( - await h.db.query.workflowRun.findFirst({ - where: eq(workflowRun.id, ANCHOR_RUN_ID), - }) - )?.publicKey, - ).toBe("committed-key"); + expect(await anchorKey()).toBe("committed-key"); }); test("cleanup waiting behind publication observes the committed key", async () => { @@ -4019,13 +3990,7 @@ describe.skipIf(!harnessDbEnvAvailable())( ? "Initialization cancelled" : "Initialization lease expired", ); - expect( - ( - await h.db.query.workflowRun.findFirst({ - where: eq(workflowRun.id, ANCHOR_RUN_ID), - }) - )?.publicKey, - ).toBeNull(); + expect(await anchorKey()).toBeNull(); expect( (await store.findById(allocation.id))?.initializationLeaseId, ).toBe(initialization.leaseId); @@ -4088,16 +4053,9 @@ describe.skipIf(!harnessDbEnvAvailable())( await h.db .update(workflowRun) - .set({ - publicKey: "generation-1-public-key", - credentialRefs: previousRefs, - }) + .set({ credentialRefs: previousRefs }) .where(eq(workflowRun.id, ANCHOR_RUN_ID)); - await recordPublicKey( - h.db, - ANCHOR_RUN_PRINCIPAL_ID, - "generation-1-public-key", - ); + await recordAnchorKey("generation-1-public-key"); expect( await store.beginReplacement({ @@ -4114,12 +4072,9 @@ describe.skipIf(!harnessDbEnvAvailable())( expect( await h.db.query.workflowRun.findFirst({ where: (row, { eq }) => eq(row.id, ANCHOR_RUN_ID), - columns: { publicKey: true, credentialRefs: true }, + columns: { credentialRefs: true }, }), - ).toEqual({ - publicKey: "generation-1-public-key", - credentialRefs: previousRefs, - }); + ).toEqual({ credentialRefs: previousRefs }); expect(await runPrincipalKeys()).toEqual([ { publicKey: "generation-1-public-key", status: "active" }, ]); @@ -4147,9 +4102,9 @@ describe.skipIf(!harnessDbEnvAvailable())( expect( await h.db.query.workflowRun.findFirst({ where: (row, { eq }) => eq(row.id, ANCHOR_RUN_ID), - columns: { publicKey: true, credentialRefs: true }, + columns: { credentialRefs: true }, }), - ).toEqual({ publicKey: null, credentialRefs: null }); + ).toEqual({ credentialRefs: null }); // The replaced sidecar's key no longer answers for the run. expect(await runPrincipalKeys()).toEqual([ { publicKey: "generation-1-public-key", status: "retired" }, diff --git a/tests/db/sidecar-lifecycle-integration.test.ts b/tests/db/sidecar-lifecycle-integration.test.ts index a68ea77ee..35e10acb2 100644 --- a/tests/db/sidecar-lifecycle-integration.test.ts +++ b/tests/db/sidecar-lifecycle-integration.test.ts @@ -17,6 +17,7 @@ import { hexEncode } from "@intx/types"; import { createSidecarAllocationStore, createWorkflowPendingProjectionStore, + loadRunPrincipalActiveKey, withExecutableWorkflowRun, } from "@intx/db"; import { sidecar, sidecarAllocation, workflowRun } from "@intx/db/schema"; @@ -591,9 +592,10 @@ describe.skipIf(!harnessDbEnvAvailable())( }), ).toMatchObject({ status: "running", - publicKey: null, credentialRefs: null, }); + // The replaced copy's key no longer answers for the run. + expect(await loadRunPrincipalActiveKey(h.db, TENANT, copy.id)).toBeNull(); }); test("a delayed cleanup hello cannot displace its running replacement", async () => { diff --git a/tests/lib/seed.ts b/tests/lib/seed.ts index cca005969..068b0d559 100644 --- a/tests/lib/seed.ts +++ b/tests/lib/seed.ts @@ -360,6 +360,8 @@ export type SeedWorkflowRun = { definitionId?: string; principalId?: string | null; address?: string | null; + // The key the deployment's sidecar minted, recorded as the active key of + // the run principal. publicKey?: string | null; status?: "deployed" | "running" | "completed" | "failed" | "cancelled"; credentialRefs?: WorkflowRunCredentialRefs | null; @@ -394,7 +396,6 @@ export async function seedWorkflowRun( definitionId, principalId: r.principalId ?? null, address: r.address ?? null, - publicKey: r.publicKey ?? null, status: r.status ?? "running", ...(r.credentialRefs !== undefined ? { credentialRefs: r.credentialRefs } @@ -409,8 +410,8 @@ export async function seedWorkflowRun( /** * Make `publicKey` the active key of the run's principal, as a deploy records - * it. The run principal is the one a test already seeded for the run, or the - * one deploy would have created. + * it. The run must be a deployment anchor. The run principal is the one a test + * already seeded for the run, or the one deploy would have created. */ export async function seedRunPrincipalKey( db: Db, @@ -425,7 +426,9 @@ export async function seedRunPrincipalKey( runId, createdAt, }); - await recordRunPrincipalKey(db, tenantId, runId, publicKey); + if (!(await recordRunPrincipalKey(db, tenantId, runId, publicKey))) { + throw new Error(`Run ${runId} is not a deployment anchor`); + } } export type SeedWorkflowRunLaunchSpec = { diff --git a/tests/workflow-deploy/multistep-conversation-failover.test.ts b/tests/workflow-deploy/multistep-conversation-failover.test.ts index 1a0186a9e..a00bd0f9d 100644 --- a/tests/workflow-deploy/multistep-conversation-failover.test.ts +++ b/tests/workflow-deploy/multistep-conversation-failover.test.ts @@ -42,12 +42,11 @@ import { type } from "arktype"; import { and, eq } from "drizzle-orm"; import { createEnvKeyCredentialCipher, generateKeyPair } from "@intx/crypto"; -import { createDB } from "@intx/db"; +import { createDB, loadRunPrincipalActiveKey } from "@intx/db"; import { principal as principalTable, sidecarAllocation as sidecarAllocationTable, tenant as tenantTable, - workflowRun as workflowRunTable, } from "@intx/db/schema"; import { createAgentRepoStore, DEFAULT_ASSET_REF } from "@intx/hub-sessions"; import { @@ -322,10 +321,9 @@ async function deployConversation( deriveRunAddress({ runId: anchorRunId, domain: tenant.domain }), ); - const readAnchor = () => - db.query.workflowRun.findFirst({ - where: eq(workflowRunTable.id, anchorRunId), - }); + // The deployment's key, which commits when an initialization completes. + const readRunKey = () => + loadRunPrincipalActiveKey(db, tenant.tenantId, anchorRunId); const readAllocation = () => db.query.sidecarAllocation.findFirst({ where: eq(sidecarAllocationTable.anchorRunId, anchorRunId), @@ -384,7 +382,7 @@ async function deployConversation( return snapshot; }; return { - readAnchor, + readRunKey, readAllocation, readEvents, inputParks, @@ -440,13 +438,13 @@ describe.skipIf(!harnessHubEnvAvailable())( const tenant = await provisionTenant("conversation_failover"); const assetId = "ast_conversation_failover_wf"; const commitSha = await publishConversation(tenant, assetId); - const { readAnchor, readAllocation, readEvents, inputParks, sendMail } = + const { readRunKey, readAllocation, readEvents, inputParks, sendMail } = await deployConversation(tenant, assetId, commitSha); // --- Two turns on the first host ----------------------------------- // No signal reports these states, so they are polled. The first deploy - // is complete once the anchor key commits. - await waitFor(async () => (await readAnchor())?.publicKey != null); + // is complete once the run principal's key commits. + await waitFor(async () => (await readRunKey()) !== null); const first = await readAllocation(); if (first?.externalRef == null) { throw new Error("the first sidecar has no process id"); @@ -464,14 +462,14 @@ describe.skipIf(!harnessHubEnvAvailable())( // releases the allocation and fails the deployment. await waitFor(async () => { const row = await readAllocation(); - const anchor = await readAnchor(); + const runKey = await readRunKey(); return ( row !== undefined && row.generation > first.generation && row.status === "allocated" && row.externalRef !== null && row.externalRef !== first.externalRef && - anchor?.publicKey != null + runKey !== null ); }); @@ -513,7 +511,7 @@ describe.skipIf(!harnessHubEnvAvailable())( assetA, await publishConversation(tenant, assetA), ); - await waitFor(async () => (await a.readAnchor())?.publicKey != null); + await waitFor(async () => (await a.readRunKey()) !== null); await a.sendMail(`Opening turn ${IMPORT_ONE}.`); await waitFor(async () => (await a.inputParks()) >= 1); await a.sendMail(`Second turn ${IMPORT_TWO}.`); @@ -538,7 +536,7 @@ describe.skipIf(!harnessHubEnvAvailable())( await publishConversation(tenant, assetB), { step1: exported }, ); - await waitFor(async () => (await b.readAnchor())?.publicKey != null); + await waitFor(async () => (await b.readRunKey()) !== null); expect(await b.exportStep1()).toEqual(exported); await b.sendMail(`Third turn ${IMPORT_THREE}.`); await waitFor(async () => (await b.inputParks()) >= 1); diff --git a/tests/workflow-deploy/walking-skeleton.e2e.test.ts b/tests/workflow-deploy/walking-skeleton.e2e.test.ts index 8998fe5e1..749aaaceb 100644 --- a/tests/workflow-deploy/walking-skeleton.e2e.test.ts +++ b/tests/workflow-deploy/walking-skeleton.e2e.test.ts @@ -54,6 +54,7 @@ import { type RepoId, } from "@intx/hub-sessions"; import { createNoopCredentialCipher } from "@intx/crypto"; +import { runPrincipalActiveKeySql } from "@intx/db"; import { workflowDefinitionVersion as workflowDefinitionVersionTable, workflowRun as workflowRunTable, @@ -566,7 +567,10 @@ describe.skipIf(!harnessDbEnvAvailable())("walking skeleton e2e", () => { const anchorRow = await h.db .select({ address: workflowRunTable.address, - publicKey: workflowRunTable.publicKey, + publicKey: runPrincipalActiveKeySql( + workflowRunTable.tenantId, + workflowRunTable.id, + ), tenantId: workflowRunTable.tenantId, }) .from(workflowRunTable) From a90280ddc89b99599109eef0157578642d78b960 Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 18:45:44 -0700 Subject: [PATCH 12/14] Park run sender mail only while its allocated attempt is open Only an allocated deploy attempt's completion or failure settles a run sender's parked mail. A deploy in flight without one has nothing that settles it, so mail parked on it could only expire undelivered. --- .../src/ws/sidecar-handler.test.ts | 41 +++++++++++++++++++ .../hub-sessions/src/ws/sidecar-handler.ts | 26 +++++------- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/packages/hub-sessions/src/ws/sidecar-handler.test.ts b/packages/hub-sessions/src/ws/sidecar-handler.test.ts index 88c437c23..9b7e05b45 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.test.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.test.ts @@ -1809,6 +1809,47 @@ describe("SidecarRouter pre-ack sender-key interlock", () => { expect(undelivered).toEqual([]); }); + test("delivers a run sender's mail keyless while its deploy is in flight with no allocated attempt open", async () => { + // Only an allocated attempt's completion or failure settles a run sender's + // parked mail. A deploy in flight with no attempt open has nothing that + // settles it, so parking would strand the mail to the TTL; the gate + // delivers it keyless instead. + const undelivered: { rawMessage: string; recipients: string[] }[] = []; + const router = createInterlockRouter(async () => null); + router.events.on("mail.outbound.undelivered", (payload) => { + undelivered.push(payload); + }); + + const senderWs = await connectAs(router, "sc-sender", SENDER); + const recipientWs = await connectAs(router, "sc-recipient", RECIPIENT); + + // The sender's deploy is sent and not yet answered, and no + // noteSenderDeployStarted opened an attempt for it. + const deploying = router + .sendAgentDeployToAllocation( + { allocationId: ATTEMPT.allocationId, generation: ATTEMPT.generation }, + SENDER, + config, + TEST_WORKFLOW, + ) + .catch((cause: unknown) => cause); + await waitUntil(() => framesOfType(senderWs, "agent.deploy").length === 1); + + sendMail(router, senderWs); + const inbound = await waitForFrame( + recipientWs, + (f) => f.type === "mail.inbound" && f.authenticatedSender === SENDER, + ); + expect(inbound.authenticatedSender).toBe(SENDER); + const grants = framesOfType(recipientWs, "run.grants"); + expect(grants).toHaveLength(1); + expect(grants[0]?.senderIdentities).toBeUndefined(); + expect(undelivered).toEqual([]); + + router.handleClose(senderWs); + await deploying; + }); + test("does not park a run sender's mail to an external recipient even with a deploy in flight", async () => { // The co-delivered key is only ever consumed by a run recipient. Mail to a // purely external recipient never uses it, so the gate takes the normal path diff --git a/packages/hub-sessions/src/ws/sidecar-handler.ts b/packages/hub-sessions/src/ws/sidecar-handler.ts index 863c2e817..c2137383b 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.ts @@ -387,10 +387,9 @@ export type SidecarRouter = { ): void; /** * Mark a run-address sender's ALLOCATED deploy as mid-flight, before the deploy - * emit and its key record. An allocated run records its key later than - * the deploy ack clears `pendingDeploys`, so this marker covers the allocated - * pre-ack window that `pendingDeploys` alone under-covers. `noteSenderDeploySettled` - * clears it only when the durable outcome is known. Cancellation leaves it + * emit and its key record. Mail the sender sends while the marker is set parks + * until the attempt settles. `noteSenderDeploySettled` clears the marker only + * when the durable outcome is known. Cancellation leaves it * pending for recovery, so a lost publication response cannot discard mail. */ noteSenderDeployStarted( @@ -960,11 +959,10 @@ export function createSidecarRouter( DeploymentRejectedError >(); // Run addresses whose ALLOCATED deploy is mid-flight -- key-record has been - // started but not yet committed. pendingDeploys clears at the deploy ack, but an - // allocated run's key is recorded LATER, on its run principal, by session-service, - // so pendingDeploys alone under-covers the allocated pre-ack window. session- - // service brackets this marker across its deploy try/catch: set before the - // deploy emit, cleared by noteSenderDeploySettled on record or failure. + // started but not yet committed. Mail from such a sender parks until the + // attempt settles. session-service brackets this marker across its deploy + // try/catch: set before the deploy emit, cleared by noteSenderDeploySettled + // on record or failure. const allocatedKeyRecordInFlight = new Map< string, AllocatedSenderDeployAttempt @@ -3390,14 +3388,12 @@ export function createSidecarRouter( }; } - // Park a run sender ONLY while a key-record settle is guaranteed to arrive -- a - // deploy is in flight. Without one, a null resolve is a transient fault or a + // Park a run sender ONLY while a key-record settle is guaranteed to arrive -- + // its allocated deploy attempt is open, and that attempt's completion or + // failure settles it. Without one, a null resolve is a transient fault or a // genuine absence on an already-settled run: no settle is coming, so parking // would strand the mail to the TTL. Deliver on the normal path instead. - const settleGuaranteed = - pendingDeploys.has(authenticatedSender) || - allocatedKeyRecordInFlight.has(authenticatedSender); - if (!settleGuaranteed) { + if (!allocatedKeyRecordInFlight.has(authenticatedSender)) { const key = await resolveSenderKey(authenticatedSender); return { deliver: true, From fb1485a09b2e8f220b2cef36cc32eee1a3b1519c Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Wed, 4 Nov 2026 18:57:48 -0700 Subject: [PATCH 13/14] Remove the deploy-ack router event and address-only settles --- .../src/ws/sidecar-events.test.ts | 35 ---------- .../hub-sessions/src/ws/sidecar-events.ts | 54 ++------------- .../src/ws/sidecar-handler.mail.test.ts | 49 +++++++++---- .../src/ws/sidecar-handler.shared.test.ts | 68 ------------------- .../src/ws/sidecar-handler.test.ts | 26 +++---- .../src/ws/sidecar-handler.transport.test.ts | 20 ------ .../hub-sessions/src/ws/sidecar-handler.ts | 63 ++++------------- tests/hub-agent/lib/deploy-flow-env.ts | 13 +++- 8 files changed, 77 insertions(+), 251 deletions(-) diff --git a/packages/hub-sessions/src/ws/sidecar-events.test.ts b/packages/hub-sessions/src/ws/sidecar-events.test.ts index 8c5d64ee8..91ce7c5bc 100644 --- a/packages/hub-sessions/src/ws/sidecar-events.test.ts +++ b/packages/hub-sessions/src/ws/sidecar-events.test.ts @@ -61,39 +61,4 @@ describe("createSidecarEmitter", () => { expect(seen).toEqual(["ran"]); }); - - test("emitAndAwait runs listeners sequentially and rethrows the first failure", async () => { - const emitter = createSidecarEmitter(); - const seen: string[] = []; - emitter.on("sidecar.allocated.connected", async () => { - seen.push("a"); - }); - emitter.on("sidecar.allocated.connected", async () => { - seen.push("b"); - throw new Error("listener b failed"); - }); - emitter.on("sidecar.allocated.connected", async () => { - seen.push("c"); - }); - - await expect( - emitter.emitAndAwait("sidecar.allocated.connected", { - allocationId: "alloc-1", - generation: 1, - }), - ).rejects.toThrow(/listener b failed/); - - expect(seen).toEqual(["a", "b"]); - }); - - test("listenerCount reflects current subscriptions", () => { - const emitter = createSidecarEmitter(); - expect(emitter.listenerCount("sidecar.allocated.connected")).toBe(0); - - const off = emitter.on("sidecar.allocated.connected", () => undefined); - expect(emitter.listenerCount("sidecar.allocated.connected")).toBe(1); - - off(); - expect(emitter.listenerCount("sidecar.allocated.connected")).toBe(0); - }); }); diff --git a/packages/hub-sessions/src/ws/sidecar-events.ts b/packages/hub-sessions/src/ws/sidecar-events.ts index 5f9665bdb..9303a05cb 100644 --- a/packages/hub-sessions/src/ws/sidecar-events.ts +++ b/packages/hub-sessions/src/ws/sidecar-events.ts @@ -1,23 +1,10 @@ // Typed event emitter for the sidecar router. // // The router emits events at the points where wire-layer frame handling -// completes and a host-side decision or side effect is required. Two -// emission shapes are exposed: -// -// - `emit(type, payload)` — notification semantics. Each listener runs -// inside its own try/catch; a thrown error is logged and does not -// affect other listeners or the wire layer. Used for events whose -// outcome does not feed back into protocol behavior. -// -// - `emitAndAwait(type, payload)` — sequential await semantics. -// Listeners run in registration order; the first rejection propagates -// to the caller and stops the chain. Used for events whose outcome -// affects subsequent wire-layer state (e.g. reconnect rollback). -// -// The TSDoc on each entry in `SidecarEventMap` records which semantic -// applies. Mixing the two on a single event is intentional: today's -// wire layer already has both behaviors, and pretending otherwise -// would silently change failure handling. +// completes and a host-side decision or side effect is required. +// `emit(type, payload)` has notification semantics: each listener runs +// inside its own try/catch, and a thrown error is logged and does not +// affect other listeners or the wire layer. import type { PackRejectReason, @@ -151,13 +138,6 @@ export type SidecarEventMap = { }; }; - /** Awaited. Emitted when an agent.deploy.ack frame arrives. Rejection - * fails the pending deploy with the listener's error. */ - "agent.deploy.ack": { - agentAddress: string; - publicKey: string; - }; - /** Notification. Emitted when the sidecar reports a change to an * agent's connector-thread state. The wire layer caches the state * per agent so the host can read it via @@ -182,14 +162,6 @@ export type SidecarEventEmitter = { listener: SidecarEventListener, ): () => void; emit(type: T, payload: SidecarEventMap[T]): void; - emitAndAwait( - type: T, - payload: SidecarEventMap[T], - ): Promise; - /** Number of listeners registered for `type`. Wire-layer callers use - * this to skip an `await` when nothing is listening, preserving the - * synchronous scheduling of unconfigured-handler paths. */ - listenerCount(type: SidecarEventType): number; }; export function createSidecarEmitter(): SidecarEventEmitter { @@ -201,7 +173,6 @@ export function createSidecarEmitter(): SidecarEventEmitter { "mail.outbound.undelivered": new Set(), "mail.persisted": new Set(), "mail.inbound.acknowledged": new Set(), - "agent.deploy.ack": new Set(), "connector.state.changed": new Set(), }; @@ -239,22 +210,7 @@ export function createSidecarEmitter(): SidecarEventEmitter { } } - async function emitAndAwait( - type: T, - payload: SidecarEventMap[T], - ): Promise { - const set = listeners[type]; - if (set.size === 0) return; - for (const listener of [...set]) { - await listener(payload); - } - } - - function listenerCount(type: SidecarEventType): number { - return listeners[type].size; - } - - return { on, emit, emitAndAwait, listenerCount }; + return { on, emit }; } export type SidecarLookups = { diff --git a/packages/hub-sessions/src/ws/sidecar-handler.mail.test.ts b/packages/hub-sessions/src/ws/sidecar-handler.mail.test.ts index a48af209a..efe2541ed 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.mail.test.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.mail.test.ts @@ -22,6 +22,25 @@ import { waitUntil } from "@intx/types/testing"; const TEST_SENDER = "sender@example.test"; +type AllocatedRouter = ReturnType; + +// Settle the test sender's deploy as an allocated deploy does: open an attempt +// for its address, then settle that attempt. Each call opens its own attempt, +// because a settle closes the attempt it names. +let senderAttempts = 0; +function settleSenderDeploy( + router: AllocatedRouter, + outcome: Parameters[1], +): void { + senderAttempts += 1; + const attempt = { + ...TEST_TARGET, + leaseId: `sender-settle-${String(senderAttempts)}`, + }; + router.noteSenderDeployStarted(TEST_IDENTITY.workflowRunAddress, attempt); + router.noteSenderDeploySettled(attempt, outcome); +} + function framesOfType(ws: { sent: string[] }, type: string) { return parsedFrames(ws).filter( (frame): frame is Record => @@ -1552,7 +1571,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { expect(undelivered).toEqual([]); ready = true; - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { recorded: "recipient-key", }); await tick(); @@ -1599,7 +1618,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { expect(framesOfType(ws, "mail.inbound")).toHaveLength(0); ready = true; - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { recorded: "recipient-key", }); await tick(); @@ -1651,7 +1670,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { expect(framesOfType(ws, "mail.inbound")).toHaveLength(0); ready = true; - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { recorded: "recipient-key", }); await tick(); @@ -1694,7 +1713,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { }), ); await tick(); - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { failed: "init failed", }); await tick(); @@ -1702,7 +1721,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { expect(undelivered).toEqual([]); ready = true; - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { recorded: "recipient-key", }); await tick(); @@ -1746,7 +1765,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { ); await tick(); expect(calls).toBe(1); - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { recorded: "recipient-key", }); await tick(); @@ -1799,7 +1818,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { ); await tick(); expect(calls).toBe(1); - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { failed: "init failed", }); await tick(); @@ -1811,7 +1830,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { expect(undelivered).toEqual([]); ready = true; - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { recorded: "recipient-key", }); await tick(); @@ -1860,10 +1879,10 @@ describe("SidecarRouter workflow-trigger mail gating", () => { ); await tick(); expect(calls).toBe(1); - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { recorded: "recipient-key", }); - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { failed: "init failed", }); await tick(); @@ -1923,7 +1942,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { await tick(); expect(calls).toBe(1); expect(ttl.armedCount()).toBe(0); - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { recorded: "recipient-key", }); await tick(); @@ -2053,7 +2072,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { }), ); await tick(); - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { recorded: "recipient-key", }); await tick(); @@ -2155,7 +2174,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { expect(undelivered.map((event) => event.recipients)).toEqual([[later]]); ready = true; - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { recorded: "recipient-key", }); await tick(); @@ -2176,7 +2195,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { expect(inboundAddresses(ws, TEST_IDENTITY.workflowRunAddress)).toBe(0); expect(undelivered.map((event) => event.recipients)).toEqual([[later]]); - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { failed: "init failed", }); await tick(); @@ -2185,7 +2204,7 @@ describe("SidecarRouter workflow-trigger mail gating", () => { expect(undelivered.map((event) => event.recipients)).toEqual([[later]]); ready = true; - router.noteSenderDeploySettled(TEST_IDENTITY.workflowRunAddress, { + settleSenderDeploy(router, { recorded: "recipient-key", }); await tick(); diff --git a/packages/hub-sessions/src/ws/sidecar-handler.shared.test.ts b/packages/hub-sessions/src/ws/sidecar-handler.shared.test.ts index b0dc1bce1..83c2b20ed 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.shared.test.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.shared.test.ts @@ -2938,74 +2938,6 @@ describe("SidecarRouter replacing a generation on the same sidecar", () => { }); }); - for (const outcome of ["stores", "fails"] as const) { - test(`an ack whose listener ${outcome} after its deploy was replaced leaves the replacement's deploy to its own reply`, async () => { - const { router, hosted } = createSharedRouter([first, second]); - const ws = await reconnect(router, [second.workflowRunAddress]); - const storing = Promise.withResolvers(); - let acks = 0; - router.events.on("agent.deploy.ack", async () => { - acks += 1; - if (acks > 1) return; - await storing.promise; - if (outcome === "fails") throw new Error("database unavailable"); - }); - - const replaced = router - .sendAgentDeployToAllocation( - target(first), - first.workflowRunAddress, - configFor(first), - TEST_WORKFLOW, - ) - .catch((error: unknown) => error); - await ws.awaitSent((sent) => - sent.some((raw) => raw.includes('"agent.deploy"')), - ); - router.handleMessage( - ws, - deployReply(ws, { publicKey: PUBLIC_KEY }, first.workflowRunAddress), - ); - await tick(); - expect(acks).toBe(1); - - const next = allocation(first.allocationId, 2); - router.fenceAllocation(next.allocationId, next.generation); - hosted.bindings = [next, second]; - await router.syncSidecar(SIDECAR); - expect(await replaced).toBeInstanceOf(Error); - router.handleMessage(ws, undeployAck(ws, first.workflowRunAddress)); - let settled = false; - const replacing = router - .sendAgentDeployToAllocation( - target(next), - next.workflowRunAddress, - configFor(next), - TEST_WORKFLOW, - ) - .finally(() => { - settled = true; - }); - await ws.awaitSent( - () => - framesOfType(ws, "agent.deploy").filter( - (frame) => frame["generation"] === 2, - ).length === 1, - ); - - storing.resolve(undefined); - await tick(); - expect(settled).toBe(false); - - const replacementKey = "cd".repeat(32); - router.handleMessage( - ws, - deployReply(ws, { publicKey: replacementKey }, next.workflowRunAddress), - ); - expect(await replacing).toEqual({ publicKey: replacementKey }); - }); - } - test("attaches the replacement at once and credits it nothing the replaced generation sends", async () => { const { router, hosted } = createSharedRouter([first, second]); const ws = await reconnect(router, [ diff --git a/packages/hub-sessions/src/ws/sidecar-handler.test.ts b/packages/hub-sessions/src/ws/sidecar-handler.test.ts index 9b7e05b45..69199f31c 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.test.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.test.ts @@ -1568,7 +1568,6 @@ describe("SidecarRouter pre-ack sender-key interlock", () => { await tick(); router.noteSenderDeploySettled(ATTEMPT, { recorded: KEY }); router.noteSenderDeploySettled(ATTEMPT, { failed: "late failure" }); - router.noteSenderDeploySettled(SENDER, { recorded: KEY }); await tick(); expect(dropped).toHaveLength(1); expect(framesOfType(recipientWs, "mail.inbound")).toHaveLength(0); @@ -1725,12 +1724,12 @@ describe("SidecarRouter pre-ack sender-key interlock", () => { expect(framesOfType(recipientWs, "mail.inbound")).toHaveLength(1); }); - test("a settle only wakes mail parked under a byte-identical sender address", async () => { - // The allocated path passes its deploy address as the settle key, and that - // address is byte-identical to the sender address the run's mail was sent - // under. Prove the interlock is key-sensitive: a settle for any other - // address does NOT wake this sender's parked mail; only the exact address - // does. A diverging key would silently drop the mail to the TTL. + test("a settle only wakes mail parked under its own deploy attempt", async () => { + // A settle names the allocated deploy attempt it completes, and wakes the + // mail of the sender that attempt started. Prove the interlock is + // attempt-sensitive: a settle for another allocation does NOT wake this + // sender's parked mail; only its own attempt does. A diverging key would + // silently drop the mail to the TTL. // // `recordedKey` models the durable public-key write the settle reports: null // while the run is pre-ack (mail parks), then the key once recorded, so the @@ -1748,15 +1747,18 @@ describe("SidecarRouter pre-ack sender-key interlock", () => { sendMail(router, senderWs); await tick(); - // The key is now recorded. A settle keyed on a DIFFERENT address matches no - // parked entry, so the sender's mail stays parked even though its key would - // now resolve -- proving the wake is keyed on the exact sender address. + // The key is now recorded. A settle for a DIFFERENT allocation matches no + // open attempt, so the sender's mail stays parked even though its key would + // now resolve -- proving the wake is keyed on the exact attempt. recordedKey = KEY; - router.noteSenderDeploySettled(RECIPIENT, { recorded: KEY }); + router.noteSenderDeploySettled( + { ...ATTEMPT, allocationId: "alloc-recipient" }, + { recorded: KEY }, + ); await tick(); expect(framesOfType(recipientWs, "mail.inbound")).toHaveLength(0); - // The byte-identical settle wakes it and it delivers with the key. + // The sender's own attempt wakes it and it delivers with the key. router.noteSenderDeploySettled(ATTEMPT, { recorded: KEY }); const inbound = await waitForFrame( recipientWs, diff --git a/packages/hub-sessions/src/ws/sidecar-handler.transport.test.ts b/packages/hub-sessions/src/ws/sidecar-handler.transport.test.ts index b236c5469..0915d1cf0 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.transport.test.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.transport.test.ts @@ -516,26 +516,6 @@ describe("SidecarRouter allocation deploy transport", () => { }); } - test("rejects a deploy when its acknowledgement subscriber fails", async () => { - const router = createAllocatedRouter(); - router.events.on("agent.deploy.ack", () => { - throw new Error("database write failed"); - }); - const ws = await connectAllocated(router); - const deploy = router.sendAgentDeployToAllocation( - TEST_TARGET, - TEST_IDENTITY.workflowRunAddress, - TEST_CONFIG, - TEST_WORKFLOW, - ); - await tick(); - - router.handleMessage(ws, deployReply(ws, { publicKey: "b".repeat(64) })); - - await expect(deploy).rejects.toThrow("Failed to store public key"); - expect(router.getRoutableAddresses()).toEqual([]); - }); - test("disconnect rejects an in-flight deploy", async () => { const router = createAllocatedRouter(); const ws = await connectAllocated(router); diff --git a/packages/hub-sessions/src/ws/sidecar-handler.ts b/packages/hub-sessions/src/ws/sidecar-handler.ts index c2137383b..a7062e8cd 100644 --- a/packages/hub-sessions/src/ws/sidecar-handler.ts +++ b/packages/hub-sessions/src/ws/sidecar-handler.ts @@ -378,11 +378,10 @@ export type SidecarRouter = { * co-delivers; a `failed` outcome drains it to `mail.outbound.undelivered`. * Allocated callbacks name their exact attempt; recovery may settle the * previous attempt by generation after claiming its reconciliation lease. - * An address-only settlement belongs to a non-allocated deployment and cannot - * settle an allocated attempt. Stale or repeated settlements are no-ops. + * Stale or repeated settlements are no-ops. */ noteSenderDeploySettled( - sender: string | AllocatedSidecarTarget | AllocatedSenderDeployAttempt, + sender: AllocatedSidecarTarget | AllocatedSenderDeployAttempt, outcome: SenderDeploySettledOutcome, ): void; /** @@ -3134,25 +3133,20 @@ export function createSidecarRouter( } function noteSenderDeploySettled( - sender: string | AllocatedSidecarTarget | AllocatedSenderDeployAttempt, + sender: AllocatedSidecarTarget | AllocatedSenderDeployAttempt, outcome: SenderDeploySettledOutcome, ): void { - if (typeof sender !== "string") { - for (const [address, attempt] of [...allocatedKeyRecordInFlight]) { - if ( - attempt.allocationId !== sender.allocationId || - attempt.generation !== sender.generation || - ("leaseId" in sender && attempt.leaseId !== sender.leaseId) - ) { - continue; - } - allocatedKeyRecordInFlight.delete(address); - settleSenderMail(address, outcome); + for (const [address, attempt] of [...allocatedKeyRecordInFlight]) { + if ( + attempt.allocationId !== sender.allocationId || + attempt.generation !== sender.generation || + ("leaseId" in sender && attempt.leaseId !== sender.leaseId) + ) { + continue; } - return; + allocatedKeyRecordInFlight.delete(address); + settleSenderMail(address, outcome); } - if (allocatedKeyRecordInFlight.has(sender)) return; - settleSenderMail(sender, outcome); } function parkDeferredRecipientResolutionMail( @@ -5053,41 +5047,12 @@ export function createSidecarRouter( : undefined; } - async function handleDeployAck( - ws: WsHandle, - frame: AgentDeployAckFrame, - ): Promise { + function handleDeployAck(ws: WsHandle, frame: AgentDeployAckFrame): void { connections.get(ws)?.unansweredDeploys.delete(frame.requestId); - const req = deployAnsweredBy(ws, frame); - if (req === undefined) { + if (deployAnsweredBy(ws, frame) === undefined) { logger.warn`Ignoring agent.deploy.ack ${frame.requestId} for ${frame.agentAddress} generation ${String(frame.generation)}: it answers no deploy in flight on this connection`; return; } - // The listeners below are awaited, and the deploy this ack answers can be - // settled meanwhile (a timeout, the allocation leaving) and the address - // deployed again. Only the deploy the ack answers may be settled by it. - const stillAnswered = (): boolean => { - if (pendingDeploys.get(frame.agentAddress) === req) return true; - logger.warn`Dropping agent.deploy.ack ${frame.requestId} for ${frame.agentAddress} generation ${String(frame.generation)}: its deploy was settled while the ack's listeners ran`; - return false; - }; - - if (events.listenerCount("agent.deploy.ack") > 0) { - try { - await events.emitAndAwait("agent.deploy.ack", { - agentAddress: frame.agentAddress, - publicKey: frame.publicKey, - }); - } catch (err) { - if (!stillAnswered()) return; - pendingDeploys.reject( - frame.agentAddress, - `Failed to store public key: ${err instanceof Error ? err.message : String(err)}`, - ); - return; - } - if (!stillAnswered()) return; - } pendingDeploys.resolve(frame.agentAddress, frame.publicKey); } diff --git a/tests/hub-agent/lib/deploy-flow-env.ts b/tests/hub-agent/lib/deploy-flow-env.ts index ca5e3c58c..56a8b57a7 100644 --- a/tests/hub-agent/lib/deploy-flow-env.ts +++ b/tests/hub-agent/lib/deploy-flow-env.ts @@ -40,6 +40,7 @@ import { Hono } from "hono"; import { upgradeWebSocket, websocket } from "hono/bun"; import type { Subprocess } from "bun"; +import { AgentDeployAckFrame } from "@intx/types/sidecar"; import { assembleSignedContent, assembleMessage, @@ -1063,9 +1064,14 @@ export async function startHub( router.events.on("agent.event", ({ agentAddress, sessionId, event }) => { agentEvents.push({ addr: agentAddress, sid: sessionId, event }); }); - router.events.on("agent.deploy.ack", ({ agentAddress, publicKey }) => { - deployAcks.set(agentAddress, publicKey); - }); + // Record each deployment's key as its sidecar's deploy acknowledgement + // reports it. + function recordDeployAck(data: string): void { + const frame: unknown = JSON.parse(data); + if (AgentDeployAckFrame.allows(frame)) { + deployAcks.set(frame.agentAddress, frame.publicKey); + } + } const sessionService = createSessionService({ loadRunCredentials: opts.loadRunCredentials ?? (async () => null), @@ -1095,6 +1101,7 @@ export async function startHub( onMessage(evt, _ws) { if (typeof evt.data === "string") { router.handleMessage(handle, evt.data); + recordDeployAck(evt.data); } }, onClose(_evt, _ws) { From 60ffc96029defbe8db8b86c6cba8ddd6300493c4 Mon Sep 17 00:00:00 2001 From: Alexander Guy Date: Thu, 5 Nov 2026 08:03:59 -0700 Subject: [PATCH 14/14] Refuse to remove the run principal of an existing run A run principal holds its deployment's only signing key, and its run names it by reference rather than by a foreign key, so the existing referenced-principal refusal did not cover it. --- packages/db/src/schema/workflow-run.ts | 3 +- packages/hub-api/src/routes/principals.ts | 46 +++++++++++++++-- tests/hub-api/principal-routes.test.ts | 61 ++++++++++++++++++++++- 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/packages/db/src/schema/workflow-run.ts b/packages/db/src/schema/workflow-run.ts index 5f08c0907..77ab09696 100644 --- a/packages/db/src/schema/workflow-run.ts +++ b/packages/db/src/schema/workflow-run.ts @@ -94,7 +94,8 @@ export const workflowRun = pgTable( // inherits the deployment's grants at read time. Find a run's principal by // `refId`, not by this link. `onDelete: "set null"` is deliberate (unlike // agent_instance's principal, which uses the implicit default): deleting a - // run's principal nulls the column rather than wedging the run row. + // run's principal nulls the column rather than wedging the run row. The + // principals route refuses to delete it while the run exists. principalId: text("principal_id").references(() => principal.id, { onDelete: "set null", }), diff --git a/packages/hub-api/src/routes/principals.ts b/packages/hub-api/src/routes/principals.ts index fe4fd3bb2..75d262458 100644 --- a/packages/hub-api/src/routes/principals.ts +++ b/packages/hub-api/src/routes/principals.ts @@ -2,7 +2,13 @@ import { eq, ne, and } from "drizzle-orm"; import { Hono } from "hono"; import { describeRoute, validator } from "hono-openapi"; -import { principal, principalRole, role, user } from "@intx/db/schema"; +import { + principal, + principalRole, + role, + user, + workflowRun, +} from "@intx/db/schema"; import { createPrincipalStore, deleteRunTokensForPrincipals, @@ -115,6 +121,15 @@ export type CreatePrincipalRoutesDeps = { sidecarRouter: Pick; }; +// A run principal whose run still exists cannot be removed: the run names it +// by `refId`, and it holds the deployment's signing key. +class RunPrincipalReferencedError extends Error { + constructor() { + super("Principal is still referenced by its run"); + this.name = "RunPrincipalReferencedError"; + } +} + export function createPrincipalRoutes({ db, requireGrant, @@ -361,7 +376,10 @@ export function createPrincipalRoutes({ // workflow_run_launch_spec.source_authority_principal_id (restrict), // and workflow_definition.creator_principal_id (NO ACTION). The delete // fires a violation only for a row actually being deleted, so the - // catch maps it to a 409 instead of a 500. + // catch maps it to a 409 instead of a 500. A run principal is the + // exception: its run names it by `refId`, which no foreign key + // covers, and it holds the deployment's signing key. The handler + // refuses it with the same 409 while its run exists. // Grants, role assignments, and principal keys cascade from the // principal. An asset it created loses its creator, so the runs that // delegate from it as creator are found before the delete and @@ -373,6 +391,25 @@ export function createPrincipalRoutes({ delegatorDeps, tenantCtx.id, async (tx) => { + const [run] = await tx + .select({ id: workflowRun.id }) + .from(principal) + .innerJoin( + workflowRun, + and( + eq(workflowRun.id, principal.refId), + eq(workflowRun.tenantId, principal.tenantId), + ), + ) + .where( + and( + eq(principal.id, principalId), + eq(principal.tenantId, tenantCtx.id), + eq(principal.kind, "workflow"), + ), + ) + .limit(1); + if (run !== undefined) throw new RunPrincipalReferencedError(); const anchorRunIds = await findDelegatedAnchorRunIds( tx, tenantCtx.id, @@ -395,7 +432,10 @@ export function createPrincipalRoutes({ }, ); } catch (err) { - if (!isReferencedRowViolation(err)) { + if ( + !(err instanceof RunPrincipalReferencedError) && + !isReferencedRowViolation(err) + ) { throw err; } return errorResponse(c, "conflict", "Principal is still referenced"); diff --git a/tests/hub-api/principal-routes.test.ts b/tests/hub-api/principal-routes.test.ts index 55c0e8ebf..f1bd9095b 100644 --- a/tests/hub-api/principal-routes.test.ts +++ b/tests/hub-api/principal-routes.test.ts @@ -9,8 +9,10 @@ import { import { eq } from "drizzle-orm"; +import { deriveRunPrincipalId } from "@intx/hub-common"; + import { createInMemoryGrantStore } from "@intx/authz"; -import { credential, principal, runToken } from "@intx/db/schema"; +import { credential, principal, principalKey, runToken } from "@intx/db/schema"; import { createApp, type GetSession } from "@intx/hub-api"; import { createSidecarEmitter, @@ -29,6 +31,7 @@ import { seedCredential, seedPrincipal, seedProvider, + seedRunPrincipalKey, seedRunToken, seedTenants, seedWorkflowRun, @@ -391,6 +394,62 @@ describe.skipIf(!harnessDbEnvAvailable())( expect(orgCredential?.principalId).toBeNull(); }); + test("refuses to remove the run principal of an existing run", async () => { + const app = await setupDelete(); + await seedWorkflowRun(h.db, { + id: DEPLOYMENT_ID, + tenantId: TENANT_ID, + anchorRunId: DEPLOYMENT_ID, + address: DEPLOYMENT_ADDRESS, + status: "running", + }); + // The run principal deploy created, holding the deployment's key. + await seedRunPrincipalKey( + h.db, + TENANT_ID, + DEPLOYMENT_ID, + "ab".repeat(32), + ); + const runPrincipalId = await deriveRunPrincipalId( + TENANT_ID, + DEPLOYMENT_ID, + ); + + const res = await app.request( + `/api/tenants/${TENANT_ID}/principals/${runPrincipalId}`, + { method: "DELETE" }, + ); + expect(res.status).toBe(409); + const body: unknown = await res.json(); + if (!isObject(body)) throw new Error("expected object body"); + const error = body["error"]; + if (!isObject(error)) throw new Error("expected error body"); + expect(error["code"]).toBe("conflict"); + expect(error["message"]).toBe("Principal is still referenced"); + + const keys = await h.db + .select({ publicKey: principalKey.publicKey }) + .from(principalKey) + .where(eq(principalKey.principalId, runPrincipalId)); + expect(keys).toEqual([{ publicKey: "ab".repeat(32) }]); + }); + + test("removes a workflow principal that names no run", async () => { + const app = await setupDelete(); + await seedPrincipal(h.db, { + id: WORKFLOW_PRINCIPAL_ID, + tenantId: TENANT_ID, + kind: "workflow", + refId: "wfd_definition", + }); + + const res = await app.request( + `/api/tenants/${TENANT_ID}/principals/${WORKFLOW_PRINCIPAL_ID}`, + { method: "DELETE" }, + ); + expect(res.status).toBe(204); + }); + test("returns not found for an unknown principal", async () => { const app = await setupDelete(); const res = await app.request(