From 5c1a3236c06e6f144151c69ca443fd19f9d5b512 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 02:23:51 -0700 Subject: [PATCH 01/14] feat(secrets): add non-secret workspace variables Workspace secrets are redacted everywhere: masked from non-admins, scrubbed out of traces by the resolved-secret projection, logged as ciphertext, and exposed to Sim by name only. That is right for API keys and wrong for the config values teams also keep there - a support email, a region, a base URL. A variable is a workspace secret with its disclosure policy flipped. Same table, same encryption at rest, same `{{NAME}}` namespace and uniqueness guarantee; a per-key `credential.env_visibility` flag turns off redaction. Read-side only. Write authorization is deliberately identical for both kinds: non-secret and non-sensitive are different axes, and a member who cannot read a secret must still not be able to repoint an `API_BASE_URL` that secret is sent to. The provenance exemption is the security-critical part. Resolvers call recordResolved for every substitution and cannot tell a variable from a secret, so a known non-secret name returns early - before the catalog lookup, so a decryption failure cannot reroute it - instead of tripping the permanent markIncomplete that would collapse the run's traces and omit every later copilot tool result. The exempt set is a required constructor parameter: a missing set over-redacts (safe), a wrong one under-redacts (not), so every construction site is forced to name its source or fail to compile. It is derived locally on each side of every boundary and never carried in provenance, so no peer can assert that one of your secrets is non-secret. Sim reads values from environment/variables.json under `nonSecretValues`, filtered by the same mount policy that already gates name discovery. Also fixes, found while wiring this up: fetchWorkspaceEnvironment and two React Query `select` callbacks rebuilt the response field-by-field and would have dropped the new field (the selects also broke structural sharing); a visibility-only PUT hit the empty-body early return before the flip handler. --- apps/sim/app/api/knowledge/search/route.ts | 17 +- .../app/api/knowledge/search/utils.test.ts | 13 +- .../app/api/knowledge/secret-provenance.ts | 29 +- .../sim/app/api/mcp/serve/[serverId]/route.ts | 7 +- apps/sim/app/api/memory/secret-provenance.ts | 17 +- apps/sim/app/api/workflows/[id]/log/route.ts | 17 +- .../workspaces/[id]/environment/route.test.ts | 44 + .../api/workspaces/[id]/environment/route.ts | 109 +- .../secret-value-field/secret-value-field.tsx | 15 +- .../secrets-manager/secrets-manager.tsx | 138 +- .../env-var-dropdown/env-var-dropdown.tsx | 14 +- apps/sim/background/webhook-execution.ts | 1 + .../background/workflow-column-execution.ts | 17 +- apps/sim/enrichments/run.test.ts | 7 +- .../executor/execution/block-executor.test.ts | 76 +- apps/sim/executor/execution/engine.test.ts | 21 +- .../execution/snapshot-serializer.test.ts | 33 +- .../handlers/agent/agent-handler.test.ts | 65 +- .../executor/handlers/agent/memory.test.ts | 81 +- .../evaluator/evaluator-handler.test.ts | 61 +- .../executor/handlers/pi/pi-handler.test.ts | 21 +- .../executor/handlers/pi/search/tool.test.ts | 33 +- .../executor/handlers/pi/sim-tools.test.ts | 41 +- .../handlers/router/router-handler.test.ts | 61 +- .../workflow/custom-block-tool-runner.test.ts | 15 +- .../workflow/workflow-handler.test.ts | 5 +- .../handlers/workflow/workflow-handler.ts | 4 + apps/sim/executor/orchestrators/loop.test.ts | 41 +- ...resolved-secret-content-projection.test.ts | 169 +- .../resolved-secret-trace-registry.test.ts | 413 +- .../utils/resolved-secret-trace-registry.ts | 93 +- apps/sim/executor/variables/resolver.test.ts | 21 +- .../variables/resolvers/block.test.ts | 7 +- .../executor/variables/resolvers/env.test.ts | 13 +- .../variables/resolvers/workflow.test.ts | 7 +- apps/sim/hooks/queries/environment.ts | 8 +- apps/sim/lib/api/contracts/environment.ts | 31 +- .../sim/lib/copilot/chat/workspace-context.ts | 36 +- apps/sim/lib/copilot/environment-context.ts | 1 + .../copilot/request/handlers/handlers.test.ts | 29 +- .../lib/copilot/request/lifecycle/run.test.ts | 99 +- .../copilot/request/lifecycle/start.test.ts | 13 +- .../lib/copilot/request/tools/client.test.ts | 24 +- .../copilot/request/tools/executor.test.ts | 45 +- .../lib/copilot/request/tools/files.test.ts | 41 +- .../tools/resolved-secret-result.test.ts | 71 +- .../lib/copilot/request/tools/tables.test.ts | 85 +- .../copilot/tool-executor/executor.test.ts | 13 +- .../tools/handlers/function-execute.test.ts | 53 +- .../tools/handlers/function-execute.ts | 18 +- .../tools/handlers/workflow/mutations.test.ts | 42 +- .../secret-mount-materializer.server.test.ts | 3 + .../tools/secret-mount-materializer.server.ts | 30 +- .../server/docs/search-documentation.test.ts | 23 +- .../server/knowledge/knowledge-base.test.ts | 43 +- .../server/media/model-boundaries.test.ts | 9 +- .../copilot/tools/server/model-input.test.ts | 21 +- .../tools/server/other/search-online.test.ts | 23 +- apps/sim/lib/copilot/vfs/serializers.test.ts | 24 + apps/sim/lib/copilot/vfs/serializers.ts | 13 +- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 22 +- apps/sim/lib/credentials/environment.ts | 115 +- apps/sim/lib/environment/api.ts | 19 +- apps/sim/lib/environment/utils.ts | 30 +- .../execution/durable-secret-provenance.ts | 3 +- .../execution/model-input-provenance.test.ts | 15 +- .../guardrails/validate_hallucination.test.ts | 17 +- ...cument-processor-secret-provenance.test.ts | 13 +- .../knowledge/model-input-provenance.test.ts | 23 +- .../lib/knowledge/model-input-provenance.ts | 15 +- apps/sim/lib/knowledge/reranker.test.ts | 13 +- apps/sim/lib/knowledge/secret-provenance.ts | 3 +- .../logs/execution/logging-session.test.ts | 9 +- .../sim/lib/logs/execution/logging-session.ts | 14 +- .../execution/trace-secret-projection.test.ts | 45 +- apps/sim/lib/logs/execution/trace-store.ts | 7 +- apps/sim/lib/mcp/resolve-config.ts | 1 + apps/sim/lib/model-router/resolve.test.ts | 59 +- apps/sim/lib/table/backfill-runner.ts | 7 +- apps/sim/lib/table/rows/secret-provenance.ts | 3 +- .../table/secret-provenance-selection.test.ts | 17 +- .../workflows/executor/execution-core.test.ts | 35 + .../lib/workflows/executor/execution-core.ts | 18 +- .../executor/input-secret-provenance.ts | 7 +- .../anthropic/streaming-tool-loop.test.ts | 13 +- .../providers/file-attachments.server.test.ts | 13 +- apps/sim/providers/index.test.ts | 111 +- apps/sim/providers/openai/utils.test.ts | 13 +- apps/sim/providers/runtime-context.test.ts | 157 +- apps/sim/tools/index.test.ts | 442 +- apps/sim/tools/request-transport.test.ts | 11 +- packages/db/migrations/0285_brainy_mauler.sql | 3 + .../db/migrations/meta/0285_snapshot.json | 18807 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 24 + .../src/mocks/environment-utils.mock.test.ts | 2 + .../src/mocks/environment-utils.mock.ts | 2 + 97 files changed, 21522 insertions(+), 1117 deletions(-) create mode 100644 packages/db/migrations/0285_brainy_mauler.sql create mode 100644 packages/db/migrations/meta/0285_snapshot.json diff --git a/apps/sim/app/api/knowledge/search/route.ts b/apps/sim/app/api/knowledge/search/route.ts index c4c412df787..76e28402eba 100644 --- a/apps/sim/app/api/knowledge/search/route.ts +++ b/apps/sim/app/api/knowledge/search/route.ts @@ -39,7 +39,10 @@ import { } from '@/app/api/knowledge/search/utils' import { createKnowledgeRegistryResponse } from '@/app/api/knowledge/secret-provenance' import { checkKnowledgeBaseAccess, type KnowledgeBaseAccessResult } from '@/app/api/knowledge/utils' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { getRerankModelPricing } from '@/providers/models' import { calculateCost } from '@/providers/utils' @@ -372,10 +375,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const resultSecretRegistry = modelInputProvenance.registry ?? - new ResolvedSecretTraceRegistry([], { - userId, - ...(workspaceId ? { workspaceId } : {}), - }) + new ResolvedSecretTraceRegistry( + [], + { + userId, + ...(workspaceId ? { workspaceId } : {}), + }, + EMPTY_NON_SECRET_NAMES + ) const resultProvenanceSnapshot = await importKnowledgeSearchResultSecretProvenance({ registry: resultSecretRegistry, results, diff --git a/apps/sim/app/api/knowledge/search/utils.test.ts b/apps/sim/app/api/knowledge/search/utils.test.ts index d6fd752c3b6..9cb42457ef9 100644 --- a/apps/sim/app/api/knowledge/search/utils.test.ts +++ b/apps/sim/app/api/knowledge/search/utils.test.ts @@ -16,7 +16,10 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vites import { env } from '@/lib/core/config/env' import * as documentsUtilsModule from '@/lib/knowledge/documents/utils' import { runWithKnowledgeModelInputProvenance } from '@/lib/knowledge/model-input-provenance' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' /** * Spy on the real documents/utils namespace instead of vi.mock: the shared @@ -804,9 +807,11 @@ describe('Knowledge Search Utils', () => { }, }) - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'encrypted-token' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'encrypted-token' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'secret-value') await runWithKnowledgeModelInputProvenance(registry, () => diff --git a/apps/sim/app/api/knowledge/secret-provenance.ts b/apps/sim/app/api/knowledge/secret-provenance.ts index 45d45206b4d..dad60e1094a 100644 --- a/apps/sim/app/api/knowledge/secret-provenance.ts +++ b/apps/sim/app/api/knowledge/secret-provenance.ts @@ -27,7 +27,10 @@ import { knowledgeDocumentTagValueSelectionKey, parseKnowledgeDocumentTagProvenanceTargets, } from '@/lib/knowledge/secret-provenance-selection' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' function invalidKnowledgeProvenanceResponse(): NextResponse { return NextResponse.json({ error: 'Invalid knowledge secret provenance' }, { status: 400 }) @@ -151,10 +154,14 @@ export async function createKnowledgeProvenanceResponse(options: { ) if (negotiation.status === 'not-requested') return NextResponse.json(options.body) if (negotiation.status === 'rejected') return invalidKnowledgeProvenanceResponse() - const registry = new ResolvedSecretTraceRegistry([], { - userId: options.userId, - ...(options.workspaceId ? { workspaceId: options.workspaceId } : {}), - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: options.userId, + ...(options.workspaceId ? { workspaceId: options.workspaceId } : {}), + }, + EMPTY_NON_SECRET_NAMES + ) for (const provenance of options.provenances) { if (provenance.status === 'unknown') { registry.markIncomplete() @@ -225,10 +232,14 @@ export async function createKnowledgePersistedResponse(options: { if (negotiation.status === 'not-requested') return NextResponse.json(options.body) if (negotiation.status === 'rejected') return invalidKnowledgeProvenanceResponse() - const registry = new ResolvedSecretTraceRegistry([], { - userId: options.userId, - ...(options.workspaceId ? { workspaceId: options.workspaceId } : {}), - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: options.userId, + ...(options.workspaceId ? { workspaceId: options.workspaceId } : {}), + }, + EMPTY_NON_SECRET_NAMES + ) await importKnowledgePersistedResponseSecretProvenance({ registry, documents: options.documents, diff --git a/apps/sim/app/api/mcp/serve/[serverId]/route.ts b/apps/sim/app/api/mcp/serve/[serverId]/route.ts index affbf3f74d8..962f83bceef 100644 --- a/apps/sim/app/api/mcp/serve/[serverId]/route.ts +++ b/apps/sim/app/api/mcp/serve/[serverId]/route.ts @@ -72,7 +72,10 @@ import { import { getMeaningfulWorkflowDescription } from '@/lib/mcp/workflow-tool-schema' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const logger = createLogger('WorkflowMcpServeAPI') const MAX_MCP_SERVE_BODY_BYTES = 10 * 1024 * 1024 @@ -286,7 +289,7 @@ async function projectWorkflowMcpModelContent( privateProvenance: unknown, scope: { userId: string; workspaceId: string } ): Promise { - const registry = new ResolvedSecretTraceRegistry([], scope) + const registry = new ResolvedSecretTraceRegistry([], scope, EMPTY_NON_SECRET_NAMES) const imported = await registry.importCrossingProvenance(privateProvenance, value, { trusted: true, }) diff --git a/apps/sim/app/api/memory/secret-provenance.ts b/apps/sim/app/api/memory/secret-provenance.ts index 272ee3a7133..fe747a035df 100644 --- a/apps/sim/app/api/memory/secret-provenance.ts +++ b/apps/sim/app/api/memory/secret-provenance.ts @@ -20,7 +20,10 @@ import { serializePrivateToolMetadataResponseEnvelope, } from '@/lib/execution/private-tool-metadata' import { readBoundMemorySecretProvenance } from '@/lib/memory/secret-provenance' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const MAX_PRIVATE_MEMORY_CROSSINGS = 10_000 const PRIVATE_MEMORY_QUERY_CHUNK_SIZE = 8 @@ -91,10 +94,14 @@ export async function createMemoryResponse(options: { if (negotiation.status === 'not-requested') return NextResponse.json(options.body) if (negotiation.status === 'rejected') return invalidMemoryProvenanceResponse() - const registry = new ResolvedSecretTraceRegistry([], { - userId: options.userId, - workspaceId: options.workspaceId, - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: options.userId, + workspaceId: options.workspaceId, + }, + EMPTY_NON_SECRET_NAMES + ) if (options.memories.length > MAX_PRIVATE_MEMORY_CROSSINGS) { registry.markIncomplete() } else { diff --git a/apps/sim/app/api/workflows/[id]/log/route.ts b/apps/sim/app/api/workflows/[id]/log/route.ts index 8e5123504ed..049c89854e7 100644 --- a/apps/sim/app/api/workflows/[id]/log/route.ts +++ b/apps/sim/app/api/workflows/[id]/log/route.ts @@ -18,7 +18,10 @@ import { validateWorkflowAccess } from '@/app/api/workflows/middleware' import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils' import type { SerializableExecutionState } from '@/executor/execution/types' import type { ExecutionResult } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const logger = createLogger('WorkflowLogAPI') @@ -122,10 +125,14 @@ export const POST = withRouteHandler( const isChatExecution = result.metadata?.source === 'chat' const triggerType = isChatExecution ? 'chat' : 'manual' const loggingSession = new LoggingSession(id, executionId, triggerType, requestId) - const resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry([], { - userId: actorUserId, - workspaceId: existingLog.workspaceId, - }) + const resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry( + [], + { + userId: actorUserId, + workspaceId: existingLog.workspaceId, + }, + EMPTY_NON_SECRET_NAMES + ) const trustedExecutionData = await materializeExecutionData( existingLog.executionData as Record, { diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.test.ts b/apps/sim/app/api/workspaces/[id]/environment/route.test.ts index 7f0f121d319..e6d6fc7f7fe 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.test.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.test.ts @@ -56,6 +56,7 @@ describe('GET /api/workspaces/[id]/environment', () => { personalDecrypted: { PERSONAL: 'personal-secret', SHARED_PERSONAL: 'shared-secret' }, personalOwners: { PERSONAL: 'u-1', SHARED_PERSONAL: 'owner-2' }, conflicts: [], + workspaceVariableKeys: [], }) mockGetPersonalEnvKeyRawAccess.mockResolvedValue({ ownedKeys: new Set(['PERSONAL']), @@ -78,6 +79,7 @@ describe('GET /api/workspaces/[id]/environment', () => { mockGetWorkspaceEnvKeyAdminAccess.mockResolvedValue({ adminKeys: new Set(), knownKeys: new Set(['OPENAI_API_KEY', 'DATABASE_URL']), + variableKeys: new Set(), }) const { status, body } = await callGet() @@ -88,11 +90,49 @@ describe('GET /api/workspaces/[id]/environment', () => { expect(body.data.workspace.DATABASE_URL).toBe('') }) + it('reveals a non-secret value to a read-only member while still masking secrets', async () => { + mockGetUserEntityPermissions.mockResolvedValue('read') + mockGetWorkspaceEnvKeyAdminAccess.mockResolvedValue({ + adminKeys: new Set(), + knownKeys: new Set(['OPENAI_API_KEY', 'DATABASE_URL']), + variableKeys: new Set(['DATABASE_URL']), + }) + + const { status, body } = await callGet() + + expect(status).toBe(200) + // Both halves asserted together: a test that only checked the variable would + // still pass if the exemption accidentally widened to every key. + expect(body.data.workspace.DATABASE_URL).toBe('postgres://secret') + expect(body.data.workspace.OPENAI_API_KEY).toBe('') + expect(body.data.visibility).toEqual({ + OPENAI_API_KEY: 'secret', + DATABASE_URL: 'variable', + }) + }) + + it('reports every key as a secret when nothing is marked non-secret', async () => { + mockGetUserEntityPermissions.mockResolvedValue('read') + mockGetWorkspaceEnvKeyAdminAccess.mockResolvedValue({ + adminKeys: new Set(), + knownKeys: new Set(['OPENAI_API_KEY', 'DATABASE_URL']), + variableKeys: new Set(), + }) + + const { body } = await callGet() + + expect(body.data.visibility).toEqual({ + OPENAI_API_KEY: 'secret', + DATABASE_URL: 'secret', + }) + }) + it('reveals only the workspace values the caller is a credential admin of', async () => { mockGetUserEntityPermissions.mockResolvedValue('write') mockGetWorkspaceEnvKeyAdminAccess.mockResolvedValue({ adminKeys: new Set(['OPENAI_API_KEY']), knownKeys: new Set(['OPENAI_API_KEY', 'DATABASE_URL']), + variableKeys: new Set(), }) const { body } = await callGet() @@ -106,6 +146,7 @@ describe('GET /api/workspaces/[id]/environment', () => { mockGetWorkspaceEnvKeyAdminAccess.mockResolvedValue({ adminKeys: new Set(), knownKeys: new Set(), + variableKeys: new Set(), }) const { body } = await callGet() @@ -119,6 +160,7 @@ describe('GET /api/workspaces/[id]/environment', () => { mockGetWorkspaceEnvKeyAdminAccess.mockResolvedValue({ adminKeys: new Set(), knownKeys: new Set(), + variableKeys: new Set(), }) const { body } = await callGet() @@ -132,6 +174,7 @@ describe('GET /api/workspaces/[id]/environment', () => { mockGetWorkspaceEnvKeyAdminAccess.mockResolvedValue({ adminKeys: new Set(), knownKeys: new Set(['OPENAI_API_KEY', 'DATABASE_URL']), + variableKeys: new Set(), }) const { body } = await callGet() @@ -144,6 +187,7 @@ describe('GET /api/workspaces/[id]/environment', () => { mockGetWorkspaceEnvKeyAdminAccess.mockResolvedValue({ adminKeys: new Set(), knownKeys: new Set(['OPENAI_API_KEY', 'DATABASE_URL']), + variableKeys: new Set(), }) mockGetPersonalEnvKeyRawAccess.mockResolvedValue({ ownedKeys: new Set(['PERSONAL']), diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.ts b/apps/sim/app/api/workspaces/[id]/environment/route.ts index 98194c979b0..c7a1d5fc226 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.ts @@ -18,8 +18,11 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { createWorkspaceEnvCredentials, deleteWorkspaceEnvCredentials, + type EnvVisibility, getPersonalEnvKeyRawAccess, getWorkspaceEnvKeyAdminAccess, + setWorkspaceEnvVisibility, + WorkspaceEnvVisibilityAccessError, } from '@/lib/credentials/environment' import { getPersonalAndWorkspaceEnv, @@ -44,10 +47,18 @@ const WORKSPACE_ENV_LOCK_TIMEOUT_MS = 5_000 /** * Restricts decrypted workspace env values to administrators. Members (including * read-only) receive the variable names with empty values so editor autocomplete - * and conflict detection keep working without leaking secret values. A value is - * revealed when the caller is a workspace admin (which includes organization - * admins) or a per-secret credential admin of that key. Mirrors the per-key edit - * gating in PUT/DELETE: if you can administer a secret, you can read it. + * and conflict detection keep working without leaking secret values. A secret's + * value is revealed when the caller is a workspace admin (which includes + * organization admins) or a per-secret credential admin of that key. Mirrors the + * per-key edit gating in PUT/DELETE: if you can administer a secret, you can + * read it. + * + * Keys marked `variable` are non-secret by definition and are returned in full + * to anyone with workspace access — that disclosure is the entire point of the + * flag, and workspace membership was already checked by the caller. + * + * Returns the masked map alongside the per-key visibility so the client can + * render secrets and variables differently without a second request. */ async function maskWorkspaceEnvForViewer({ workspaceDecrypted, @@ -59,20 +70,23 @@ async function maskWorkspaceEnvForViewer({ workspaceId: string userId: string permission: PermissionType -}): Promise> { +}): Promise<{ masked: Record; visibility: Record }> { const workspaceKeys = Object.keys(workspaceDecrypted) - const { adminKeys } = await getWorkspaceEnvKeyAdminAccess({ + const { adminKeys, variableKeys } = await getWorkspaceEnvKeyAdminAccess({ workspaceId, envKeys: workspaceKeys, userId, }) const masked: Record = {} + const visibility: Record = {} for (const key of workspaceKeys) { - const canViewValue = permission === 'admin' || adminKeys.has(key) + const isVariable = variableKeys.has(key) + const canViewValue = isVariable || permission === 'admin' || adminKeys.has(key) masked[key] = canViewValue ? workspaceDecrypted[key] : '' + visibility[key] = isVariable ? 'variable' : 'secret' } - return masked + return { masked, visibility } } async function maskPersonalEnvForViewer({ @@ -128,18 +142,20 @@ export const GET = withRouteHandler( const { workspaceDecrypted, personalDecrypted, personalOwners, conflicts } = await getPersonalAndWorkspaceEnv(userId, workspaceId) - const workspace = await maskWorkspaceEnvForViewer({ - workspaceDecrypted, - workspaceId, - userId, - permission, - }) - const personal = await maskPersonalEnvForViewer({ - personalDecrypted, - personalOwners, - workspaceId, - userId, - }) + const [{ masked: workspace, visibility }, personal] = await Promise.all([ + maskWorkspaceEnvForViewer({ + workspaceDecrypted, + workspaceId, + userId, + permission, + }), + maskPersonalEnvForViewer({ + personalDecrypted, + personalOwners, + workspaceId, + userId, + }), + ]) return NextResponse.json( { @@ -147,6 +163,7 @@ export const GET = withRouteHandler( workspace, personal, conflicts, + visibility, }, }, { status: 200 } @@ -183,7 +200,7 @@ export const PUT = withRouteHandler( const parsed = await parseRequest(upsertWorkspaceEnvironmentContract, request, context) if (!parsed.success) return parsed.response - const { variables } = parsed.data.body + const { variables, visibility } = parsed.data.body const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) if (!permission) { @@ -191,7 +208,9 @@ export const PUT = withRouteHandler( } const incomingKeys = Object.keys(variables) - if (incomingKeys.length === 0) { + // A visibility-only request carries no values but is still a real mutation, + // so it must not take the no-op path below. + if (incomingKeys.length === 0 && !visibility) { return NextResponse.json({ success: true }) } const { adminKeys, knownKeys } = await getWorkspaceEnvKeyAdminAccess({ @@ -274,7 +293,41 @@ export const PUT = withRouteHandler( invalidateEffectiveDecryptedEnvCache({ workspaceId }) const newKeys = Object.keys(variables).filter((k) => !(k in existingEncrypted)) - await createWorkspaceEnvCredentials({ workspaceId, newKeys, actingUserId: userId }) + await createWorkspaceEnvCredentials({ + workspaceId, + newKeys, + actingUserId: userId, + visibilityByKey: visibility, + }) + + // Flips are applied after the credential rows exist so a create-as-variable + // in the same request lands on a row rather than silently no-opping. Only + // pre-existing keys can flip; brand-new ones got their policy above. + let flippedKeys: string[] = [] + if (visibility) { + const existingKeyUpdates = Object.fromEntries( + Object.entries(visibility).filter(([key]) => !newKeys.includes(key)) + ) + try { + const { changedKeys } = await setWorkspaceEnvVisibility({ + workspaceId, + updates: existingKeyUpdates, + actingUserId: userId, + }) + flippedKeys = changedKeys + } catch (error) { + if (error instanceof WorkspaceEnvVisibilityAccessError) { + logger.warn(`[${requestId}] Workspace env visibility change denied`, { + workspaceId, + userId, + keys: error.keys, + }) + return NextResponse.json({ error: error.message }, { status: 403 }) + } + throw error + } + if (flippedKeys.length > 0) invalidateEffectiveDecryptedEnvCache({ workspaceId }) + } recordAudit({ workspaceId, @@ -284,11 +337,19 @@ export const PUT = withRouteHandler( action: AuditAction.ENVIRONMENT_UPDATED, resourceType: AuditResourceType.ENVIRONMENT, resourceId: workspaceId, - description: `Updated ${Object.keys(variables).length} workspace environment variable(s)`, + description: + incomingKeys.length === 0 && flippedKeys.length > 0 + ? `Changed visibility of ${flippedKeys.length} workspace environment variable(s)` + : `Updated ${incomingKeys.length} workspace environment variable(s)`, metadata: { variableCount: Object.keys(variables).length, updatedKeys: Object.keys(variables), totalKeysAfterUpdate: Object.keys(merged).length, + // Disclosure changes are the security-relevant part of this audit + // entry: `secret -> variable` cannot be undone by flipping back. + ...(flippedKeys.length > 0 + ? { visibilityChangedKeys: flippedKeys, visibilityChanges: visibility } + : {}), }, request, }) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx index 9c5e61c8df1..4828bd01645 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx @@ -3,6 +3,7 @@ import type { ComponentProps, CSSProperties } from 'react' import { useState } from 'react' import { ChipInput } from '@sim/emcn' +import type { EnvVisibility } from '@/lib/api/contracts/environment' const BULLET = '\u2022' @@ -29,6 +30,12 @@ type SecretValueFieldProps = Omit< unmasked?: boolean /** Force read-only even when {@link canEdit} is true (e.g. a conflicted field). */ readOnly?: boolean + /** + * Disclosure policy for this key. `variable` means the value is non-secret and + * is shown in full to everyone — including viewers who cannot edit it, who + * would otherwise get the bullet mask. Defaults to `secret`. + */ + visibility?: EnvVisibility } /** @@ -48,6 +55,7 @@ export function SecretValueField({ canEdit = true, unmasked = false, readOnly = false, + visibility = 'secret', onFocus, onBlur, style, @@ -55,9 +63,12 @@ export function SecretValueField({ ...props }: SecretValueFieldProps) { const [focused, setFocused] = useState(false) + const nonSecret = visibility === 'variable' const editable = canEdit && !readOnly - const maskActive = canEdit && !unmasked && !focused - const displayValue = canEdit ? value : BULLET.repeat(VIEWER_MASK_LENGTH) + const maskActive = !nonSecret && canEdit && !unmasked && !focused + // A non-secret value is shown in full even to a viewer who cannot edit it — + // the bullet mask below is only for values the server withheld. + const displayValue = nonSecret || canEdit ? value : BULLET.repeat(VIEWER_MASK_LENGTH) const mergedStyle: CSSProperties | undefined = maskActive ? ({ ...style, WebkitTextSecurity: 'disc' } as CSSProperties) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx index 2ad13f3d873..2155f735a42 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx @@ -1,19 +1,19 @@ 'use client' import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' -import { ChipInput, cn, toast } from '@sim/emcn' +import { ChipConfirmModal, ChipInput, ChipTag, cn, toast } from '@sim/emcn' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' import { useParams, useRouter } from 'next/navigation' import { canMutateWorkspaceSettingsSection } from '@/components/settings/navigation' import { saveDiscardActions } from '@/components/settings/save-discard-actions' +import type { EnvVisibility } from '@/lib/api/contracts/environment' import { clearPendingCredentialCreateRequest, PENDING_CREDENTIAL_CREATE_REQUEST_EVENT, type PendingCredentialCreateRequest, readPendingCredentialCreateRequest, } from '@/lib/credentials/client-state' -import type { WorkspaceEnvironmentData } from '@/lib/environment/api' import { UnsavedChangesModal } from '@/app/workspace/[workspaceId]/components/credential-detail' import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu' import { SecretValueField } from '@/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field' @@ -52,13 +52,23 @@ interface SecretRowMenuProps { onViewDetails?: () => void /** Deletes the secret (or clears the draft row); omit when the caller can't delete. */ onDelete?: () => void + /** Flips the row between secret and non-secret; omit when the caller can't. */ + onToggleVisibility?: () => void + /** Current disclosure policy, used to label the toggle. */ + visibility?: EnvVisibility } /** * Trailing `...` actions menu for a secret row. Mirrors the Teammates / * Organization member menu so the settings experience is consistent. */ -function SecretRowMenu({ onCopyName, onViewDetails, onDelete }: SecretRowMenuProps) { +function SecretRowMenu({ + onCopyName, + onViewDetails, + onDelete, + onToggleVisibility, + visibility = 'secret', +}: SecretRowMenuProps) { return ( @@ -200,6 +219,10 @@ interface WorkspaceVariableRowProps { onValueChange: (key: string, value: string) => void onDelete: (key: string) => void onViewDetails?: (envKey: string) => void + /** Disclosure policy for this key; `variable` renders the value unmasked. */ + visibility: EnvVisibility + /** Flips the disclosure policy; omit when the caller can't administer the key. */ + onToggleVisibility?: (envKey: string, next: EnvVisibility) => void } function WorkspaceVariableRow({ @@ -216,6 +239,8 @@ function WorkspaceVariableRow({ onValueChange, onDelete, onViewDetails, + visibility, + onToggleVisibility, }: WorkspaceVariableRowProps) { /** * Salts the generated `name` attributes so password managers can't match them @@ -250,13 +275,23 @@ function WorkspaceVariableRow({ value={value} onChange={(next) => onValueChange(envKey, next)} canEdit={canEdit} + visibility={visibility} name={`workspace_env_value_${envKey}_${autofillSalt}`} /> - copyName(envKey)} - onViewDetails={hasCredential && onViewDetails ? () => onViewDetails(envKey) : undefined} - onDelete={canEdit ? () => onDelete(envKey) : undefined} - /> +
+ {visibility === 'variable' && Non-secret} + copyName(envKey)} + onViewDetails={hasCredential && onViewDetails ? () => onViewDetails(envKey) : undefined} + onDelete={canEdit ? () => onDelete(envKey) : undefined} + visibility={visibility} + onToggleVisibility={ + canEdit && onToggleVisibility + ? () => onToggleVisibility(envKey, visibility === 'variable' ? 'secret' : 'variable') + : undefined + } + /> +
) } @@ -352,19 +387,8 @@ export function SecretsManager() { const workspaceId = (params?.workspaceId as string) || '' const { data: personalEnvData, isLoading: isPersonalLoading } = usePersonalEnvironment() - const { data: workspaceEnvData, isLoading: isWorkspaceLoading } = useWorkspaceEnvironment( - workspaceId, - { - select: useCallback( - (data: WorkspaceEnvironmentData): WorkspaceEnvironmentData => ({ - workspace: data.workspace || {}, - personal: data.personal || {}, - conflicts: data.conflicts || [], - }), - [] - ), - } - ) + const { data: workspaceEnvData, isLoading: isWorkspaceLoading } = + useWorkspaceEnvironment(workspaceId) const savePersonalMutation = useSavePersonalEnvironment() const upsertWorkspaceMutation = useUpsertWorkspaceEnvironment() const removeWorkspaceMutation = useRemoveWorkspaceEnvironment() @@ -395,6 +419,10 @@ export function SecretsManager() { const [workspaceVars, setWorkspaceVars] = useState>({}) const [renamingKey, setRenamingKey] = useState(null) const [pendingKeyValue, setPendingKeyValue] = useState('') + const [pendingVisibilityChange, setPendingVisibilityChange] = useState<{ + envKey: string + next: EnvVisibility + } | null>(null) const initialWorkspaceVarsRef = useRef>({}) const scrollContainerRef = useRef(null) const initialVarsRef = useRef([]) @@ -412,6 +440,29 @@ export function SecretsManager() { return map }, [workspaceEnvCredentials]) + /** + * Applies a confirmed disclosure change. Sent as a visibility-only PUT — the + * value is untouched, so re-sending it would only mean a redundant re-encrypt. + */ + const handleConfirmVisibilityChange = useCallback(async () => { + if (!pendingVisibilityChange || !workspaceId) return + const { envKey, next } = pendingVisibilityChange + setPendingVisibilityChange(null) + try { + await upsertWorkspaceMutation.mutateAsync({ + workspaceId, + variables: {}, + visibility: { [envKey]: next }, + }) + toast.success( + next === 'variable' ? `${envKey} is now a non-secret variable` : `${envKey} is now a secret` + ) + } catch (error) { + logger.error('Failed to change secret visibility', { error }) + toast.error(error instanceof Error ? error.message : 'Failed to change visibility') + } + }, [pendingVisibilityChange, workspaceId, upsertWorkspaceMutation]) + const filteredEnvVars = useMemo(() => { const mapped = envVars.map((envVar, index) => ({ envVar, originalIndex: index })) if (!searchTerm.trim()) return mapped @@ -1026,6 +1077,12 @@ export function SecretsManager() { onViewDetails={ canCreateWorkspaceSecret && cred ? handleViewDetails : undefined } + visibility={workspaceEnvData?.visibility?.[key] ?? 'secret'} + onToggleVisibility={ + cred + ? (envKey, next) => setPendingVisibilityChange({ envKey, next }) + : undefined + } /> ) })} @@ -1072,6 +1129,45 @@ export function SecretsManager() { )} + {/* + The gate for this change is server-side; this dialog's job is the copy. + Turning a secret into a variable is a disclosure that flipping back does + not undo, and flipping back is therefore not remediation — rotation is. + */} + { + if (!open) setPendingVisibilityChange(null) + }} + title={ + pendingVisibilityChange?.next === 'variable' + ? 'Make this value visible?' + : 'Make this value secret?' + } + text={ + pendingVisibilityChange?.next === 'variable' + ? [ + 'Everyone in this workspace, and Sim, will be able to read ', + { text: pendingVisibilityChange?.envKey ?? '', bold: true }, + '. Its value will also appear in run logs and traces. Switching it back later will not undo this.', + ] + : [ + 'New runs will hide ', + { text: pendingVisibilityChange?.envKey ?? '', bold: true }, + ' again, but its value has already been visible in logs, traces, and Sim conversations. ', + { text: 'Rotate the value if it is sensitive.', error: true }, + ] + } + confirm={{ + label: pendingVisibilityChange?.next === 'variable' ? 'Make visible' : 'Make secret', + onClick: handleConfirmVisibilityChange, + // Making a value public is the consequential direction, so it keeps + // the destructive default; hiding it again is a plain primary. + variant: pendingVisibilityChange?.next === 'variable' ? 'destructive' : 'primary', + pending: upsertWorkspaceMutation.isPending, + }} + /> + = ({ // React Query hooks for environment variables const { data: personalEnv = {} } = usePersonalEnvironment() - const { data: workspaceEnvData } = useWorkspaceEnvironment(workspaceId || '', { - select: useCallback( - (data: WorkspaceEnvironmentData): WorkspaceEnvironmentData => ({ - workspace: data.workspace || {}, - personal: data.personal || {}, - conflicts: data.conflicts || [], - }), - [] - ), - }) + const { data: workspaceEnvData } = useWorkspaceEnvironment(workspaceId || '') const userEnvVars = Object.keys(personalEnv) const [selectedIndex, setSelectedIndex] = useState(0) diff --git a/apps/sim/background/webhook-execution.ts b/apps/sim/background/webhook-execution.ts index bb3ba290468..3d6708e4103 100644 --- a/apps/sim/background/webhook-execution.ts +++ b/apps/sim/background/webhook-execution.ts @@ -598,6 +598,7 @@ async function executeWebhookJobInternal( personalDecrypted: secretEnvironment.personalDecrypted, workspaceDecrypted: secretEnvironment.workspaceDecrypted, decryptionFailures: secretEnvironment.decryptionFailures, + nonSecretNames: new Set(secretEnvironment.workspaceVariableKeys), scope: secretScope, }) } catch (error) { diff --git a/apps/sim/background/workflow-column-execution.ts b/apps/sim/background/workflow-column-execution.ts index 4bc37bb3fd7..fb9a3e8295d 100644 --- a/apps/sim/background/workflow-column-execution.ts +++ b/apps/sim/background/workflow-column-execution.ts @@ -54,7 +54,10 @@ import { type QueuedWorkflowGroupCellPayload, type WorkflowGroupCellPayload, } from '@/lib/table/workflow-columns' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' export type { WorkflowGroupCellPayload } @@ -576,7 +579,11 @@ async function runWorkflowAndWriteTerminal( ], { userId: enrichmentBillingAttribution.actorUserId, workspaceId } ) - const enrichmentRegistry = new ResolvedSecretTraceRegistry([], inputProvenance.scope) + const enrichmentRegistry = new ResolvedSecretTraceRegistry( + [], + inputProvenance.scope, + EMPTY_NON_SECRET_NAMES + ) await enrichmentRegistry.importCrossingProvenance(inputProvenance, enrichInputs, { trusted: true, }) @@ -961,7 +968,11 @@ async function runWorkflowAndWriteTerminal( ], { userId: workflowRecord.userId, workspaceId } ) - const inputRegistry = new ResolvedSecretTraceRegistry([], rowInputProvenance.scope) + const inputRegistry = new ResolvedSecretTraceRegistry( + [], + rowInputProvenance.scope, + EMPTY_NON_SECRET_NAMES + ) await inputRegistry.importCrossingProvenance(rowInputProvenance, input, { trusted: true }) progressWriter = createWorkflowCellProgressWriter({ diff --git a/apps/sim/enrichments/run.test.ts b/apps/sim/enrichments/run.test.ts index d4cf951e6a1..8199f35bc7e 100644 --- a/apps/sim/enrichments/run.test.ts +++ b/apps/sim/enrichments/run.test.ts @@ -8,7 +8,10 @@ vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) import { runEnrichment, skippedEnrichmentDetail } from '@/enrichments/run' import type { EnrichmentConfig, EnrichmentProvider } from '@/enrichments/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const ICON = (() => null) as unknown as EnrichmentConfig['icon'] @@ -96,7 +99,7 @@ describe('runEnrichment cascade detail', () => { }) it('threads the isolated row provenance registry through each provider tool call', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) mockExecuteTool.mockResolvedValue({ success: true, output: { email: 'j@acme.com' } }) await runEnrichment( diff --git a/apps/sim/executor/execution/block-executor.test.ts b/apps/sim/executor/execution/block-executor.test.ts index 74755b004f1..1f9be78729d 100644 --- a/apps/sim/executor/execution/block-executor.test.ts +++ b/apps/sim/executor/execution/block-executor.test.ts @@ -12,7 +12,10 @@ import type { DAGNode } from '@/executor/dag/builder' import { BlockExecutor } from '@/executor/execution/block-executor' import { ExecutionState } from '@/executor/execution/state' import type { BlockHandler, ExecutionContext } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { VariableResolver } from '@/executor/variables/resolver' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' @@ -367,9 +370,11 @@ describe('BlockExecutor', () => { state ) const ctx = createContext(state) - ctx.resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-api-key' }, - ]) + ctx.resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-api-key' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) await expect(executor.execute(ctx, createNode(block), block)).resolves.toEqual(output) expect(state.getBlockOutput(block.id)).toEqual(output) @@ -418,7 +423,8 @@ describe('BlockExecutor', () => { const onBlockComplete = vi.fn(async () => {}) const registry = new ResolvedSecretTraceRegistry( [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-secret' }], - { userId: 'user-1', workspaceId: 'workspace-1' } + { userId: 'user-1', workspaceId: 'workspace-1' }, + EMPTY_NON_SECRET_NAMES ) const executor = new BlockExecutor( [ @@ -463,9 +469,11 @@ describe('BlockExecutor', () => { const state = new ExecutionState() const resolver = new VariableResolver(workflow, {}, state) const onBlockComplete = vi.fn(async () => {}) - const registry = new ResolvedSecretTraceRegistry([ - { name: 'SHORT_SECRET', plaintext: 'Test', encryptedValue: 'encrypted-test' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'SHORT_SECRET', plaintext: 'Test', encryptedValue: 'encrypted-test' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) const handler: BlockHandler = { canHandle: () => true, execute: async (blockContext, block) => { @@ -695,13 +703,17 @@ describe('BlockExecutor', () => { parallels: {}, } const state = new ExecutionState() - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'OPENAI_API_KEY', - plaintext: secret, - encryptedValue: 'encrypted-openai-api-key', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'OPENAI_API_KEY', + plaintext: secret, + encryptedValue: 'encrypted-openai-api-key', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) const resolver = new VariableResolver(workflow, {}, state) const syntaxError = 'Syntax Error: Line 1: `return {{OPENAI_API_KEY}}` - Invalid or unexpected token' @@ -820,13 +832,17 @@ describe('BlockExecutor', () => { state ) const ctx = createContext(state) - ctx.resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry([ - { - name: 'API_KEY', - plaintext: secret, - encryptedValue: 'encrypted-api-key', - }, - ]) + ctx.resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry( + [ + { + name: 'API_KEY', + plaintext: secret, + encryptedValue: 'encrypted-api-key', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) const node = createNode(block) node.outgoingEdges.set('error-edge', { target: 'error-handler', sourceHandle: EDGE.ERROR }) @@ -1034,9 +1050,11 @@ describe('BlockExecutor streaming pump', () => { }) const { executor, block, state } = createExecutor(handler) const ctx = createContext(state) - ctx.resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-api-key' }, - ]) + ctx.resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-api-key' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) ctx.onStream = async (streamingExec) => { const reader = streamingExec.stream.getReader() try { @@ -1083,9 +1101,11 @@ describe('BlockExecutor streaming pump', () => { const { executor, block, state } = createExecutor(handler) block.config.params = { responseFormat: 'json' } const ctx = createContext(state) - ctx.resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-api-key' }, - ]) + ctx.resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-api-key' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) await executor.execute(ctx, createNode(block), block) diff --git a/apps/sim/executor/execution/engine.test.ts b/apps/sim/executor/execution/engine.test.ts index f8bb0aaf4e8..aadabee0bca 100644 --- a/apps/sim/executor/execution/engine.test.ts +++ b/apps/sim/executor/execution/engine.test.ts @@ -35,7 +35,10 @@ import type { DAG, DAGNode } from '@/executor/dag/builder' import type { EdgeManager } from '@/executor/execution/edge-manager' import type { NodeExecutionOrchestrator } from '@/executor/orchestrators/node' import type { ExecutionContext } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import type { SerializedBlock } from '@/serializer/types' import { ExecutionEngine } from './engine' @@ -200,9 +203,11 @@ describe('ExecutionEngine', () => { it('persists the selected final block provenance instead of run-global matches', async () => { const node = createMockNode('function', 'function') - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'Test', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'Test', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'Test') const context = createMockContext({ decisions: { router: new Map(), condition: new Map() }, @@ -1010,9 +1015,11 @@ describe('ExecutionEngine', () => { const dag = createMockDAG([startNode, errorNode]) const context = createMockContext({ - resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-api-key' }, - ]), + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-api-key' }], + undefined, + EMPTY_NON_SECRET_NAMES + ), }) const edgeManager = createMockEdgeManager((node) => { if (node.id === 'start') return ['error-node'] diff --git a/apps/sim/executor/execution/snapshot-serializer.test.ts b/apps/sim/executor/execution/snapshot-serializer.test.ts index 075d3fc5636..1f8d9bd4ecf 100644 --- a/apps/sim/executor/execution/snapshot-serializer.test.ts +++ b/apps/sim/executor/execution/snapshot-serializer.test.ts @@ -6,7 +6,10 @@ import type { DAG, DAGNode } from '@/executor/dag/builder' import { EdgeManager } from '@/executor/execution/edge-manager' import { serializePauseSnapshot } from '@/executor/execution/snapshot-serializer' import type { ExecutionContext } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' function createContext(overrides: Partial = {}): ExecutionContext { return { @@ -40,9 +43,11 @@ function createContext(overrides: Partial = {}): ExecutionCont describe('serializePauseSnapshot', () => { it('persists encrypted resolved-secret provenance and the source execution id', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'raw-secret', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'raw-secret', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'raw-secret') const context = createContext({ resolvedSecretTraceRegistry: registry }) @@ -60,10 +65,14 @@ describe('serializePauseSnapshot', () => { }) it('persists a complete zero-entry provenance state for a fresh execution', () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) const snapshot = serializePauseSnapshot( createContext({ resolvedSecretTraceRegistry: registry }), @@ -135,9 +144,11 @@ describe('serializePauseSnapshot', () => { }) it('does not persist a temporary activation guard as permanent incompleteness', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'raw-secret', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'raw-secret', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'raw-secret') const completePendingActivation = registry.beginPendingActivation() diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index 19cb8423617..a7cf9537b34 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -23,7 +23,10 @@ import { getAllBlocks } from '@/blocks' import { AGENT, BlockType, isMcpTool } from '@/executor/constants' import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler' import type { ExecutionContext, StreamingExecution } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { executeProviderRequest } from '@/providers' import { installStreamingCostPolicy } from '@/providers/cost-policy' import { SIM_AUTO_MODEL_ID } from '@/providers/models' @@ -184,7 +187,11 @@ describe('AgentBlockHandler', () => { version: '1.0.0', loops: {}, } as SerializedWorkflow, - resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ), } mockGetProviderFromModel.mockReturnValue('mock-provider') @@ -2646,13 +2653,17 @@ describe('AgentBlockHandler', () => { } it('projects provider errors and internal runtime identifiers before logging', () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'TOKEN', - plaintext: 'diagnostic-secret', - encryptedValue: 'encrypted-diagnostic-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'TOKEN', + plaintext: 'diagnostic-secret', + encryptedValue: 'encrypted-diagnostic-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'diagnostic-secret') const ctx = { ...mockContext, resolvedSecretTraceRegistry: registry } @@ -2708,13 +2719,17 @@ describe('AgentBlockHandler', () => { }) it('projects tool diagnostics without logging code or raw params', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'TOKEN', - plaintext: 'tool-secret', - encryptedValue: 'encrypted-tool-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'TOKEN', + plaintext: 'tool-secret', + encryptedValue: 'encrypted-tool-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'tool-secret') const ctx = { ...mockContext, resolvedSecretTraceRegistry: registry } vi.spyOn(handler as never, 'createCustomTool' as never).mockRejectedValueOnce( @@ -2785,13 +2800,17 @@ describe('AgentBlockHandler', () => { }) it('projects malformed model content and response format only in diagnostics', () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'TOKEN', - plaintext: 'format-secret', - encryptedValue: 'encrypted-format-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'TOKEN', + plaintext: 'format-secret', + encryptedValue: 'encrypted-format-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'format-secret') const ctx = { ...mockContext, resolvedSecretTraceRegistry: registry } const content = 'not-json format-secret __var_TOKEN __sim_runtime_test_1' diff --git a/apps/sim/executor/handlers/agent/memory.test.ts b/apps/sim/executor/handlers/agent/memory.test.ts index d6a6e90c72c..3999666f341 100644 --- a/apps/sim/executor/handlers/agent/memory.test.ts +++ b/apps/sim/executor/handlers/agent/memory.test.ts @@ -18,7 +18,10 @@ import { hashDurableSecretProvenanceValue } from '@/lib/execution/durable-secret import { MEMORY } from '@/executor/constants' import { Memory } from '@/executor/handlers/agent/memory' import type { Message } from '@/executor/handlers/agent/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const mockMemoryLogger = vi.mocked(loggerMock.createLogger).mock.results[ vi.mocked(loggerMock.createLogger).mock.calls.findIndex(([name]) => name === 'Memory') @@ -246,9 +249,11 @@ describe('Memory', () => { } it('does not reinterpret dormant catalog values as secret-bearing memory', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockRedactObjectStrings.mockImplementationOnce(async (content: unknown) => { expect(content).toBe('Bearer secret-value') return content @@ -267,7 +272,7 @@ describe('Memory', () => { }) it('does not write memory when projection fails closed', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() const appendMessage = vi .spyOn(memoryService as any, 'appendMessage') @@ -283,9 +288,11 @@ describe('Memory', () => { }) it('preserves legacy stored messages when no current resolution activated the value', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) vi.spyOn(memoryService as any, 'fetchMemory').mockResolvedValue({ messages: [{ role: 'assistant', content: 'old secret-value' }], provenance: { status: 'exact', entries: [] }, @@ -301,10 +308,14 @@ describe('Memory', () => { it('keeps raw foreign-scope content in functional storage', async () => { mockDecryptSecret.mockResolvedValueOnce({ decrypted: 'foreign-secret' }) - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) await registry.importProvenance( { version: 1, @@ -326,9 +337,11 @@ describe('Memory', () => { it.each(['123', 'true'])( 'projects low-entropy secret %s only in model text and arguments', async (secret) => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) const converted = secret === '123' ? 123 : true const message: Message = { @@ -404,9 +417,11 @@ describe('Memory', () => { it.each(['name', 'functionName', 'toolCallId', 'toolName'] as const)( 'rejects an active resolved secret in the %s control field', (field) => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'control-secret', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'control-secret', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'control-secret') const message = { role: 'assistant', @@ -437,10 +452,14 @@ describe('Memory', () => { it('does not activate provenance from a message dropped by the selected window', async () => { const oldSecretMessage: Message = { role: 'user', content: 'same-value' } const retainedPublicMessage: Message = { role: 'assistant', content: 'same-value' } - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) vi.spyOn(memoryService as any, 'fetchMemory').mockResolvedValueOnce({ messages: [oldSecretMessage, retainedPublicMessage], provenance: { @@ -470,13 +489,17 @@ describe('Memory', () => { describe('secret-safe diagnostics', () => { it('never logs conversation IDs while retaining structural memory metadata', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'TOKEN', - plaintext: 'conversation-secret', - encryptedValue: 'encrypted-conversation-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'TOKEN', + plaintext: 'conversation-secret', + encryptedValue: 'encrypted-conversation-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) const ctx = { workspaceId: 'workspace-1', resolvedSecretTraceRegistry: registry, diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts index 1483c5d9fe0..28c7073d4e1 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts @@ -40,7 +40,10 @@ import { import { BlockType } from '@/executor/constants' import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler' import type { ExecutionContext } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' @@ -206,23 +209,27 @@ describe('EvaluatorBlockHandler', () => { const contentSecret = 'resolved-evaluator-content' const metricSecret = 'resolved-evaluator-metric' const credentialSecret = 'resolved-evaluator-credential' - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'CONTENT_SECRET', - plaintext: contentSecret, - encryptedValue: 'encrypted-evaluator-content', - }, - { - name: 'METRIC_SECRET', - plaintext: metricSecret, - encryptedValue: 'encrypted-evaluator-metric', - }, - { - name: 'API_KEY', - plaintext: credentialSecret, - encryptedValue: 'encrypted-evaluator-credential', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'CONTENT_SECRET', + plaintext: contentSecret, + encryptedValue: 'encrypted-evaluator-content', + }, + { + name: 'METRIC_SECRET', + plaintext: metricSecret, + encryptedValue: 'encrypted-evaluator-metric', + }, + { + name: 'API_KEY', + plaintext: credentialSecret, + encryptedValue: 'encrypted-evaluator-credential', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('CONTENT_SECRET', contentSecret) registry.recordResolved('METRIC_SECRET', metricSecret) registry.recordResolved('API_KEY', credentialSecret) @@ -611,13 +618,17 @@ describe('EvaluatorBlockHandler', () => { it('projects evaluator failures before logging without changing the thrown error', async () => { const providerError = 'provider echoed resolved-evaluator-secret __var_CONTENT_SECRET' - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'CONTENT_SECRET', - plaintext: 'resolved-evaluator-secret', - encryptedValue: 'encrypted-evaluator-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'CONTENT_SECRET', + plaintext: 'resolved-evaluator-secret', + encryptedValue: 'encrypted-evaluator-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('CONTENT_SECRET', 'resolved-evaluator-secret') mockContext.resolvedSecretTraceRegistry = registry mockFetch.mockResolvedValueOnce({ diff --git a/apps/sim/executor/handlers/pi/pi-handler.test.ts b/apps/sim/executor/handlers/pi/pi-handler.test.ts index a236123c412..2fa928fc077 100644 --- a/apps/sim/executor/handlers/pi/pi-handler.test.ts +++ b/apps/sim/executor/handlers/pi/pi-handler.test.ts @@ -108,7 +108,10 @@ vi.mock('@/blocks/utils', () => ({ import { PiBlockHandler, parsePiReviewMentions } from '@/executor/handlers/pi/pi-handler' import type { ExecutionContext, StreamingExecution } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import type { SerializedBlock } from '@/serializer/types' const block = { id: 'blk', metadata: { id: 'pi' } } as unknown as SerializedBlock @@ -118,7 +121,11 @@ function ctx(overrides: Partial = {}): ExecutionContext { workflowId: 'wf', workspaceId: 'ws', userId: 'user', - resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ), ...overrides, } as ExecutionContext } @@ -188,9 +195,11 @@ describe('PiBlockHandler', () => { }) it('projects activated task secrets at the final Pi input boundary', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('API_KEY', 'secret-value') await handler.execute( @@ -207,7 +216,7 @@ describe('PiBlockHandler', () => { [ 'incomplete', (() => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() return registry })(), diff --git a/apps/sim/executor/handlers/pi/search/tool.test.ts b/apps/sim/executor/handlers/pi/search/tool.test.ts index 30ec5f97b83..9961e3b47f9 100644 --- a/apps/sim/executor/handlers/pi/search/tool.test.ts +++ b/apps/sim/executor/handlers/pi/search/tool.test.ts @@ -17,10 +17,17 @@ import { PARALLEL_EMPTY_RESULTS_ERROR, } from '@/executor/handlers/pi/search/tool' import type { ExecutionContext } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' function executionContext( - registry: ResolvedSecretTraceRegistry | undefined = new ResolvedSecretTraceRegistry() + registry: ResolvedSecretTraceRegistry | undefined = new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ) ): ExecutionContext { return { executionId: 'exec-1', @@ -114,9 +121,11 @@ describe('buildPiSearchToolSpec', () => { it('projects named provenance before normalizing and serializing provider output', async () => { const secret = 'quoted"\\secret\nnext-line' - const registry = new ResolvedSecretTraceRegistry([ - { name: 'SEARCH_QUERY', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'SEARCH_QUERY', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('SEARCH_QUERY', secret) const mergeSpy = vi.spyOn(registry, 'mergeToolCallRegistry') const context = executionContext(registry) @@ -150,9 +159,11 @@ describe('buildPiSearchToolSpec', () => { }) it('preserves an unrelated low-entropy result absent from this search call inputs', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'LOW_ENTROPY', plaintext: 'Test', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'LOW_ENTROPY', plaintext: 'Test', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('LOW_ENTROPY', 'Test') mockExecuteTool.mockResolvedValue({ success: true, @@ -177,7 +188,7 @@ describe('buildPiSearchToolSpec', () => { }) it('projects anonymous provenance learned by the isolated search call', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) mockExecuteTool.mockImplementation(async (_toolId, _params, options) => { vi.spyOn(options.resolvedSecretTraceRegistry, 'getModelEgressSnapshot').mockReturnValue({ complete: true, @@ -207,7 +218,7 @@ describe('buildPiSearchToolSpec', () => { [ 'incomplete', (() => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() return registry })(), @@ -224,7 +235,7 @@ describe('buildPiSearchToolSpec', () => { }) it('does not merge an incomplete search call registry or return its output', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const mergeSpy = vi.spyOn(registry, 'mergeToolCallRegistry') mockExecuteTool.mockImplementation(async (_toolId, _params, options) => { options.resolvedSecretTraceRegistry.markIncomplete() diff --git a/apps/sim/executor/handlers/pi/sim-tools.test.ts b/apps/sim/executor/handlers/pi/sim-tools.test.ts index 2e1760cfbcc..90c09fe2455 100644 --- a/apps/sim/executor/handlers/pi/sim-tools.test.ts +++ b/apps/sim/executor/handlers/pi/sim-tools.test.ts @@ -15,7 +15,10 @@ vi.mock('@/tools/utils.server', () => ({ getToolAsync: vi.fn() })) import { buildSimToolSpecs } from '@/executor/handlers/pi/sim-tools' import type { ExecutionContext } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { ToolSchemaEnrichmentError } from '@/tools/params' function executionContext(registry: ResolvedSecretTraceRegistry | undefined): ExecutionContext { @@ -26,7 +29,7 @@ function executionContext(registry: ResolvedSecretTraceRegistry | undefined): Ex } function completeExecutionContext(): ExecutionContext { - return executionContext(new ResolvedSecretTraceRegistry()) + return executionContext(new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES)) } const toolInput = [{ type: 'exa', operation: 'exa_search', usageControl: 'auto' }] @@ -104,7 +107,11 @@ describe('buildSimToolSpecs', () => { workspaceId: 'ws-1', workflowId: 'wf-1', userId: 'user-1', - resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ), } as ExecutionContext const [spec] = await buildSimToolSpecs(trustedCtx, [ @@ -126,9 +133,11 @@ describe('buildSimToolSpecs', () => { success: true, output: { authorization: 'Bearer secret-value' }, }) - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('API_KEY', 'secret-value') const [spec] = await buildSimToolSpecs(executionContext(registry), toolInput) @@ -151,7 +160,7 @@ describe('buildSimToolSpecs', () => { output: { token: 'foreign-secret' }, } }) - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const [spec] = await buildSimToolSpecs(executionContext(registry), toolInput) @@ -179,9 +188,11 @@ describe('buildSimToolSpecs', () => { it('does not rewrite an unrelated result that collides with low-entropy run provenance', async () => { mockToolAdapter() mockExecuteTool.mockResolvedValue({ success: true, output: 'Test' }) - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TEST_SECRET', plaintext: 'Test', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TEST_SECRET', plaintext: 'Test', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TEST_SECRET', 'Test') const [spec] = await buildSimToolSpecs(executionContext(registry), toolInput) @@ -191,9 +202,11 @@ describe('buildSimToolSpecs', () => { it('projects error text returned or thrown by a Sim tool', async () => { mockToolAdapter({ apiKey: 'secret-value' }) - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('API_KEY', 'secret-value') const [spec] = await buildSimToolSpecs(executionContext(registry), toolInput) @@ -219,7 +232,7 @@ describe('buildSimToolSpecs', () => { [ 'incomplete', (() => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() return registry })(), diff --git a/apps/sim/executor/handlers/router/router-handler.test.ts b/apps/sim/executor/handlers/router/router-handler.test.ts index db605abaa1a..d262f79f290 100644 --- a/apps/sim/executor/handlers/router/router-handler.test.ts +++ b/apps/sim/executor/handlers/router/router-handler.test.ts @@ -41,7 +41,10 @@ import { generateRouterPrompt, generateRouterV2Prompt } from '@/blocks/blocks/ro import { BlockType } from '@/executor/constants' import { RouterBlockHandler } from '@/executor/handlers/router/router-handler' import type { ExecutionContext } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { getProviderFromModel } from '@/providers/utils' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' @@ -246,18 +249,22 @@ describe('RouterBlockHandler', () => { it('sends only model-visible legacy router provenance and excludes credentials', async () => { const promptSecret = 'resolved-router-prompt' const credentialSecret = 'resolved-router-credential' - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'PROMPT_SECRET', - plaintext: promptSecret, - encryptedValue: 'encrypted-router-prompt', - }, - { - name: 'API_KEY', - plaintext: credentialSecret, - encryptedValue: 'encrypted-router-credential', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'PROMPT_SECRET', + plaintext: promptSecret, + encryptedValue: 'encrypted-router-prompt', + }, + { + name: 'API_KEY', + plaintext: credentialSecret, + encryptedValue: 'encrypted-router-credential', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('PROMPT_SECRET', promptSecret) registry.recordResolved('API_KEY', credentialSecret) mockContext.resolvedSecretTraceRegistry = registry @@ -653,18 +660,22 @@ describe('RouterBlockHandler V2', () => { it('sends only model-visible router V2 provenance and excludes credentials', async () => { const contextSecret = 'resolved-router-v2-context' const credentialSecret = 'resolved-router-v2-credential' - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'CONTEXT_SECRET', - plaintext: contextSecret, - encryptedValue: 'encrypted-router-v2-context', - }, - { - name: 'API_KEY', - plaintext: credentialSecret, - encryptedValue: 'encrypted-router-v2-credential', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'CONTEXT_SECRET', + plaintext: contextSecret, + encryptedValue: 'encrypted-router-v2-context', + }, + { + name: 'API_KEY', + plaintext: credentialSecret, + encryptedValue: 'encrypted-router-v2-credential', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('CONTEXT_SECRET', contextSecret) registry.recordResolved('API_KEY', credentialSecret) mockContext.resolvedSecretTraceRegistry = registry diff --git a/apps/sim/executor/handlers/workflow/custom-block-tool-runner.test.ts b/apps/sim/executor/handlers/workflow/custom-block-tool-runner.test.ts index 85f519ca400..56849339e7d 100644 --- a/apps/sim/executor/handlers/workflow/custom-block-tool-runner.test.ts +++ b/apps/sim/executor/handlers/workflow/custom-block-tool-runner.test.ts @@ -19,7 +19,10 @@ import { buildCustomBlockExecutionContext, runCustomBlockTool, } from '@/executor/handlers/workflow/custom-block-tool-runner' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const mockRunnerLogger = vi.mocked(createLogger).mock.results[ @@ -98,9 +101,11 @@ describe('runCustomBlockTool', () => { it('does not log a secret-bearing child workflow error with or without provenance', async () => { const secret = 'custom-block-child-secret-value' const message = `${secret} __var_API_KEY __sim_code_0_binding_0` - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockExecute.mockRejectedValue(new Error(message)) const projected = await runCustomBlockTool( @@ -181,7 +186,7 @@ describe('buildCustomBlockExecutionContext cancellation', () => { describe('buildCustomBlockExecutionContext secret provenance', () => { it('carries the server-only parent registry without putting it in model parameters', () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const ctx = buildCustomBlockExecutionContext( { workspaceId: 'ws-1' }, diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index ea7dfcc04d3..162fbf4b277 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -18,6 +18,7 @@ import { import type { ExecutionContext } from '@/executor/types' import { ANONYMOUS_SECRET_TRACE_REPLACEMENT, + EMPTY_NON_SECRET_NAMES, ResolvedSecretTraceRegistry, } from '@/executor/utils/resolved-secret-trace-registry' import type { SerializedBlock } from '@/serializer/types' @@ -1259,7 +1260,7 @@ describe('WorkflowBlockHandler', () => { ], } }) - const parentRegistry = new ResolvedSecretTraceRegistry() + const parentRegistry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const result = await handler.execute( customBlockContext({ resolvedSecretTraceRegistry: parentRegistry }), customBlock(), @@ -1554,7 +1555,7 @@ describe('WorkflowBlockHandler', () => { }) it('leaves regular workflow blocks entirely alone', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const ctx = { ...mockContext, workspaceId: 'workspace-1', diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index c3cd00b84d6..33ea100706b 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -440,6 +440,10 @@ export class WorkflowBlockHandler implements BlockHandler { personalDecrypted: ownerEnv.personalDecrypted, workspaceDecrypted: ownerEnv.workspaceDecrypted, decryptionFailures: ownerEnv.decryptionFailures, + // Recomputed from the source owner's own environment, never inherited + // from the parent: a name that is non-secret in the parent workspace + // may well be a secret in this one. + nonSecretNames: new Set(ownerEnv.workspaceVariableKeys), scope: { userId: loadUserId, workspaceId: sourceWorkspaceId }, }) if (ctx.resolvedSecretTraceRegistry) { diff --git a/apps/sim/executor/orchestrators/loop.test.ts b/apps/sim/executor/orchestrators/loop.test.ts index 67af5313573..91198703231 100644 --- a/apps/sim/executor/orchestrators/loop.test.ts +++ b/apps/sim/executor/orchestrators/loop.test.ts @@ -11,7 +11,10 @@ import type { EdgeManager } from '@/executor/execution/edge-manager' import type { BlockStateController } from '@/executor/execution/types' import { LoopOrchestrator } from '@/executor/orchestrators/loop' import type { ExecutionContext } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const { mockExecuteInIsolatedVM, mockUploadFile } = vi.hoisted(() => ({ mockExecuteInIsolatedVM: vi.fn(), @@ -220,13 +223,17 @@ describe('LoopOrchestrator', () => { } const orchestrator = new LoopOrchestrator(dag, createState(), resolver as any) const ctx = createContext() - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'FOREACH_SECRET', - plaintext: resolvedSecret, - encryptedValue: 'encrypted-foreach-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'FOREACH_SECRET', + plaintext: resolvedSecret, + encryptedValue: 'encrypted-foreach-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) ctx.resolvedSecretTraceRegistry = registry await expect(orchestrator.initializeLoopScope(ctx, loopId)).rejects.toThrow( @@ -383,13 +390,17 @@ describe('LoopOrchestrator', () => { condition: '', } as Record const ctx = createContext(scope) - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'CONDITION_SECRET', - plaintext: resolvedSecret, - encryptedValue: 'encrypted-condition-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'CONDITION_SECRET', + plaintext: resolvedSecret, + encryptedValue: 'encrypted-condition-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('CONDITION_SECRET', resolvedSecret) ctx.resolvedSecretTraceRegistry = registry diff --git a/apps/sim/executor/utils/resolved-secret-content-projection.test.ts b/apps/sim/executor/utils/resolved-secret-content-projection.test.ts index aa984a98801..7b553dcb14f 100644 --- a/apps/sim/executor/utils/resolved-secret-content-projection.test.ts +++ b/apps/sim/executor/utils/resolved-secret-content-projection.test.ts @@ -9,17 +9,24 @@ import { projectResolvedSecretModelJsonContent, projectResolvedSecretModelJsonStrings, } from '@/executor/utils/resolved-secret-content-projection' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' describe('projectResolvedSecretModelContent', () => { it('projects activated literals in values, keys, errors, and output streams', () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'API_KEY', - plaintext: 'quoted"secret\\with\nnewline', - encryptedValue: 'encrypted-value', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'API_KEY', + plaintext: 'quoted"secret\\with\nnewline', + encryptedValue: 'encrypted-value', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('API_KEY', 'quoted"secret\\with\nnewline') const input = { 'key-quoted"secret\\with\nnewline': { @@ -51,9 +58,11 @@ describe('projectResolvedSecretModelContent', () => { }) it('ignores sibling pending work but fails closed for permanent or missing registry state', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'secret-value') const finish = registry.beginPendingActivation() @@ -74,9 +83,11 @@ describe('projectResolvedSecretModelContent', () => { }) it('never emits internal runtime binding aliases', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'secret-value') const projection = projectResolvedSecretModelContent( @@ -93,7 +104,7 @@ describe('projectResolvedSecretModelContent', () => { }) it('preserves foreign internal-looking text that the execution did not register', () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const projection = projectResolvedSecretModelContent( { @@ -125,7 +136,7 @@ describe('projectResolvedSecretModelContent', () => { }) it('preserves an unregistered opaque-placeholder-shaped literal', () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) expect(projectResolvedSecretModelContent('{{[REDACTED_SECRET]}}', registry)).toEqual({ safe: true, @@ -134,14 +145,18 @@ describe('projectResolvedSecretModelContent', () => { }) it('keeps longest-match semantics when a known opaque placeholder is nested in a secret', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'Test', plaintext: 'Test', encryptedValue: 'test-ciphertext' }, - { - name: 'COMPOSITE', - plaintext: 'x{{Test}}y', - encryptedValue: 'composite-ciphertext', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'Test', plaintext: 'Test', encryptedValue: 'test-ciphertext' }, + { + name: 'COMPOSITE', + plaintext: 'x{{Test}}y', + encryptedValue: 'composite-ciphertext', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('Test', 'Test') registry.recordResolved('COMPOSITE', 'x{{Test}}y') @@ -152,11 +167,15 @@ describe('projectResolvedSecretModelContent', () => { }) it('projects exact typed primitive secrets without rewriting unrelated primitives', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'NUMBER', plaintext: '123', encryptedValue: 'number-ciphertext' }, - { name: 'BOOLEAN', plaintext: 'true', encryptedValue: 'boolean-ciphertext' }, - { name: 'NULL', plaintext: 'null', encryptedValue: 'null-ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'NUMBER', plaintext: '123', encryptedValue: 'number-ciphertext' }, + { name: 'BOOLEAN', plaintext: 'true', encryptedValue: 'boolean-ciphertext' }, + { name: 'NULL', plaintext: 'null', encryptedValue: 'null-ciphertext' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('NUMBER', '123') registry.recordResolved('BOOLEAN', 'true') registry.recordResolved('NULL', 'null') @@ -187,9 +206,11 @@ describe('projectResolvedSecretModelContent', () => { }) it.each(['123', 'true'])('keeps projected JSON argument strings valid (%s)', (secret) => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) const typedValue = secret === '123' ? 123 : true @@ -208,9 +229,11 @@ describe('projectResolvedSecretModelContent', () => { }) it('is stable when a secret literal overlaps its own provenance alias', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'TOKEN', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'TOKEN', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'TOKEN') const first = projectResolvedSecretModelContent('Bearer TOKEN', registry) @@ -220,9 +243,11 @@ describe('projectResolvedSecretModelContent', () => { }) it('preserves the canonical provenance label when its name equals the secret plaintext', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('Test', 'Test') expect( @@ -245,9 +270,11 @@ describe('projectResolvedSecretModelContent', () => { }) it('atomically projects the selected provenance label when its name contains the value', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'TOK', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'TOK', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'TOK') expect(projectResolvedSecretModelContent('Bearer {{TOKEN}}', registry)).toEqual({ @@ -257,7 +284,7 @@ describe('projectResolvedSecretModelContent', () => { }) it('fails closed when provenance-derived matcher patterns exceed capacity', () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const getModelEgressSnapshot = vi.spyOn(registry, 'getModelEgressSnapshot').mockReturnValue({ complete: true, matches: [ @@ -274,9 +301,11 @@ describe('projectResolvedSecretModelContent', () => { }) it('keeps provenance-shaped content deterministic without trusting it as a protocol handle', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('Test', 'Test') expect(projectResolvedSecretModelContent('{{Test}}', registry)).toEqual({ @@ -291,7 +320,7 @@ describe('projectResolvedSecretModelContent', () => { describe('projectResolvedSecretModelJsonContent', () => { it('normalizes dates using their JSON wire representation', () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const createdAt = new Date('2026-08-05T12:34:56.789Z') expect(projectResolvedSecretModelJsonContent({ createdAt }, registry)).toEqual({ @@ -302,9 +331,11 @@ describe('projectResolvedSecretModelJsonContent', () => { }) it('projects active secrets emitted by toJSON after materialization', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'secret-value') expect( @@ -321,7 +352,7 @@ describe('projectResolvedSecretModelJsonContent', () => { }) it('does not invoke JSON serialization when provenance is incomplete', () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() const toJSON = vi.fn(() => ({ value: 'untrusted' })) @@ -330,7 +361,7 @@ describe('projectResolvedSecretModelJsonContent', () => { }) it('uses native JSON semantics for undefined and non-finite numbers', () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) expect( projectResolvedSecretModelJsonContent( @@ -353,7 +384,7 @@ describe('projectResolvedSecretModelJsonContent', () => { }) it('returns a controlled unsafe result for values JSON cannot serialize', () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const cyclic: Record = {} cyclic.self = cyclic @@ -362,9 +393,11 @@ describe('projectResolvedSecretModelJsonContent', () => { }) it('enforces the byte limit after secret aliases are projected', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'X', plaintext: 'x', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'X', plaintext: 'x', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('X', 'x') expect(projectResolvedSecretModelJsonContent({ a: 'x' }, registry, 9)).toEqual({ safe: false }) @@ -374,9 +407,11 @@ describe('projectResolvedSecretModelJsonContent', () => { describe('projectResolvedSecretDiagnosticError', () => { it('projects plaintext and internal aliases without mutating the runtime error', () => { const secret = 'diagnostic-secret-value' - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('API_KEY', secret) const message = `request failed: ${secret} __var_API_KEY __sim_code_4_binding_2` const error = new Error(message) @@ -399,9 +434,11 @@ describe('projectResolvedSecretDiagnosticError', () => { it('uses a known compiler alias as diagnostic-only provenance', () => { const secret = 'diagnostic-secret-value' - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) const error = new Error(`request failed: ${secret} __var_API_KEY`) expect(projectResolvedSecretDiagnosticError(error, registry)).toEqual( @@ -411,9 +448,11 @@ describe('projectResolvedSecretDiagnosticError', () => { }) it('falls back to text-free diagnostics for unknown or runtime-only aliases', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) expect( projectResolvedSecretDiagnosticError(new Error('secret-value __var_UNKNOWN'), registry) @@ -428,7 +467,7 @@ describe('projectResolvedSecretDiagnosticError', () => { it('falls back to text-free structure when provenance is missing or incomplete', () => { const error = new Error('secret __var_API_KEY') - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() expect(projectResolvedSecretDiagnosticError(error, undefined)).toEqual({ diff --git a/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts b/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts index 91269f82c6a..c1d15025317 100644 --- a/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts +++ b/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts @@ -11,6 +11,7 @@ vi.mock('@/lib/core/security/encryption', () => ({ import { ANONYMOUS_SECRET_TRACE_REPLACEMENT, createResolvedSecretTraceRegistry, + EMPTY_NON_SECRET_NAMES, isResolvedSecretTraceProvenanceV1, RESOLVED_SECRET_TRACE_CHECKPOINT_VERSION, ResolvedSecretTraceProvenanceAccumulator, @@ -149,9 +150,11 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('starts with an inert catalog and activates only an exact successful resolution', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-value' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-value' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) expect(registry.getActiveMatches()).toEqual([]) expect(registry.recordResolved('API_KEY', 'wrong-value')).toBe(false) @@ -166,9 +169,11 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('keeps dormant catalog values out of model-egress snapshots', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-value' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-value' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) expect(registry.getActiveMatches()).toEqual([]) const snapshot = registry.getModelEgressSnapshot() @@ -179,9 +184,11 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('does not invalidate the model matcher for duplicate activations', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-value' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-value' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) expect(registry.recordResolved('API_KEY', 'secret-value')).toBe(true) const revision = registry.getModelEgressRevision() @@ -190,13 +197,17 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('fails model egress closed when activated provenance cannot be compiled into a matcher', () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'OVERSIZED', - plaintext: 's'.repeat(64 * 1024 + 1), - encryptedValue: 'encrypted-value', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'OVERSIZED', + plaintext: 's'.repeat(64 * 1024 + 1), + encryptedValue: 'encrypted-value', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) expect(registry.isComplete()).toBe(true) expect(registry.recordResolved('OVERSIZED', 's'.repeat(64 * 1024 + 1))).toBe(true) @@ -210,7 +221,9 @@ describe('ResolvedSecretTraceRegistry', () => { name: `SECRET_${index}`, plaintext: `${prefix}${suffix}`, encryptedValue: `encrypted-${index}`, - })) + })), + undefined, + EMPTY_NON_SECRET_NAMES ) expect(registry.isComplete()).toBe(true) @@ -226,7 +239,8 @@ describe('ResolvedSecretTraceRegistry', () => { mockDecryptSecret.mockResolvedValueOnce({ decrypted: 'same-secret' }) const registry = new ResolvedSecretTraceRegistry( [{ name: 'LOCAL', plaintext: 'same-secret', encryptedValue: 'local-ciphertext' }], - { userId: 'user-1', workspaceId: 'workspace-1' } + { userId: 'user-1', workspaceId: 'workspace-1' }, + EMPTY_NON_SECRET_NAMES ) await registry.importProvenance( { @@ -249,9 +263,11 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('projects committed provenance while temporary activations are pending', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-value' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-value' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) const completeFirst = registry.beginPendingActivation() const completeSecond = registry.beginPendingActivation() @@ -293,10 +309,14 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('seeds a tool child only with active provenance present in that tool input', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'INPUT', plaintext: 'input-secret', encryptedValue: 'input-ciphertext' }, - { name: 'UNRELATED', plaintext: 'Test', encryptedValue: 'unrelated-ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'INPUT', plaintext: 'input-secret', encryptedValue: 'input-ciphertext' }, + { name: 'UNRELATED', plaintext: 'Test', encryptedValue: 'unrelated-ciphertext' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('INPUT', 'input-secret') registry.recordResolved('UNRELATED', 'Test') @@ -309,11 +329,15 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('forks independent roots without treating static param names or array indexes as data', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'PROMPT', plaintext: 'prompt', encryptedValue: 'prompt-ciphertext' }, - { name: 'ZERO', plaintext: '0', encryptedValue: 'zero-ciphertext' }, - { name: 'VALUE', plaintext: 'input-secret', encryptedValue: 'value-ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'PROMPT', plaintext: 'prompt', encryptedValue: 'prompt-ciphertext' }, + { name: 'ZERO', plaintext: '0', encryptedValue: 'zero-ciphertext' }, + { name: 'VALUE', plaintext: 'input-secret', encryptedValue: 'value-ciphertext' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('PROMPT', 'prompt') registry.recordResolved('ZERO', '0') registry.recordResolved('VALUE', 'input-secret') @@ -337,6 +361,7 @@ describe('ResolvedSecretTraceRegistry', () => { workspaceEncrypted: { SHARED: 'workspace-encrypted' }, personalDecrypted: { SHARED: 'personal-secret' }, workspaceDecrypted: { SHARED: 'workspace-secret' }, + nonSecretNames: EMPTY_NON_SECRET_NAMES, }) expect(registry.recordResolved('SHARED', 'workspace-secret')).toBe(true) @@ -354,6 +379,7 @@ describe('ResolvedSecretTraceRegistry', () => { personalDecrypted: { FAILED: '', DECRYPTED_ONLY: 'not-catalogued' }, workspaceDecrypted: {}, decryptionFailures: ['FAILED'], + nonSecretNames: EMPTY_NON_SECRET_NAMES, }) expect(registry.isComplete()).toBe(true) @@ -370,6 +396,7 @@ describe('ResolvedSecretTraceRegistry', () => { personalDecrypted: { SHARED: '' }, workspaceDecrypted: { SHARED: 'workspace-secret' }, decryptionFailures: ['SHARED'], + nonSecretNames: EMPTY_NON_SECRET_NAMES, }) expect(registry.recordResolved('SHARED', 'workspace-secret')).toBe(true) @@ -379,9 +406,11 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('exports encrypted active provenance without plaintext', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'raw-secret', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'raw-secret', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'raw-secret') const serialized = JSON.stringify(registry.exportProvenance()) @@ -405,6 +434,7 @@ describe('ResolvedSecretTraceRegistry', () => { restoredCheckpointVersion: RESOLVED_SECRET_TRACE_CHECKPOINT_VERSION, restoreTrusted: true, requireRestoredProvenance: true, + nonSecretNames: EMPTY_NON_SECRET_NAMES, }) registry.recordResolved('TOKEN', 'new-secret') @@ -427,6 +457,7 @@ describe('ResolvedSecretTraceRegistry', () => { workspaceDecrypted: {}, restoreTrusted: true, requireRestoredProvenance: true, + nonSecretNames: EMPTY_NON_SECRET_NAMES, }) expect(registry.isComplete()).toBe(true) @@ -443,6 +474,7 @@ describe('ResolvedSecretTraceRegistry', () => { workspaceDecrypted: {}, restoreTrusted: true, requireRestoredProvenance: true, + nonSecretNames: EMPTY_NON_SECRET_NAMES, }) expect(registry.isComplete()).toBe(true) @@ -455,7 +487,7 @@ describe('ResolvedSecretTraceRegistry', () => { complete: true, entries: [{ name: 'TOKEN', encryptedValue: 'ciphertext' }], } - const untrusted = new ResolvedSecretTraceRegistry() + const untrusted = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) expect(await untrusted.importProvenance(provenance, { trusted: false })).toBe(false) expect(untrusted.isComplete()).toBe(false) expect(mockDecryptSecret).not.toHaveBeenCalled() @@ -468,22 +500,23 @@ describe('ResolvedSecretTraceRegistry', () => { restoredCheckpointVersion: RESOLVED_SECRET_TRACE_CHECKPOINT_VERSION, requireRestoredProvenance: true, restoreTrusted: true, + nonSecretNames: EMPTY_NON_SECRET_NAMES, }) expect(missing.isComplete()).toBe(false) - const malformed = new ResolvedSecretTraceRegistry() + const malformed = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) expect(await malformed.importProvenance({ version: 1 }, { trusted: true })).toBe(false) expect(malformed.isComplete()).toBe(false) mockDecryptSecret.mockRejectedValueOnce(new Error('cannot decrypt')) - const undecryptable = new ResolvedSecretTraceRegistry() + const undecryptable = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) expect(await undecryptable.importProvenance(provenance, { trusted: true })).toBe(false) expect(undecryptable.isComplete()).toBe(false) }) it('uses anonymous replacements for cross-scope provenance', async () => { mockDecryptSecret.mockResolvedValueOnce({ decrypted: 'publisher-secret' }) - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) await registry.importProvenance( { version: 1, @@ -508,23 +541,43 @@ describe('ResolvedSecretTraceRegistry', () => { entries: [{ name: 'TOKEN', encryptedValue: 'ciphertext' }], scope: { userId: 'user-1', workspaceId: 'workspace-1' }, } - const sameScope = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) - const mismatchedScope = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-2', - }) - const differentUserSameWorkspace = new ResolvedSecretTraceRegistry([], { - userId: 'user-2', - workspaceId: 'workspace-1', - }) - const missingReceiverScope = new ResolvedSecretTraceRegistry() - const missingSourceScope = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const sameScope = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) + const mismatchedScope = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-2', + }, + EMPTY_NON_SECRET_NAMES + ) + const differentUserSameWorkspace = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-2', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) + const missingReceiverScope = new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ) + const missingSourceScope = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) expect(await sameScope.importProvenance(provenance, { trusted: true })).toBe(true) expect(await mismatchedScope.importProvenance(provenance, { trusted: true })).toBe(true) @@ -558,10 +611,14 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('filters same-scope provenance to the exact crossing value while preserving its name', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) const provenance: ResolvedSecretTraceProvenanceV1 = { version: 1, complete: true, @@ -586,10 +643,14 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('filters and anonymizes provenance crossing from another scope', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) const provenance: ResolvedSecretTraceProvenanceV1 = { version: 1, complete: true, @@ -618,7 +679,7 @@ describe('ResolvedSecretTraceRegistry', () => { it('filters an authenticated model-input envelope to the exact crossing value', async () => { const scope = { userId: 'user-1', workspaceId: 'workspace-1' } - const registry = new ResolvedSecretTraceRegistry([], scope) + const registry = new ResolvedSecretTraceRegistry([], scope, EMPTY_NON_SECRET_NAMES) const provenance: ResolvedSecretTraceProvenanceV1 = { version: 1, complete: true, @@ -643,7 +704,7 @@ describe('ResolvedSecretTraceRegistry', () => { it('imports exact authenticated provenance from a JSON-encoded crossing value', async () => { const scope = { userId: 'user-1', workspaceId: 'workspace-1' } - const registry = new ResolvedSecretTraceRegistry([], scope) + const registry = new ResolvedSecretTraceRegistry([], scope, EMPTY_NON_SECRET_NAMES) const encryptedValue = 'quote"-ciphertext' const plaintext = `decrypted:${encryptedValue}` @@ -663,11 +724,15 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('exports only active secrets whose exact literals cross a value boundary', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'PRESENT', plaintext: 'present-secret', encryptedValue: 'present-ciphertext' }, - { name: 'ABSENT', plaintext: 'absent-secret', encryptedValue: 'absent-ciphertext' }, - { name: 'UNUSED', plaintext: 'unused-secret', encryptedValue: 'unused-ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'PRESENT', plaintext: 'present-secret', encryptedValue: 'present-ciphertext' }, + { name: 'ABSENT', plaintext: 'absent-secret', encryptedValue: 'absent-ciphertext' }, + { name: 'UNUSED', plaintext: 'unused-secret', encryptedValue: 'unused-ciphertext' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('PRESENT', 'present-secret') registry.recordResolved('ABSENT', 'absent-secret') @@ -685,9 +750,11 @@ describe('ResolvedSecretTraceRegistry', () => { it('exports active provenance when a model-bound JSON string contains escaped secret bytes', () => { const secret = 'quote" slash\\ newline\n' - const registry = new ResolvedSecretTraceRegistry([ - { name: 'PRESENT', plaintext: secret, encryptedValue: 'present-ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'PRESENT', plaintext: secret, encryptedValue: 'present-ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('PRESENT', secret) expect( @@ -702,12 +769,16 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('exports active numeric, boolean, and null literals crossing a value boundary', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'NUMBER', plaintext: '1234', encryptedValue: 'number-ciphertext' }, - { name: 'BOOLEAN', plaintext: 'false', encryptedValue: 'boolean-ciphertext' }, - { name: 'NULL', plaintext: 'null', encryptedValue: 'null-ciphertext' }, - { name: 'ABSENT', plaintext: '5678', encryptedValue: 'absent-ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'NUMBER', plaintext: '1234', encryptedValue: 'number-ciphertext' }, + { name: 'BOOLEAN', plaintext: 'false', encryptedValue: 'boolean-ciphertext' }, + { name: 'NULL', plaintext: 'null', encryptedValue: 'null-ciphertext' }, + { name: 'ABSENT', plaintext: '5678', encryptedValue: 'absent-ciphertext' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('NUMBER', '1234') registry.recordResolved('BOOLEAN', 'false') registry.recordResolved('NULL', 'null') @@ -730,9 +801,11 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('marks a bounded cross-boundary scan incomplete when an enumerable accessor is opaque', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'secret') const value = {} Object.defineProperty(value, 'opaque', { @@ -748,9 +821,11 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('does not claim a complete cross-boundary scan for opaque large-value refs', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'secret') expect( @@ -768,9 +843,11 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('does not snapshot or enqueue an entire wide crossing object', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'needle-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'needle-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'needle-value') const wideValue: Record = {} for (let index = 0; index < 30_000; index++) { @@ -790,12 +867,16 @@ describe('ResolvedSecretTraceRegistry', () => { }) it('handles duplicate and empty values deterministically', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'Z_TOKEN', plaintext: 'same', encryptedValue: 'z-ciphertext' }, - { name: 'A_TOKEN', plaintext: 'same', encryptedValue: 'a-ciphertext' }, - { name: 'EMPTY', plaintext: '', encryptedValue: 'empty-ciphertext' }, - { name: 'A', plaintext: 'A', encryptedValue: 'short-ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'Z_TOKEN', plaintext: 'same', encryptedValue: 'z-ciphertext' }, + { name: 'A_TOKEN', plaintext: 'same', encryptedValue: 'a-ciphertext' }, + { name: 'EMPTY', plaintext: '', encryptedValue: 'empty-ciphertext' }, + { name: 'A', plaintext: 'A', encryptedValue: 'short-ciphertext' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('Z_TOKEN', 'same') registry.recordResolved('A_TOKEN', 'same') registry.recordResolved('EMPTY', '') @@ -815,7 +896,7 @@ describe('ResolvedSecretTraceRegistry', () => { plaintext: `value-${index}`, encryptedValue: `ciphertext-${index}`, })) - const registry = new ResolvedSecretTraceRegistry(entries) + const registry = new ResolvedSecretTraceRegistry(entries, undefined, EMPTY_NON_SECRET_NAMES) for (const entry of entries) { registry.recordResolved(entry.name, entry.plaintext) @@ -837,9 +918,11 @@ describe('ResolvedSecretTraceRegistry', () => { expect(Buffer.byteLength(JSON.stringify(provenance), 'utf8')).toBeGreaterThan(8 * 1024 * 1024) expect(isResolvedSecretTraceProvenanceV1(provenance)).toBe(false) - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret', encryptedValue }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret', encryptedValue }], + undefined, + EMPTY_NON_SECRET_NAMES + ) expect(registry.recordResolved('TOKEN', 'secret')).toBe(true) expect(registry.isComplete()).toBe(false) expect(registry.exportProvenance().entries).toEqual([]) @@ -905,7 +988,11 @@ describe('ResolvedSecretTraceRegistry', () => { } } - const registry = new ResolvedSecretTraceRegistry(catalogEntries()) + const registry = new ResolvedSecretTraceRegistry( + catalogEntries(), + undefined, + EMPTY_NON_SECRET_NAMES + ) expect(yieldedEntries).toBe(10_001) expect(registry.isComplete()).toBe(false) @@ -914,16 +1001,132 @@ describe('ResolvedSecretTraceRegistry', () => { it('marks an oversized dormant catalog value incomplete without retaining it', () => { const oversizedPlaintext = 'x'.repeat(8 * 1024 * 1024) - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'OVERSIZED', - plaintext: oversizedPlaintext, - encryptedValue: 'ciphertext', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'OVERSIZED', + plaintext: oversizedPlaintext, + encryptedValue: 'ciphertext', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) expect(registry.isComplete()).toBe(false) expect(registry.recordResolved('OVERSIZED', oversizedPlaintext)).toBe(false) expect(registry.getActiveMatches()).toEqual([]) }) }) + +describe('ResolvedSecretTraceRegistry non-secret exemption', () => { + beforeEach(() => { + vi.clearAllMocks() + mockDecryptSecret.mockImplementation(async (encryptedValue: string) => ({ + decrypted: `decrypted:${encryptedValue}`, + })) + }) + + /** + * The regression this whole mechanism exists to prevent. Resolvers call + * `recordResolved` for every `{{NAME}}` they substitute and cannot tell a + * variable from a secret. Without the exempt branch the name would miss the + * catalog and trip `markIncomplete()`, which is permanent — collapsing the + * run's traces to structure-only and omitting every later copilot tool result. + */ + it('does not mark the registry incomplete when a non-secret name resolves', () => { + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-value' }], + undefined, + new Set(['SUPPORT_EMAIL']) + ) + + expect(registry.recordResolved('SUPPORT_EMAIL', 'help@acme.com')).toBe(false) + expect(registry.isComplete()).toBe(true) + expect(registry.getActiveMatches()).toEqual([]) + }) + + it('never activates or exports a non-secret name', async () => { + const registry = await createResolvedSecretTraceRegistry({ + personalEncrypted: {}, + workspaceEncrypted: { SUPPORT_EMAIL: 'encrypted-email', API_KEY: 'encrypted-key' }, + personalDecrypted: {}, + workspaceDecrypted: { SUPPORT_EMAIL: 'help@acme.com', API_KEY: 'sk-live' }, + nonSecretNames: new Set(['SUPPORT_EMAIL']), + }) + + registry.recordResolved('SUPPORT_EMAIL', 'help@acme.com') + registry.recordResolved('API_KEY', 'sk-live') + + expect(registry.isComplete()).toBe(true) + expect(registry.getActiveMatches()).toEqual([ + { plaintext: 'sk-live', replacement: '{{API_KEY}}' }, + ]) + + const provenance = registry.exportProvenance() + expect(provenance.entries.map((entry) => entry.name)).toEqual(['API_KEY']) + }) + + it('still poisons the registry for an unknown SECRET name', () => { + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-value' }], + undefined, + new Set(['SUPPORT_EMAIL']) + ) + + expect(registry.recordResolved('UNKNOWN', 'whatever')).toBe(false) + expect(registry.isComplete()).toBe(false) + }) + + /** + * The exempt check runs before the catalog lookup precisely so a non-secret + * whose decryption failed — and is therefore absent from the catalog for a + * second, unrelated reason — cannot take the poison path. + */ + it('does not poison when a non-secret value failed to decrypt', async () => { + const registry = await createResolvedSecretTraceRegistry({ + personalEncrypted: {}, + workspaceEncrypted: { SUPPORT_EMAIL: 'encrypted-email' }, + personalDecrypted: {}, + workspaceDecrypted: { SUPPORT_EMAIL: '' }, + decryptionFailures: ['SUPPORT_EMAIL'], + nonSecretNames: new Set(['SUPPORT_EMAIL']), + }) + + expect(registry.recordResolved('SUPPORT_EMAIL', 'help@acme.com')).toBe(false) + expect(registry.isComplete()).toBe(true) + }) + + /** + * Documents an accepted limitation: exemption is by NAME, while projection + * matches by BYTES. A non-secret that collides in value with a real secret is + * still replaced. Safe (it over-redacts), but it also means a `write` member + * could use it as a confirmation oracle for a guessed low-entropy secret. + */ + it('still redacts a non-secret whose value collides with an active secret', () => { + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'shared-value', encryptedValue: 'encrypted-key' }], + undefined, + new Set(['SUPPORT_EMAIL']) + ) + + registry.recordResolved('API_KEY', 'shared-value') + registry.recordResolved('SUPPORT_EMAIL', 'shared-value') + + expect(registry.getActiveMatches()).toEqual([ + { plaintext: 'shared-value', replacement: '{{API_KEY}}' }, + ]) + }) + + it('inherits the exempt set into a tool-call fork', () => { + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-value', encryptedValue: 'encrypted-value' }], + undefined, + new Set(['SUPPORT_EMAIL']) + ) + + const fork = registry.forkForToolCall() + expect(fork.recordResolved('SUPPORT_EMAIL', 'help@acme.com')).toBe(false) + expect(fork.isComplete()).toBe(true) + }) +}) diff --git a/apps/sim/executor/utils/resolved-secret-trace-registry.ts b/apps/sim/executor/utils/resolved-secret-trace-registry.ts index 00b2414f5e9..981910b1bcc 100644 --- a/apps/sim/executor/utils/resolved-secret-trace-registry.ts +++ b/apps/sim/executor/utils/resolved-secret-trace-registry.ts @@ -69,6 +69,14 @@ export interface ExportResolvedSecretTraceProvenanceForValueOptions { anonymous?: boolean } +/** + * The exempt set for a registry that has no non-secret values. + * + * Named rather than inlined as `new Set()` so a call site that genuinely has + * nothing to exempt reads as a decision, not an omission. + */ +export const EMPTY_NON_SECRET_NAMES: ReadonlySet = new Set() + export interface CreateResolvedSecretTraceRegistryOptions { personalEncrypted: Record workspaceEncrypted: Record @@ -80,6 +88,23 @@ export interface CreateResolvedSecretTraceRegistryOptions { restoreTrusted?: boolean requireRestoredProvenance?: boolean scope?: ResolvedSecretTraceScopeV1 + /** + * Env names explicitly marked non-secret, which are exempt from redaction: + * kept out of the catalog so they can never activate, and short-circuited in + * {@link ResolvedSecretTraceRegistry.recordResolved} so resolving one does not + * poison the registry. + * + * Required — never optional with an empty default. A missing exempt set makes + * a variable behave as a secret (over-redaction, safe); a *wrong* one makes a + * secret behave as a variable (silent disclosure). Forcing every construction + * site to name its source turns the dangerous direction into a decision + * someone had to write down, and a newly added site into a compile error. + * + * Derived locally from the workspace's stored visibility on each side of every + * boundary — deliberately never carried in provenance, so a peer cannot assert + * that one of your secrets is non-secret. + */ + nonSecretNames: ReadonlySet } function compareStrings(left: string, right: string): number { @@ -242,6 +267,10 @@ function buildEffectiveCatalogEntry( name: string, encryptedValue: string ): ResolvedSecretTraceCatalogEntry | undefined { + // Non-secret values never enter the catalog, so they can never be activated, + // never enter provenance, and never reach the projection matcher. Covers both + // catalog loops, since every entry is built here. + if (options.nonSecretNames.has(name)) return undefined const plaintext = hasOwn(options.workspaceDecrypted, name) ? options.workspaceDecrypted[name] : options.personalDecrypted[name] @@ -437,13 +466,23 @@ export class ResolvedSecretTraceRegistry { private pendingActivations = 0 private modelEgressRevision = 0 private readonly scope?: ResolvedSecretTraceScopeV1 + private readonly nonSecretNames: ReadonlySet private readonly completeProvenanceEnvelopeBytes: number + /** + * Every parameter is required by design. `nonSecretNames` in particular has no + * default: see {@link CreateResolvedSecretTraceRegistryOptions.nonSecretNames} + * for why a silently-empty exempt set is the failure mode worth a compile + * error. Pass {@link EMPTY_NON_SECRET_NAMES} when there is genuinely nothing + * to exempt. + */ constructor( - catalogEntries: Iterable = [], - scope?: ResolvedSecretTraceScopeV1 + catalogEntries: Iterable, + scope: ResolvedSecretTraceScopeV1 | undefined, + nonSecretNames: ReadonlySet ) { this.scope = scope ? cloneProvenanceScope(scope) : undefined + this.nonSecretNames = nonSecretNames this.completeProvenanceEnvelopeBytes = serializedProvenanceEnvelopeByteSize(true, this.scope) if (this.completeProvenanceEnvelopeBytes > MAX_SERIALIZED_PROVENANCE_BYTES) { this.markIncomplete() @@ -468,7 +507,14 @@ export class ResolvedSecretTraceRegistry { * available to later calls in the run. */ forkForToolCall(): ResolvedSecretTraceRegistry { - const fork = new ResolvedSecretTraceRegistry(this.catalog.values(), this.scope) + // A fork is the same execution under a narrower lens, so it inherits the + // exempt set: without it, a `{{VARIABLE}}` resolved inside a tool call would + // miss the catalog and poison the fork. + const fork = new ResolvedSecretTraceRegistry( + this.catalog.values(), + this.scope, + this.nonSecretNames + ) for (const entry of this.activeEntries.values()) { fork.addActiveEntry({ ...entry }) } @@ -492,7 +538,14 @@ export class ResolvedSecretTraceRegistry { * but nested object keys remain user-controlled input. */ forkForToolInputValues(values: Iterable): ResolvedSecretTraceRegistry { - const fork = new ResolvedSecretTraceRegistry(this.catalog.values(), this.scope) + // A fork is the same execution under a narrower lens, so it inherits the + // exempt set: without it, a `{{VARIABLE}}` resolved inside a tool call would + // miss the catalog and poison the fork. + const fork = new ResolvedSecretTraceRegistry( + this.catalog.values(), + this.scope, + this.nonSecretNames + ) if (!this.complete) { fork.markIncomplete() return fork @@ -564,6 +617,17 @@ export class ResolvedSecretTraceRegistry { /** Activates a configured secret only when the resolved runtime value matches its catalog value. */ recordResolved(name: string, resolvedValue: string): boolean { if (resolvedValue.length === 0) return false + + // Checked BEFORE the catalog lookup, and this ordering is load bearing. + // Resolvers call `recordResolved` for every `{{NAME}}` they substitute and + // cannot tell a variable from a secret, so a known non-secret name would + // otherwise fall through to the `markIncomplete()` below — which is + // permanent, and would collapse the whole run's traces to structure-only and + // omit every subsequent copilot tool result. Checking first also keeps a + // variable whose decryption failed (and so is absent from the catalog for a + // second reason) off that same poison path. + if (this.nonSecretNames.has(name)) return false + const catalogEntry = this.catalog.get(name) if (!catalogEntry || catalogEntry.plaintext !== resolvedValue) { this.markIncomplete() @@ -623,7 +687,14 @@ export class ResolvedSecretTraceRegistry { return false } - const sourceRegistry = new ResolvedSecretTraceRegistry([], provenance.scope) + // Deliberately EMPTY, not `this.nonSecretNames`: this throwaway decrypts and + // narrows provenance asserted by a *foreign* scope, whose values must not be + // exempted by this workspace's visibility settings. + const sourceRegistry = new ResolvedSecretTraceRegistry( + [], + provenance.scope, + EMPTY_NON_SECRET_NAMES + ) const sourceImported = await sourceRegistry.importProvenance(provenance, { trusted: true }) const filteredProvenance = sourceRegistry.exportProvenanceForValue(value) const filteredImported = await this.importProvenance(filteredProvenance, { trusted: true }) @@ -1121,7 +1192,8 @@ export async function createResolvedSecretTraceRegistry( const failedNames = new Set(options.decryptionFailures ?? []) const registry = new ResolvedSecretTraceRegistry( iterateEffectiveCatalogEntries(options, failedNames), - options.scope + options.scope, + options.nonSecretNames ) if (options.restoredProvenance !== undefined) { @@ -1139,11 +1211,16 @@ export async function createResolvedSecretTraceRegistry( return registry } -/** Creates a scoped fail-closed registry when trusted catalog provenance is unavailable. */ +/** + * Creates a scoped fail-closed registry when trusted catalog provenance is unavailable. + * + * Takes no exempt set: the registry is permanently incomplete, so every consumer + * already redacts maximally and no exemption could change the outcome. + */ export function createIncompleteResolvedSecretTraceRegistry( scope?: ResolvedSecretTraceScopeV1 ): ResolvedSecretTraceRegistry { - const registry = new ResolvedSecretTraceRegistry([], scope) + const registry = new ResolvedSecretTraceRegistry([], scope, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() return registry } diff --git a/apps/sim/executor/variables/resolver.test.ts b/apps/sim/executor/variables/resolver.test.ts index ccec07b2aa4..778349152b0 100644 --- a/apps/sim/executor/variables/resolver.test.ts +++ b/apps/sim/executor/variables/resolver.test.ts @@ -10,7 +10,10 @@ import { import { BlockType } from '@/executor/constants' import { ExecutionState } from '@/executor/execution/state' import type { ExecutionContext } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { VariableResolver } from '@/executor/variables/resolver' import { navigatePathAsync } from '@/executor/variables/resolvers/reference-async.server' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' @@ -168,9 +171,11 @@ describe('VariableResolver function block inputs', () => { it('records a secret reached through workflow-variable indirection', async () => { const { ctx, resolver } = createResolver() - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'resolved-secret', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'resolved-secret', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) ctx.workflowVariables = { 'var-1': { id: 'var-1', name: 'indirect', type: 'string', value: '{{TOKEN}}' }, } @@ -267,9 +272,11 @@ describe('VariableResolver function block inputs', () => { 'preserves an exact-name/exact-value secret in %s source until the execution boundary', async (language) => { const { block, ctx, resolver } = createResolver(language) - const registry = new ResolvedSecretTraceRegistry([ - { name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) const source = 'return {{Test}}' ctx.environmentVariables = { Test: 'Test' } ctx.resolvedSecretTraceRegistry = registry diff --git a/apps/sim/executor/variables/resolvers/block.test.ts b/apps/sim/executor/variables/resolvers/block.test.ts index c13e43f0464..3bab09318e9 100644 --- a/apps/sim/executor/variables/resolvers/block.test.ts +++ b/apps/sim/executor/variables/resolvers/block.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it, vi } from 'vitest' import { compactExecutionPayload } from '@/lib/execution/payloads/serializer' import { ExecutionState } from '@/executor/execution/state' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { BlockResolver } from './block' import { RESOLVED_EMPTY, type ResolutionContext } from './reference' @@ -627,7 +630,7 @@ describe('BlockResolver', () => { const workflow = createTestWorkflow([{ id: 'source' }]) const resolver = new BlockResolver(workflow) const ctx = createTestContext('current') - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) ctx.executionContext.resolvedSecretTraceRegistry = registry ctx.executionState.setBlockOutput('source', { result: 'secret-value' }, 0, { version: 1, diff --git a/apps/sim/executor/variables/resolvers/env.test.ts b/apps/sim/executor/variables/resolvers/env.test.ts index 72f72e65b30..58861661512 100644 --- a/apps/sim/executor/variables/resolvers/env.test.ts +++ b/apps/sim/executor/variables/resolvers/env.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { EnvResolver } from './env' import type { ResolutionContext } from './reference' @@ -51,9 +54,11 @@ describe('EnvResolver', () => { describe('resolve', () => { it('records only successful Secrets-tab substitutions', () => { const resolver = new EnvResolver() - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: 'secret-api-key', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'secret-api-key', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) const ctx = createTestContext({ API_KEY: 'secret-api-key' }) ctx.executionContext.resolvedSecretTraceRegistry = registry diff --git a/apps/sim/executor/variables/resolvers/workflow.test.ts b/apps/sim/executor/variables/resolvers/workflow.test.ts index 4d884b532a3..2a1b909e452 100644 --- a/apps/sim/executor/variables/resolvers/workflow.test.ts +++ b/apps/sim/executor/variables/resolvers/workflow.test.ts @@ -4,7 +4,10 @@ import { isLargeArrayManifest, } from '@/lib/execution/payloads/large-array-manifest' import { compactExecutionPayload } from '@/lib/execution/payloads/serializer' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { navigatePathAsync } from '@/executor/variables/resolvers/reference-async.server' import type { ResolutionContext } from './reference' import { WorkflowResolver } from './workflow' @@ -183,7 +186,7 @@ describe('WorkflowResolver', () => { const variables = { 'var-1': { id: 'var-1', name: 'token', type: 'plain', value: 'secret-value' }, } - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const context = createTestContext(variables) context.executionContext.resolvedSecretTraceRegistry = registry context.executionContext.workflowVariableResolvedSecretTraceProvenance = { diff --git a/apps/sim/hooks/queries/environment.ts b/apps/sim/hooks/queries/environment.ts index 29e74661ab1..99a2fe16f40 100644 --- a/apps/sim/hooks/queries/environment.ts +++ b/apps/sim/hooks/queries/environment.ts @@ -94,10 +94,14 @@ export function useUpsertWorkspaceEnvironment() { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ workspaceId, variables }: UpsertWorkspaceEnvironmentParams) => { + mutationFn: async ({ + workspaceId, + variables, + visibility, + }: UpsertWorkspaceEnvironmentParams) => { const data = await requestJson(upsertWorkspaceEnvironmentContract, { params: { id: workspaceId }, - body: { variables }, + body: { variables, ...(visibility ? { visibility } : {}) }, }) logger.info(`Upserted workspace environment variables for workspace: ${workspaceId}`) return data diff --git a/apps/sim/lib/api/contracts/environment.ts b/apps/sim/lib/api/contracts/environment.ts index 5af24b918f1..2aa2d76423c 100644 --- a/apps/sim/lib/api/contracts/environment.ts +++ b/apps/sim/lib/api/contracts/environment.ts @@ -10,10 +10,21 @@ export const environmentVariablesSchema = z.record(z.string(), z.string()) export const personalEnvironmentDataSchema = z.record(z.string(), environmentVariableSchema) +/** + * Disclosure policy for a workspace environment key. `secret` is masked from + * non-admins and redacted from traces; `variable` is readable by every member. + * Write authorization is identical for both — this governs reads only. + */ +export const envVisibilitySchema = z.enum(['secret', 'variable']) + +/** Per-key disclosure policy. Keys absent from the map are secrets. */ +export const envVisibilityMapSchema = z.record(z.string(), envVisibilitySchema) + export const workspaceEnvironmentDataSchema = z.object({ workspace: environmentVariablesSchema.default({}), personal: environmentVariablesSchema.default({}), conflicts: z.array(z.string()).default([]), + visibility: envVisibilityMapSchema.default({}), }) export const workspaceEnvironmentParamsSchema = z.object({ @@ -24,6 +35,17 @@ export const savePersonalEnvironmentBodySchema = z.object({ variables: environmentVariablesSchema, }) +/** + * Workspace upsert body. `variables` is the key -> value map (both kinds of + * key); `visibility` is the separate, optional per-key disclosure policy. + * The two are deliberately distinct fields — `variables` predates this feature + * and does not mean "non-secret values". + */ +export const upsertWorkspaceEnvironmentBodySchema = z.object({ + variables: environmentVariablesSchema, + visibility: envVisibilityMapSchema.optional(), +}) + export const removeWorkspaceEnvironmentBodySchema = z.object({ keys: z.array(z.string()).min(1), }) @@ -32,6 +54,13 @@ const successResponseSchema = z.object({ success: z.literal(true), }) +export type EnvironmentVariable = z.output +export type EnvVisibility = z.output +export type EnvVisibilityMap = z.output +export type WorkspaceEnvironmentData = z.output +export type UpsertWorkspaceEnvironmentBody = z.input +export type SavePersonalEnvironmentBody = z.input + export const getPersonalEnvironmentContract = defineRouteContract({ method: 'GET', path: '/api/environment', @@ -69,7 +98,7 @@ export const upsertWorkspaceEnvironmentContract = defineRouteContract({ method: 'PUT', path: '/api/workspaces/[id]/environment', params: workspaceEnvironmentParamsSchema, - body: savePersonalEnvironmentBodySchema, + body: upsertWorkspaceEnvironmentBodySchema, response: { mode: 'json', schema: successResponseSchema, diff --git a/apps/sim/lib/copilot/chat/workspace-context.ts b/apps/sim/lib/copilot/chat/workspace-context.ts index d2144ab844a..419feec9683 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.ts @@ -78,6 +78,14 @@ export interface WorkspaceMdData { role?: string | null }> envVariables: string[] + /** + * Subset of {@link envVariables} explicitly marked non-secret. Rendered as a + * marker rather than a separate section: env vars are secret or non-secret, + * not a third kind of thing, and a `## Workspace Variables` heading would + * collide with workflow variables (``) in the agent's model. + * Values live in `environment/variables.json` under `nonSecretValues`. + */ + nonSecretEnvVariables: string[] customTools?: Array<{ id: string; name: string }> customBlocks?: Array<{ type: string; name: string; description?: string }> mcpServers?: Array<{ id: string; name: string; url?: string | null; enabled: boolean }> @@ -261,8 +269,16 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { } if (data.envVariables.length > 0) { - const lines = [...data.envVariables].sort(stableCompare).map((v) => `- ${v}`) - sections.push(`## Environment Variables (${data.envVariables.length})\n${lines.join('\n')}`) + const nonSecret = new Set(data.nonSecretEnvVariables) + const lines = [...data.envVariables] + .sort(stableCompare) + .map((v) => (nonSecret.has(v) ? `- ${v} (non-secret)` : `- ${v}`)) + const note = nonSecret.size + ? '\nValues for the names marked non-secret are in environment/variables.json under nonSecretValues; every other name is a secret whose value you cannot read.' + : '' + sections.push( + `## Environment Variables (${data.envVariables.length})${note}\n${lines.join('\n')}` + ) } if (data.customTools && data.customTools.length > 0) { @@ -513,6 +529,15 @@ async function buildWorkspaceMdData( envVariables: [...new Set(envCredentials.map((credential) => credential.envKey))].sort( stableCompare ), + // Derived from the same credential rows, so this costs no extra query and + // no decryption — only the names are needed here. + nonSecretEnvVariables: [ + ...new Set( + envCredentials + .filter((credential) => credential.envVisibility === 'variable') + .map((credential) => credential.envKey) + ), + ].sort(stableCompare), customTools: customTools.map((t) => ({ id: t.id, name: t.title })), customBlocks: customBlockSummaries, mcpServers: mcpServerRows, @@ -557,6 +582,13 @@ export async function generateWorkspaceContext( return buildWorkspaceMd({ ...data, envVariables: filterSecretNamesByMountPolicy(data.envVariables, options?.secretMountPolicy), + // Filtered by the same policy so a restricted mount list cannot be widened + // just because a key is non-secret, and so a name can never be marked + // non-secret in the inventory while being absent from the list above. + nonSecretEnvVariables: filterSecretNamesByMountPolicy( + data.nonSecretEnvVariables, + options?.secretMountPolicy + ), }) } diff --git a/apps/sim/lib/copilot/environment-context.ts b/apps/sim/lib/copilot/environment-context.ts index dfbe22e77a3..80b8b2f4f4e 100644 --- a/apps/sim/lib/copilot/environment-context.ts +++ b/apps/sim/lib/copilot/environment-context.ts @@ -22,6 +22,7 @@ export async function createCopilotEnvironmentContext( personalDecrypted: environment.personalDecrypted, workspaceDecrypted: environment.workspaceDecrypted, decryptionFailures: environment.decryptionFailures, + nonSecretNames: new Set(environment.workspaceVariableKeys), scope: { userId, workspaceId }, }) diff --git a/apps/sim/lib/copilot/request/handlers/handlers.test.ts b/apps/sim/lib/copilot/request/handlers/handlers.test.ts index b98cbb79438..19798fdf6d7 100644 --- a/apps/sim/lib/copilot/request/handlers/handlers.test.ts +++ b/apps/sim/lib/copilot/request/handlers/handlers.test.ts @@ -87,7 +87,10 @@ import { subAgentHandlers, } from '@/lib/copilot/request/handlers' import type { ExecutionContext, StreamEvent, StreamingContext } from '@/lib/copilot/request/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' describe('sse-handlers tool lifecycle', () => { let context: StreamingContext @@ -132,7 +135,11 @@ describe('sse-handlers tool lifecycle', () => { execContext = { userId: 'user-1', workflowId: 'workflow-1', - resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry([]), + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ), } }) @@ -474,13 +481,17 @@ describe('sse-handlers tool lifecycle', () => { }) it('projects resolved Function secrets before every Copilot-visible result sink', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'SECRET', - plaintext: 'secret-value', - encryptedValue: 'encrypted-secret-value', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'SECRET', + plaintext: 'secret-value', + encryptedValue: 'encrypted-secret-value', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('SECRET', 'secret-value') execContext.resolvedSecretTraceRegistry = registry execContext.chatId = 'chat-1' diff --git a/apps/sim/lib/copilot/request/lifecycle/run.test.ts b/apps/sim/lib/copilot/request/lifecycle/run.test.ts index 15c328f8904..957d47c9c16 100644 --- a/apps/sim/lib/copilot/request/lifecycle/run.test.ts +++ b/apps/sim/lib/copilot/request/lifecycle/run.test.ts @@ -5,7 +5,10 @@ import { resetEnvFlagsMock, resetEnvironmentUtilsMock, setEnvFlags } from '@sim/testing' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { ExecutionContext, StreamingContext } from '@/lib/copilot/request/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' afterAll(resetEnvironmentUtilsMock) @@ -163,12 +166,16 @@ describe('runCopilotLifecycle', () => { mockGetMothershipBaseURL.mockResolvedValue('http://mothership.test') mockGetMothershipSourceEnvHeaders.mockReturnValue({}) mockPrepareCopilotEnvironmentContext.mockResolvedValue({ - resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ), }) }) it('threads trace provenance through server execution context only', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const executionContext: ExecutionContext = { userId: 'user-1', workflowId: '', @@ -245,13 +252,17 @@ describe('runCopilotLifecycle', () => { ) it('does not infer model provenance from a dormant environment catalog', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'RUNTIME_TOKEN', - plaintext: 'runtime-secret', - encryptedValue: 'runtime-ciphertext', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'RUNTIME_TOKEN', + plaintext: 'runtime-secret', + encryptedValue: 'runtime-ciphertext', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockPrepareCopilotEnvironmentContext.mockResolvedValueOnce({ resolvedSecretTraceRegistry: registry, }) @@ -281,9 +292,11 @@ describe('runCopilotLifecycle', () => { it('projects secrets in every model-visible initial Go payload field without rewriting foreign aliases', async () => { const secret = 'mothership-secret' - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) let capturedRequestBody = '' mockRunStreamLoop.mockImplementationOnce(async (_url: string, request: RequestInit) => { @@ -341,9 +354,11 @@ describe('runCopilotLifecycle', () => { }) it('projects large tool catalogs at the tool-definition boundary', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'catalog-secret', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'catalog-secret', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'catalog-secret') const toolCount = 4_000 const propertiesPerTool = 8 @@ -399,9 +414,11 @@ describe('runCopilotLifecycle', () => { }) it('projects selected JSON and attachment fields exactly once when plaintext overlaps its alias', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'TOKEN', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'TOKEN', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'TOKEN') let capturedRequestBody = '' mockRunStreamLoop.mockImplementationOnce(async (_url: string, request: RequestInit) => { @@ -523,9 +540,11 @@ describe('runCopilotLifecycle', () => { it.each(['123', 'true'])( 'keeps low-entropy Copilot JSON valid while separating content from controls (%s)', async (secret) => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) const converted = secret === '123' ? 123 : true let capturedRequestBody = '' @@ -881,9 +900,11 @@ describe('runCopilotLifecycle', () => { 'guards arbitrary %s schema controls before initial Copilot model egress', async (controlKey) => { const secret = `copilot-schema-control-secret-${controlKey}` - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) const unsafeSchema = { type: 'object', @@ -950,7 +971,7 @@ describe('runCopilotLifecycle', () => { ) it('omits malformed and oversized optional Copilot response schemas', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) for (const [index, schema] of [ { properties: { field: 'not-a-schema' } }, { allOf: new Array(100_001) }, @@ -987,9 +1008,11 @@ describe('runCopilotLifecycle', () => { ])( 'preserves validated canonical schema controls when an active secret has value %s', async (secret, schema) => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) let capturedRequestBody = '' mockRunStreamLoop.mockImplementationOnce(async (_url: string, request: RequestInit) => { @@ -1051,9 +1074,11 @@ describe('runCopilotLifecycle', () => { ) it('forwards safe canonical schema controls byte-for-byte to Copilot', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'unrelated-secret', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'unrelated-secret', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) const schema = { type: ['object', 'null'], nullable: true, @@ -1083,7 +1108,7 @@ describe('runCopilotLifecycle', () => { }) it('fails before the initial Go request when model projection is incomplete', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() const result = await runCopilotLifecycle( @@ -1527,9 +1552,11 @@ describe('runCopilotLifecycle', () => { }) it('fails closed instead of sending a secret-bearing tool name on resume', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'unsafe-tool', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'unsafe-tool', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'unsafe-tool') mockRunStreamLoop.mockImplementationOnce( async ( diff --git a/apps/sim/lib/copilot/request/lifecycle/start.test.ts b/apps/sim/lib/copilot/request/lifecycle/start.test.ts index c3dcfcb02d8..eaf6d8e3356 100644 --- a/apps/sim/lib/copilot/request/lifecycle/start.test.ts +++ b/apps/sim/lib/copilot/request/lifecycle/start.test.ts @@ -11,7 +11,10 @@ import { MothershipStreamV1CompletionStatus, MothershipStreamV1EventType, } from '@/lib/copilot/generated/mothership-stream-v1' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const { runCopilotLifecycle, @@ -332,9 +335,11 @@ describe('createSSEStream terminal error handling', () => { contentBlocks: [], toolCalls: [], }) - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'secret-value') const stream = createSSEStream({ diff --git a/apps/sim/lib/copilot/request/tools/client.test.ts b/apps/sim/lib/copilot/request/tools/client.test.ts index 8e0456dae08..858e9d4222b 100644 --- a/apps/sim/lib/copilot/request/tools/client.test.ts +++ b/apps/sim/lib/copilot/request/tools/client.test.ts @@ -41,7 +41,10 @@ import { } from '@/lib/copilot/request/tools/client' import { sealClientToolContext } from '@/lib/copilot/request/tools/client-completion-seal.server' import { TOOL_RESULT_UNAVAILABLE_ERROR } from '@/lib/copilot/request/tools/resolved-secret-result' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const TRACE_SCOPE = { userId: 'user-1', workspaceId: 'workspace-1' } @@ -54,7 +57,8 @@ function createParentRegistry(): ResolvedSecretTraceRegistry { encryptedValue: 'encrypted-parent-secret', }, ], - TRACE_SCOPE + TRACE_SCOPE, + EMPTY_NON_SECRET_NAMES ) registry.recordResolved('PARENT_SECRET', 'parent-secret-value') return registry @@ -69,7 +73,8 @@ function createClientRegistry(): ResolvedSecretTraceRegistry { encryptedValue: 'encrypted-secret', }, ], - TRACE_SCOPE + TRACE_SCOPE, + EMPTY_NON_SECRET_NAMES ) registry.recordResolved('SECRET', 'resolved-secret') return registry @@ -338,7 +343,7 @@ describe('workflow client tool completion', () => { }) it('imports and projects a secret activated only inside the child workflow', async () => { - const registry = new ResolvedSecretTraceRegistry([], TRACE_SCOPE) + const registry = new ResolvedSecretTraceRegistry([], TRACE_SCOPE, EMPTY_NON_SECRET_NAMES) waitForToolConfirmation.mockResolvedValue({ status: 'success', data: { workflowId: 'workflow-1', executionId: 'execution-1' }, @@ -380,7 +385,7 @@ describe('workflow client tool completion', () => { }) it('discards imported child provenance when workflow-result projection fails', async () => { - const registry = new ResolvedSecretTraceRegistry([], TRACE_SCOPE) + const registry = new ResolvedSecretTraceRegistry([], TRACE_SCOPE, EMPTY_NON_SECRET_NAMES) const cyclicOutput: Record = { value: 'child-secret-value' } cyclicOutput.self = cyclicOutput waitForToolConfirmation.mockResolvedValue({ @@ -424,7 +429,7 @@ describe('workflow client tool completion', () => { }) it('corrects the client terminal status from the bound execution log', async () => { - const registry = new ResolvedSecretTraceRegistry([], TRACE_SCOPE) + const registry = new ResolvedSecretTraceRegistry([], TRACE_SCOPE, EMPTY_NON_SECRET_NAMES) waitForToolConfirmation.mockResolvedValue({ status: 'success', data: { workflowId: 'workflow-1', executionId: 'execution-1' }, @@ -664,7 +669,8 @@ describe('generic client tool completion', () => { encryptedValue: 'encrypted-low-entropy-secret', }, ], - TRACE_SCOPE + TRACE_SCOPE, + EMPTY_NON_SECRET_NAMES ) registry.recordResolved('LOW_ENTROPY_SECRET', 'true') const sealedContext = await sealClientToolContext({ @@ -860,7 +866,7 @@ describe('generic client tool completion', () => { it('fails structurally when a restarted execution uses a new registry instance', async () => { const sourceRegistry = createClientRegistry() - const resumedRegistry = new ResolvedSecretTraceRegistry([], TRACE_SCOPE) + const resumedRegistry = new ResolvedSecretTraceRegistry([], TRACE_SCOPE, EMPTY_NON_SECRET_NAMES) const sealedContext = await sealClientToolContext({ toolCallId: 'tool-1', runId: 'run-1', @@ -905,7 +911,7 @@ describe('generic client tool completion', () => { }) it('fails structurally for a legacy raw confirmation without sealed provenance', async () => { - const registry = new ResolvedSecretTraceRegistry([], TRACE_SCOPE) + const registry = new ResolvedSecretTraceRegistry([], TRACE_SCOPE, EMPTY_NON_SECRET_NAMES) waitForToolConfirmation.mockResolvedValue({ status: 'error', message: 'raw error secret', diff --git a/apps/sim/lib/copilot/request/tools/executor.test.ts b/apps/sim/lib/copilot/request/tools/executor.test.ts index 5078e2377fe..801288ae565 100644 --- a/apps/sim/lib/copilot/request/tools/executor.test.ts +++ b/apps/sim/lib/copilot/request/tools/executor.test.ts @@ -82,7 +82,10 @@ import { toolWatchdogTimeoutMs, } from '@/lib/copilot/request/tools/executor' import type { ExecutionContext, ToolCallState } from '@/lib/copilot/request/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' function buildStreamingContext(toolCall: ToolCallState) { return createStreamingContext({ @@ -160,9 +163,11 @@ describe('buildToolExecutionContext', () => { }) it('isolates one tool from a sibling secret activation and merges settled provenance', () => { - const parentRegistry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret', encryptedValue: 'encrypted-secret' }, - ]) + const parentRegistry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret', encryptedValue: 'encrypted-secret' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) const completeSiblingActivation = parentRegistry.beginPendingActivation() const executionContext: ExecutionContext = { userId: 'user-1', @@ -193,9 +198,11 @@ describe('executeToolAndReport provenance isolation', () => { }) it('merges a complete child only after its projected result is safe', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) executeTool.mockImplementationOnce( async ( _toolName: string, @@ -226,9 +233,11 @@ describe('executeToolAndReport provenance isolation', () => { }) it('structurally omits an incomplete result without poisoning the parent turn', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) executeTool.mockImplementationOnce( async ( _toolName: string, @@ -259,9 +268,11 @@ describe('executeToolAndReport provenance isolation', () => { }) it('structurally fails an incomplete thrown error without poisoning the parent turn', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) executeTool.mockImplementationOnce( async ( _toolName: string, @@ -288,9 +299,11 @@ describe('executeToolAndReport provenance isolation', () => { }) it('discards an incomplete child when execution is aborted before result delivery', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) const abortController = new AbortController() executeTool.mockImplementationOnce( async ( diff --git a/apps/sim/lib/copilot/request/tools/files.test.ts b/apps/sim/lib/copilot/request/tools/files.test.ts index f09a7c396d6..44c961a3552 100644 --- a/apps/sim/lib/copilot/request/tools/files.test.ts +++ b/apps/sim/lib/copilot/request/tools/files.test.ts @@ -29,7 +29,10 @@ import { } from '@/lib/copilot/request/tools/files' import { projectToolResultForCopilot } from '@/lib/copilot/request/tools/resolved-secret-result' import type { ExecutionContext } from '@/lib/copilot/request/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' describe('unwrapFunctionExecuteOutput', () => { it('unwraps the function_execute envelope { result, stdout }', () => { @@ -114,7 +117,11 @@ describe('maybeWriteOutputToFile', () => { workflowId: 'wf-1', workspaceId: 'workspace-1', userPermission: 'write', - resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ), ...overrides, } } @@ -167,18 +174,22 @@ describe('maybeWriteOutputToFile', () => { }) it('persists canonical aliases and leaves unrelated low-entropy public values unchanged', async () => { - const parentRegistry = new ResolvedSecretTraceRegistry([ - { - name: 'OUTPUT_SECRET', - plaintext: 'secret-value', - encryptedValue: 'encrypted-output-secret', - }, - { - name: 'UNRELATED', - plaintext: 'true', - encryptedValue: 'encrypted-unrelated', - }, - ]) + const parentRegistry = new ResolvedSecretTraceRegistry( + [ + { + name: 'OUTPUT_SECRET', + plaintext: 'secret-value', + encryptedValue: 'encrypted-output-secret', + }, + { + name: 'UNRELATED', + plaintext: 'true', + encryptedValue: 'encrypted-unrelated', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) parentRegistry.recordResolved('UNRELATED', 'true') const toolRegistry = parentRegistry.forkForToolInput({ code: 'return {{OUTPUT_SECRET}}' }) toolRegistry.recordResolved('OUTPUT_SECRET', 'secret-value') @@ -205,7 +216,7 @@ describe('maybeWriteOutputToFile', () => { const laterRead = projectToolResultForCopilot( { success: true, output: { content: persisted } }, - new ResolvedSecretTraceRegistry() + new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) ) expect(JSON.parse((laterRead.output as { content: string }).content)).toEqual({ token: '{{OUTPUT_SECRET}}', diff --git a/apps/sim/lib/copilot/request/tools/resolved-secret-result.test.ts b/apps/sim/lib/copilot/request/tools/resolved-secret-result.test.ts index bd58ff50ce3..73c575b7981 100644 --- a/apps/sim/lib/copilot/request/tools/resolved-secret-result.test.ts +++ b/apps/sim/lib/copilot/request/tools/resolved-secret-result.test.ts @@ -7,16 +7,23 @@ import { projectToolResultForCopilot, TOOL_RESULT_UNAVAILABLE_ERROR, } from '@/lib/copilot/request/tools/resolved-secret-result' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' function createRegistry(): ResolvedSecretTraceRegistry { - return new ResolvedSecretTraceRegistry([ - { - name: 'SECRET', - plaintext: 'secret-value', - encryptedValue: 'encrypted-secret-value', - }, - ]) + return new ResolvedSecretTraceRegistry( + [ + { + name: 'SECRET', + plaintext: 'secret-value', + encryptedValue: 'encrypted-secret-value', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) } describe('projectToolResultForCopilot', () => { @@ -48,9 +55,11 @@ describe('projectToolResultForCopilot', () => { ) it('projects an exact-name/exact-value Function result to its named placeholder', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('Test', 'Test') const runtimeResult = { success: true, @@ -128,11 +137,15 @@ describe('projectToolResultForCopilot', () => { }) it('uses an opaque marker when a replacement contains another active literal', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'MIDDLE', plaintext: 'B', encryptedValue: 'encrypted-b' }, - { name: 'BRACE', plaintext: '{', encryptedValue: 'encrypted-brace' }, - { name: 'JOINED', plaintext: 'ac', encryptedValue: 'encrypted-ac' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'MIDDLE', plaintext: 'B', encryptedValue: 'encrypted-b' }, + { name: 'BRACE', plaintext: '{', encryptedValue: 'encrypted-brace' }, + { name: 'JOINED', plaintext: 'ac', encryptedValue: 'encrypted-ac' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('MIDDLE', 'B') registry.recordResolved('BRACE', '{') registry.recordResolved('JOINED', 'ac') @@ -144,9 +157,11 @@ describe('projectToolResultForCopilot', () => { }) it('keeps content and the control error safe from active one-character values', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'F_SECRET', plaintext: 'F', encryptedValue: 'encrypted-f' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'F_SECRET', plaintext: 'F', encryptedValue: 'encrypted-f' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('F_SECRET', 'F') const projected = projectToolResultForCopilot( @@ -176,11 +191,15 @@ describe('projectToolResultForCopilot', () => { }) it('projects exact typed primitive secrets and the same values as strings', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'NUMBER', plaintext: '123', encryptedValue: 'number-ciphertext' }, - { name: 'BOOLEAN', plaintext: 'true', encryptedValue: 'boolean-ciphertext' }, - { name: 'NULL', plaintext: 'null', encryptedValue: 'null-ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'NUMBER', plaintext: '123', encryptedValue: 'number-ciphertext' }, + { name: 'BOOLEAN', plaintext: 'true', encryptedValue: 'boolean-ciphertext' }, + { name: 'NULL', plaintext: 'null', encryptedValue: 'null-ciphertext' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('NUMBER', '123') registry.recordResolved('BOOLEAN', 'true') registry.recordResolved('NULL', 'null') @@ -227,7 +246,7 @@ describe('projectToolResultForCopilot', () => { }) it('serializes table-style dates for Copilot without mutating the runtime result', () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const createdAt = new Date('2026-08-05T12:34:56.789Z') const runtimeResult = { success: true, @@ -249,7 +268,7 @@ describe('projectToolResultForCopilot', () => { }) it('preserves foreign internal-looking tool output when the registry has no matching alias', () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) expect( projectToolResultForCopilot( diff --git a/apps/sim/lib/copilot/request/tools/tables.test.ts b/apps/sim/lib/copilot/request/tools/tables.test.ts index 55ad05c06e7..79d0d5585b2 100644 --- a/apps/sim/lib/copilot/request/tools/tables.test.ts +++ b/apps/sim/lib/copilot/request/tools/tables.test.ts @@ -35,7 +35,10 @@ import { maybeWriteReadCsvToTable, } from '@/lib/copilot/request/tools/tables' import type { ExecutionContext } from '@/lib/copilot/request/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const tableLogger = vi.mocked(loggerMock.createLogger).mock.results[ vi @@ -76,7 +79,11 @@ function buildContext(overrides: Partial = {}): ExecutionConte workflowId: 'wf-1', workspaceId: 'workspace-1', userPermission: 'write', - resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ), ...overrides, } } @@ -148,18 +155,22 @@ describe('maybeWriteOutputToTable', () => { }) it('projects activated secrets before persistence without rewriting sibling literals', async () => { - const parentRegistry = new ResolvedSecretTraceRegistry([ - { - name: 'OUTPUT_SECRET', - plaintext: 'secret-value', - encryptedValue: 'encrypted-output-secret', - }, - { - name: 'UNRELATED', - plaintext: 'true', - encryptedValue: 'encrypted-unrelated', - }, - ]) + const parentRegistry = new ResolvedSecretTraceRegistry( + [ + { + name: 'OUTPUT_SECRET', + plaintext: 'secret-value', + encryptedValue: 'encrypted-output-secret', + }, + { + name: 'UNRELATED', + plaintext: 'true', + encryptedValue: 'encrypted-unrelated', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) parentRegistry.recordResolved('UNRELATED', 'true') const toolRegistry = parentRegistry.forkForToolInput({ code: 'return {{OUTPUT_SECRET}}' }) toolRegistry.recordResolved('OUTPUT_SECRET', 'secret-value') @@ -191,13 +202,13 @@ describe('maybeWriteOutputToTable', () => { const laterRead = projectToolResultForCopilot( { success: true, output: { data: { rows: persistedRows } } }, - new ResolvedSecretTraceRegistry() + new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) ) expect(laterRead.output).toEqual({ data: { rows: persistedRows } }) }) it('does not write when table persistence provenance is incomplete', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() const result = await maybeWriteOutputToTable( @@ -271,9 +282,11 @@ describe('maybeWriteOutputToTable', () => { }) it('keeps raw errors for terminal projection but projects application logs and OTel events', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'SECRET', plaintext: 'secret-value', encryptedValue: 'encrypted-secret-value' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'SECRET', plaintext: 'secret-value', encryptedValue: 'encrypted-secret-value' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('SECRET', 'secret-value') mockReplaceTableRows.mockRejectedValue(new Error('Duplicate value "secret-value"')) @@ -344,10 +357,14 @@ describe('maybeWriteReadCsvToTable', () => { }) it('projects active secret literals into string-compatible CSV columns', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'NUMBER', plaintext: '123', encryptedValue: 'encrypted-number' }, - { name: 'BOOLEAN', plaintext: 'true', encryptedValue: 'encrypted-boolean' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'NUMBER', plaintext: '123', encryptedValue: 'encrypted-number' }, + { name: 'BOOLEAN', plaintext: 'true', encryptedValue: 'encrypted-boolean' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('NUMBER', '123') registry.recordResolved('BOOLEAN', 'true') @@ -374,10 +391,14 @@ describe('maybeWriteReadCsvToTable', () => { }) it('rejects active secret literals in number and boolean columns before mutation', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'NUMBER', plaintext: '123', encryptedValue: 'encrypted-number' }, - { name: 'BOOLEAN', plaintext: 'true', encryptedValue: 'encrypted-boolean' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'NUMBER', plaintext: '123', encryptedValue: 'encrypted-number' }, + { name: 'BOOLEAN', plaintext: 'true', encryptedValue: 'encrypted-boolean' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('NUMBER', '123') registry.recordResolved('BOOLEAN', 'true') @@ -399,7 +420,7 @@ describe('maybeWriteReadCsvToTable', () => { }) it('does not import CSV rows when persistence provenance is incomplete', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() const result = await maybeWriteReadCsvToTable( @@ -462,9 +483,11 @@ describe('maybeWriteReadCsvToTable', () => { }) it('projects active secret literals in CSV-import log and OTel errors', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'SECRET', plaintext: 'secret-value', encryptedValue: 'encrypted-secret-value' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'SECRET', plaintext: 'secret-value', encryptedValue: 'encrypted-secret-value' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('SECRET', 'secret-value') mockReplaceTableRows.mockRejectedValue(new Error('Duplicate value "secret-value"')) diff --git a/apps/sim/lib/copilot/tool-executor/executor.test.ts b/apps/sim/lib/copilot/tool-executor/executor.test.ts index 30b5d8d4f17..83534d7c70b 100644 --- a/apps/sim/lib/copilot/tool-executor/executor.test.ts +++ b/apps/sim/lib/copilot/tool-executor/executor.test.ts @@ -5,7 +5,10 @@ import { loggerMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const { getToolEntry, isKnownTool, isSimExecuted, isClientExecuted } = vi.hoisted(() => ({ getToolEntry: vi.fn(), @@ -91,9 +94,11 @@ describe('copilot tool executor fallback', () => { it('projects resolved secrets before logging registered handler failures', async () => { const secret = 'mounted-secret-value' - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-secret' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-secret' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('API_KEY', secret) isKnownTool.mockReturnValue(true) isSimExecuted.mockReturnValue(true) diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts index 044142f0c96..24071eb625d 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts @@ -106,7 +106,10 @@ vi.mock('@/lib/execution/remote-sandbox/workspace-sandboxes', () => ({ import { projectToolResultForCopilot } from '@/lib/copilot/request/tools/resolved-secret-result' import { executeFunctionExecute } from '@/lib/copilot/tools/handlers/function-execute' import { executeRunCode } from '@/lib/copilot/tools/handlers/run-code' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const table = { id: 'tbl_1', @@ -172,7 +175,8 @@ describe('executeFunctionExecute trace-secret provenance', () => { encryptedValue: 'encrypted-secret-value', }, ], - { userId: 'u1', workspaceId: 'ws_1' } + { userId: 'u1', workspaceId: 'ws_1' }, + EMPTY_NON_SECRET_NAMES ) const runtimeResult = await executeFunctionExecute( { @@ -348,10 +352,14 @@ describe('executeFunctionExecute trace-secret provenance', () => { }) const runtimeResult = { success: true, output: { result: 'secret-value' } } mockExecuteTool.mockResolvedValue(runtimeResult) - const resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry([], { - userId: 'u1', - workspaceId: 'ws_1', - }) + const resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'u1', + workspaceId: 'ws_1', + }, + EMPTY_NON_SECRET_NAMES + ) vi.spyOn(resolvedSecretTraceRegistry, 'importProvenance').mockRejectedValueOnce( new Error('provenance import failed') ) @@ -385,7 +393,8 @@ describe('executeFunctionExecute trace-secret provenance', () => { encryptedValue: 'encrypted-secret-value', }, ], - { userId: 'u1', workspaceId: 'ws_1' } + { userId: 'u1', workspaceId: 'ws_1' }, + EMPTY_NON_SECRET_NAMES ) mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => { expect(resolvedSecretTraceRegistry.isComplete()).toBe(false) @@ -460,7 +469,8 @@ describe('executeFunctionExecute trace-secret provenance', () => { encryptedValue: 'encrypted-secret-value', }, ], - { userId: 'u1', workspaceId: 'ws_1' } + { userId: 'u1', workspaceId: 'ws_1' }, + EMPTY_NON_SECRET_NAMES ) await expect( @@ -492,7 +502,8 @@ describe('executeFunctionExecute trace-secret provenance', () => { encryptedValue: 'encrypted-secret-value', }, ], - { userId: 'u1', workspaceId: 'ws_1' } + { userId: 'u1', workspaceId: 'ws_1' }, + EMPTY_NON_SECRET_NAMES ) await expect( @@ -787,10 +798,14 @@ describe('executeFunctionExecute file mounts', () => { { trusted: true } ) ?? false ) - const parentRegistry = new ResolvedSecretTraceRegistry([], { - userId: 'u1', - workspaceId: 'ws_1', - }) + const parentRegistry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'u1', + workspaceId: 'ws_1', + }, + EMPTY_NON_SECRET_NAMES + ) mockExecuteTool.mockResolvedValue({ success: true, output: { result: 'secret-value' }, @@ -836,10 +851,14 @@ describe('executeFunctionExecute file mounts', () => { { trusted: true } ) ?? false ) - const parentRegistry = new ResolvedSecretTraceRegistry([], { - userId: 'u1', - workspaceId: 'ws_1', - }) + const parentRegistry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'u1', + workspaceId: 'ws_1', + }, + EMPTY_NON_SECRET_NAMES + ) mockExecuteTool.mockResolvedValue({ success: true, output: { result: 'ordinary' } }) const result = await executeFunctionExecute( diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.ts index 94e774e4996..2863da01620 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts @@ -584,7 +584,11 @@ export async function executeFunctionExecute( try { const secretActorUserId = context.secretActorUserId === undefined ? context.userId : context.secretActorUserId - let mounted: MaterializedCopilotCodeSecrets = { envVars: {}, catalogEntries: [] } + let mounted: MaterializedCopilotCodeSecrets = { + envVars: {}, + catalogEntries: [], + nonSecretNames: [], + } if (requestedNames.length > 0) { if (!secretActorUserId) { throw new CopilotCodeSecretAccessError('Secret access is unavailable for this Copilot run') @@ -600,10 +604,14 @@ export async function executeFunctionExecute( requestedNames, }) } - mountedRegistry = new ResolvedSecretTraceRegistry(mounted.catalogEntries, { - userId: secretActorUserId ?? context.userId, - ...(context.workspaceId ? { workspaceId: context.workspaceId } : {}), - }) + mountedRegistry = new ResolvedSecretTraceRegistry( + mounted.catalogEntries, + { + userId: secretActorUserId ?? context.userId, + ...(context.workspaceId ? { workspaceId: context.workspaceId } : {}), + }, + new Set(mounted.nonSecretNames) + ) enrichedParams.envVars = mounted.envVars enrichedParams.secretScope = 'selected' diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.test.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.test.ts index fa9dc2465e7..7ba271694fe 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.test.ts @@ -25,6 +25,7 @@ import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attr import type { ExecutionContext } from '@/lib/copilot/request/types' import { ANONYMOUS_SECRET_TRACE_REPLACEMENT, + EMPTY_NON_SECRET_NAMES, ResolvedSecretTraceRegistry, } from '@/executor/utils/resolved-secret-trace-registry' @@ -630,7 +631,8 @@ describe('Copilot workflow execution billing attribution', () => { encryptedValue: 'unrelated-ciphertext', }, ], - { userId: 'user-1', workspaceId: 'workspace-1' } + { userId: 'user-1', workspaceId: 'workspace-1' }, + EMPTY_NON_SECRET_NAMES ) registry.recordResolved('INPUT_SECRET', 'input-secret') registry.recordResolved('UNRELATED_SECRET', 'unrelated-secret') @@ -678,10 +680,14 @@ describe('Copilot workflow execution billing attribution', () => { }) it('imports child provenance without returning private metadata to the model', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) const context: ExecutionContext = { ...executionContext, resolvedSecretTraceRegistry: registry, @@ -718,10 +724,14 @@ describe('Copilot workflow execution billing attribution', () => { }) it('keeps unrelated tool-result projection available while child provenance is pending', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) const context: ExecutionContext = { ...executionContext, resolvedSecretTraceRegistry: registry, @@ -773,7 +783,7 @@ describe('Copilot workflow execution billing attribution', () => { }) it('marks provenance incomplete when child execution returns no trusted state', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const context: ExecutionContext = { ...executionContext, resolvedSecretTraceRegistry: registry, @@ -789,10 +799,14 @@ describe('Copilot workflow execution billing attribution', () => { }) it('filters and anonymizes cross-workspace child provenance to values that cross back', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) const context: ExecutionContext = { ...executionContext, resolvedSecretTraceRegistry: registry, diff --git a/apps/sim/lib/copilot/tools/secret-mount-materializer.server.test.ts b/apps/sim/lib/copilot/tools/secret-mount-materializer.server.test.ts index 14313766c5d..902dc347d55 100644 --- a/apps/sim/lib/copilot/tools/secret-mount-materializer.server.test.ts +++ b/apps/sim/lib/copilot/tools/secret-mount-materializer.server.test.ts @@ -109,6 +109,9 @@ describe('materializeCopilotCodeSecrets', () => { catalogEntries: [ { name: 'API_KEY', plaintext: 'plain:personal-cipher', encryptedValue: 'personal-cipher' }, ], + // A personal secret is never non-secret: the schema check constraint + // restricts `variable` to `env_workspace` rows. + nonSecretNames: [], }) }) diff --git a/apps/sim/lib/copilot/tools/secret-mount-materializer.server.ts b/apps/sim/lib/copilot/tools/secret-mount-materializer.server.ts index 47d81467c8f..3aeb7abe556 100644 --- a/apps/sim/lib/copilot/tools/secret-mount-materializer.server.ts +++ b/apps/sim/lib/copilot/tools/secret-mount-materializer.server.ts @@ -35,7 +35,14 @@ interface AuthorizedEncryptedSecret { export interface MaterializedCopilotCodeSecrets { envVars: Record + /** Catalog entries for genuine secrets only — non-secret names are excluded. */ catalogEntries: ResolvedSecretTraceCatalogEntry[] + /** + * Mounted names explicitly marked non-secret. Kept out of `catalogEntries` and + * handed to the registry as its exempt set, so their values survive verbatim + * in tool results instead of being replaced with `{{NAME}}`. + */ + nonSecretNames: string[] } export class CopilotCodeSecretAccessError extends Error { @@ -133,7 +140,7 @@ export async function materializeCopilotCodeSecrets(params: { requestedNames: readonly string[] }): Promise { const requestedNames = normalizeRequestedNames(params.requestedNames) - if (requestedNames.length === 0) return { envVars: {}, catalogEntries: [] } + if (requestedNames.length === 0) return { envVars: {}, catalogEntries: [], nonSecretNames: [] } const access = await checkWorkspaceAccess(params.workspaceId, params.actorUserId) if (!access.exists || !access.canWrite) { @@ -305,8 +312,27 @@ export async function materializeCopilotCodeSecrets(params: { } } + // Resolved from stored visibility rather than anything the caller supplied. + // Non-secret names are dropped from the catalog so they can never activate, + // which is what keeps their values readable in the tool result. + const nonSecretRows = await db + .select({ envKey: credential.envKey }) + .from(credential) + .where( + and( + eq(credential.workspaceId, params.workspaceId), + eq(credential.type, 'env_workspace'), + eq(credential.envVisibility, 'variable'), + inArray(credential.envKey, requestedNames) + ) + ) + const nonSecretNames = new Set( + nonSecretRows.map((row) => row.envKey).filter((key): key is string => key !== null) + ) + return { envVars: Object.fromEntries(decryptedEntries.map((entry) => [entry.name, entry.plaintext])), - catalogEntries: decryptedEntries, + catalogEntries: decryptedEntries.filter((entry) => !nonSecretNames.has(entry.name)), + nonSecretNames: [...nonSecretNames], } } diff --git a/apps/sim/lib/copilot/tools/server/docs/search-documentation.test.ts b/apps/sim/lib/copilot/tools/server/docs/search-documentation.test.ts index f61622fafff..95463005cbb 100644 --- a/apps/sim/lib/copilot/tools/server/docs/search-documentation.test.ts +++ b/apps/sim/lib/copilot/tools/server/docs/search-documentation.test.ts @@ -16,7 +16,10 @@ vi.mock('@/lib/knowledge/embeddings', () => ({ })) import { searchDocumentationServerTool } from '@/lib/copilot/tools/server/docs/search-documentation' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' describe('documentation search model boundary', () => { beforeEach(() => { @@ -25,13 +28,17 @@ describe('documentation search model boundary', () => { }) it('projects the query immediately before embedding without logging plaintext', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'DOCS_QUERY', - plaintext: 'private documentation query', - encryptedValue: 'encrypted-query', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'DOCS_QUERY', + plaintext: 'private documentation query', + encryptedValue: 'encrypted-query', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('DOCS_QUERY', 'private documentation query') const result = await searchDocumentationServerTool.execute( diff --git a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.test.ts b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.test.ts index f5f68d6303a..ebf0a47eade 100644 --- a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.test.ts +++ b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.test.ts @@ -107,7 +107,10 @@ import { getKnowledgeBaseById } from '@/lib/knowledge/service' import { resolveWorkspaceFileReference } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { executeKnowledgeSearch } from '@/app/api/knowledge/search/utils' import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const knowledgeLoggerIndex = loggerMock.createLogger.mock.calls.findIndex( ([name]) => name === 'KnowledgeBaseServerTool' @@ -229,13 +232,17 @@ describe('knowledge base query model boundary', () => { }) it('projects the query at embedding, search, and usage boundaries', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'KB_QUERY', - plaintext: 'private knowledge query', - encryptedValue: 'encrypted-query', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'KB_QUERY', + plaintext: 'private knowledge query', + encryptedValue: 'encrypted-query', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('KB_QUERY', 'private knowledge query') const result = await knowledgeBaseServerTool.execute( @@ -278,13 +285,17 @@ describe('knowledge base query model boundary', () => { }) it('imports exact persisted result provenance before the Copilot result is projected', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'STORED_TOKEN', - plaintext: 'stored-secret-value', - encryptedValue: 'encrypted-stored-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'STORED_TOKEN', + plaintext: 'stored-secret-value', + encryptedValue: 'encrypted-stored-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) const results = [ { id: 'embedding-1', @@ -329,7 +340,7 @@ describe('knowledge base query model boundary', () => { }) it('fails closed when persisted result provenance cannot be established', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) vi.mocked(executeKnowledgeSearch).mockResolvedValue([ { id: 'embedding-1', diff --git a/apps/sim/lib/copilot/tools/server/media/model-boundaries.test.ts b/apps/sim/lib/copilot/tools/server/media/model-boundaries.test.ts index 5bd556bd2bb..655c3bd3c8c 100644 --- a/apps/sim/lib/copilot/tools/server/media/model-boundaries.test.ts +++ b/apps/sim/lib/copilot/tools/server/media/model-boundaries.test.ts @@ -46,7 +46,10 @@ import type { ServerToolContext } from '@/lib/copilot/tools/server/base-tool' import { generateImageServerTool } from '@/lib/copilot/tools/server/image/generate-image' import { generateAudioServerTool } from '@/lib/copilot/tools/server/media/generate-audio' import { generateVideoServerTool } from '@/lib/copilot/tools/server/media/generate-video' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const file = { id: 'file-1', @@ -66,7 +69,9 @@ function contextWithSecrets( entries: Array<{ name: string; plaintext: string }> ): ServerToolContext { const registry = new ResolvedSecretTraceRegistry( - entries.map((entry) => ({ ...entry, encryptedValue: `encrypted-${entry.name}` })) + entries.map((entry) => ({ ...entry, encryptedValue: `encrypted-${entry.name}` })), + undefined, + EMPTY_NON_SECRET_NAMES ) for (const entry of entries) registry.recordResolved(entry.name, entry.plaintext) return { diff --git a/apps/sim/lib/copilot/tools/server/model-input.test.ts b/apps/sim/lib/copilot/tools/server/model-input.test.ts index ac4daaaf882..10274145798 100644 --- a/apps/sim/lib/copilot/tools/server/model-input.test.ts +++ b/apps/sim/lib/copilot/tools/server/model-input.test.ts @@ -17,7 +17,10 @@ import { assertOpaqueWorkspaceFileModelSafe, projectServerToolModelInput, } from '@/lib/copilot/tools/server/model-input' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const file = { id: 'file-1', @@ -40,10 +43,14 @@ describe('server tool model-input boundary', () => { }) it('projects only active text secrets to their canonical aliases', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'PROMPT', plaintext: 'private prompt', encryptedValue: 'encrypted-prompt' }, - { name: 'LYRICS', plaintext: 'private lyrics', encryptedValue: 'encrypted-lyrics' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'PROMPT', plaintext: 'private prompt', encryptedValue: 'encrypted-prompt' }, + { name: 'LYRICS', plaintext: 'private lyrics', encryptedValue: 'encrypted-lyrics' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('PROMPT', 'private prompt') registry.recordResolved('LYRICS', 'private lyrics') @@ -60,7 +67,7 @@ describe('server tool model-input boundary', () => { 'could not be projected safely' ) - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() expect(() => projectServerToolModelInput( @@ -71,7 +78,7 @@ describe('server tool model-input boundary', () => { }) it('binds opaque checks to the exact workspace file before allowing model egress', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) await expect( assertOpaqueWorkspaceFileModelSafe({ diff --git a/apps/sim/lib/copilot/tools/server/other/search-online.test.ts b/apps/sim/lib/copilot/tools/server/other/search-online.test.ts index f8dbbc0ef98..5563e1533de 100644 --- a/apps/sim/lib/copilot/tools/server/other/search-online.test.ts +++ b/apps/sim/lib/copilot/tools/server/other/search-online.test.ts @@ -14,16 +14,23 @@ vi.mock('@/lib/copilot/generated/tool-catalog-v1', () => ({ vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) import { searchOnlineServerTool } from '@/lib/copilot/tools/server/other/search-online' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' function activeQueryRegistry(): ResolvedSecretTraceRegistry { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'SEARCH_QUERY', - plaintext: 'private search query', - encryptedValue: 'encrypted-query', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'SEARCH_QUERY', + plaintext: 'private search query', + encryptedValue: 'encrypted-query', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('SEARCH_QUERY', 'private search query') return registry } diff --git a/apps/sim/lib/copilot/vfs/serializers.test.ts b/apps/sim/lib/copilot/vfs/serializers.test.ts index 5e86b445895..4fd3a9d116f 100644 --- a/apps/sim/lib/copilot/vfs/serializers.test.ts +++ b/apps/sim/lib/copilot/vfs/serializers.test.ts @@ -15,6 +15,7 @@ import { serializeBlockSchema, serializeCredentials, serializeDeployments, + serializeEnvironmentVariables, serializeFileMeta, serializeIntegrationSchema, serializeKBMeta, @@ -528,3 +529,26 @@ describe('serializeCredentials — type distinguishes reconnect flow', () => { expect(json[0].type).toBeUndefined() }) }) + +describe('serializeEnvironmentVariables', () => { + it('emits secret names only, and values solely for non-secret keys', () => { + const json = JSON.parse( + serializeEnvironmentVariables(['MY_OPENAI_KEY'], ['STRIPE_KEY', 'SUPPORT_EMAIL'], { + SUPPORT_EMAIL: 'help@acme.com', + }) + ) + + expect(json.personal).toEqual(['MY_OPENAI_KEY']) + expect(json.workspace).toEqual(['STRIPE_KEY', 'SUPPORT_EMAIL']) + expect(json.nonSecretValues).toEqual({ SUPPORT_EMAIL: 'help@acme.com' }) + // The rule the agent prompt states: absence from nonSecretValues means secret. + expect(json.nonSecretValues.STRIPE_KEY).toBeUndefined() + expect(json.nonSecretValues.MY_OPENAI_KEY).toBeUndefined() + }) + + it('defaults to an empty non-secret map so every name reads as a secret', () => { + const json = JSON.parse(serializeEnvironmentVariables([], ['STRIPE_KEY'])) + + expect(json.nonSecretValues).toEqual({}) + }) +}) diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index 0fc76e89926..6d56dbffe98 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -818,16 +818,25 @@ export function serializeApiKeyIntegrations( /** * Serialize environment variables for VFS environment/variables.json. - * Shows variable NAMES only — NOT values. + * + * Secrets appear as NAMES ONLY. Keys explicitly marked non-secret additionally + * appear under `nonSecretValues` with their value, which is what lets the agent + * use and quote them instead of guessing. + * + * The key is named `nonSecretValues` rather than `values` so the file is + * self-describing when read cold, and so the safety rule can be stated in one + * line: a name absent from `nonSecretValues` is a secret. */ export function serializeEnvironmentVariables( personalVarNames: string[], - workspaceVarNames: string[] + workspaceVarNames: string[], + nonSecretValues: Record = {} ): string { return JSON.stringify( { personal: personalVarNames, workspace: workspaceVarNames, + nonSecretValues, }, null, 2 diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 4a7b340a07c..11c98d5f913 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -829,6 +829,7 @@ export class WorkspaceVFS { files: fileSummary, oauthIntegrations: envSummary.oauthIntegrations, envVariables: envSummary.envVariables, + nonSecretEnvVariables: envSummary.nonSecretEnvVariables, customTools: toolsSummary, customBlocks: customBlocksSummary, mcpServers: mcpServersSummary, @@ -2388,6 +2389,7 @@ export class WorkspaceVFS { ): Promise<{ oauthIntegrations: WorkspaceMdData['oauthIntegrations'] envVariables: WorkspaceMdData['envVariables'] + nonSecretEnvVariables: WorkspaceMdData['nonSecretEnvVariables'] }> { try { const isWorkspaceAdmin = await hasWorkspaceAdminAccess(userId, workspaceId) @@ -2455,9 +2457,21 @@ export class WorkspaceVFS { Object.keys(envData.workspaceEncrypted), secretMountPolicy ) + // Values are emitted only for keys explicitly marked non-secret, and only + // for names that already survived the mount policy above — a workspace + // that restricts discovery to selected names must not have that choice + // quietly widened just because a key is non-secret. Names still come from + // the encrypted maps, so no secret plaintext can reach the VFS. + const visibleWorkspaceVarNames = new Set(workspaceVarNames) + const nonSecretValues: Record = {} + for (const name of envData.workspaceVariableKeys) { + if (!visibleWorkspaceVarNames.has(name)) continue + const value = envData.workspaceDecrypted[name] + if (value !== undefined) nonSecretValues[name] = value + } this.files.set( 'environment/variables.json', - serializeEnvironmentVariables(personalVarNames, workspaceVarNames) + serializeEnvironmentVariables(personalVarNames, workspaceVarNames, nonSecretValues) ) const envKeys = [...visibleEnvCredentialNames] @@ -2469,13 +2483,17 @@ export class WorkspaceVFS { role: c.role, })), envVariables: envKeys, + // Already narrowed to mount-policy-visible names by the filter above. + nonSecretEnvVariables: visibleEnvCredentials + .filter((credential) => credential.envVisibility === 'variable') + .map((credential) => credential.envKey), } } catch (err) { logger.warn('Failed to materialize environment data', { workspaceId, error: toError(err).message, }) - return { oauthIntegrations: [], envVariables: [] } + return { oauthIntegrations: [], envVariables: [], nonSecretEnvVariables: [] } } } } diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index e70be39ebff..596a59b3173 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -1,5 +1,11 @@ import { db } from '@sim/db' -import { credential, credentialMember, permissions, workspace } from '@sim/db/schema' +import { + credential, + type credentialEnvVisibilityEnum, + credentialMember, + permissions, + workspace, +} from '@sim/db/schema' import { permissionSatisfies } from '@sim/platform-authz/workspace' import { chunkArray } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' @@ -100,11 +106,20 @@ export async function getCredentialCreationWorkspaceContext(params: { } } +/** Disclosure policy for an env credential. Mirrors `credentialEnvVisibilityEnum`. */ +export type EnvVisibility = (typeof credentialEnvVisibilityEnum.enumValues)[number] + export interface WorkspaceEnvKeyAdminAccess { /** Keys for which the caller is an active credential admin. */ adminKeys: Set /** Keys that already have an `env_workspace` credential (regardless of role). */ knownKeys: Set + /** + * Keys marked non-secret. Feeds the read/mask path only — write + * authorization is identical for secrets and variables, so callers gating + * writes must not consult this. + */ + variableKeys: Set } export interface PersonalEnvKeyRawAccess { @@ -181,11 +196,14 @@ export async function getWorkspaceEnvKeyAdminAccess(params: { }): Promise { const { workspaceId, envKeys, userId } = params const keys = Array.from(new Set(envKeys.filter(Boolean))) - if (keys.length === 0) return { adminKeys: new Set(), knownKeys: new Set() } + if (keys.length === 0) { + return { adminKeys: new Set(), knownKeys: new Set(), variableKeys: new Set() } + } const rows = await db .select({ envKey: credential.envKey, + envVisibility: credential.envVisibility, role: credentialMember.role, status: credentialMember.status, }) @@ -204,18 +222,21 @@ export async function getWorkspaceEnvKeyAdminAccess(params: { const knownKeys = new Set() const adminKeys = new Set() + const variableKeys = new Set() for (const row of rows) { if (!row.envKey) continue knownKeys.add(row.envKey) if (row.role === 'admin' && row.status === 'active') adminKeys.add(row.envKey) + if (row.envVisibility === 'variable') variableKeys.add(row.envKey) } - return { adminKeys, knownKeys } + return { adminKeys, knownKeys, variableKeys } } interface AccessibleEnvCredential { type: 'env_workspace' | 'env_personal' envKey: string envOwnerUserId: string | null + envVisibility: EnvVisibility updatedAt: Date } @@ -385,8 +406,10 @@ export async function createWorkspaceEnvCredentials(params: { workspaceId: string newKeys: string[] actingUserId: string + /** Per-key disclosure policy for the new keys. Anything unlisted is a secret. */ + visibilityByKey?: Record }): Promise { - const { workspaceId, newKeys, actingUserId } = params + const { workspaceId, newKeys, actingUserId, visibilityByKey } = params const keys = Array.from(new Set(newKeys.filter(Boolean))) if (keys.length === 0) return @@ -405,6 +428,7 @@ export async function createWorkspaceEnvCredentials(params: { type: 'env_workspace' as const, displayName: envKey, envKey, + envVisibility: visibilityByKey?.[envKey] ?? ('secret' as const), createdBy: actingUserId, createdAt: now, updatedAt: now, @@ -457,6 +481,79 @@ export async function deleteWorkspaceEnvCredentials(params: { ) } +/** Thrown when the caller may not change the disclosure policy of a key. */ +export class WorkspaceEnvVisibilityAccessError extends Error { + constructor(readonly keys: string[]) { + super('You must be an admin of these secrets to change their visibility') + this.name = 'WorkspaceEnvVisibilityAccessError' + } +} + +/** + * The single path that changes an env key's disclosure policy. + * + * Centralized because flipping `secret -> variable` is a disclosure event that + * cannot be undone: by the time the flag is reverted the value has plausibly + * reached trace spans, log rows, agent context, and members' browsers. Both + * directions therefore take the stricter gate (workspace admin, or credential + * admin on that specific key) rather than the workspace `write` that suffices + * for editing a value. + * + * A no-op request is authorized trivially — asking for the visibility a key + * already has reveals nothing and must not 403. + */ +export async function setWorkspaceEnvVisibility(params: { + workspaceId: string + updates: Record + actingUserId: string + executor?: DbOrTx +}): Promise<{ changedKeys: string[] }> { + const { workspaceId, updates, actingUserId, executor = db } = params + const requestedKeys = Object.keys(updates).filter(Boolean) + if (requestedKeys.length === 0) return { changedKeys: [] } + + const existingRows = await executor + .select({ + id: credential.id, + envKey: credential.envKey, + envVisibility: credential.envVisibility, + }) + .from(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + eq(credential.type, 'env_workspace'), + inArray(credential.envKey, requestedKeys) + ) + ) + + const changing = existingRows.filter( + (row) => row.envKey !== null && updates[row.envKey] !== row.envVisibility + ) + if (changing.length === 0) return { changedKeys: [] } + + const changingKeys = changing.map((row) => row.envKey as string) + const [isWorkspaceAdmin, { adminKeys }] = await Promise.all([ + hasWorkspaceAdminAccess(actingUserId, workspaceId), + getWorkspaceEnvKeyAdminAccess({ workspaceId, envKeys: changingKeys, userId: actingUserId }), + ]) + + const forbidden = isWorkspaceAdmin ? [] : changingKeys.filter((key) => !adminKeys.has(key)) + if (forbidden.length > 0) { + throw new WorkspaceEnvVisibilityAccessError(forbidden) + } + + const now = new Date() + for (const row of changing) { + await executor + .update(credential) + .set({ envVisibility: updates[row.envKey as string], updatedAt: now }) + .where(eq(credential.id, row.id)) + } + + return { changedKeys: changingKeys } +} + export async function syncPersonalEnvCredentialsForUser(params: { userId: string envKeys: string[] @@ -571,6 +668,7 @@ export async function getAccessibleEnvCredentials( type: credential.type, envKey: credential.envKey, envOwnerUserId: credential.envOwnerUserId, + envVisibility: credential.envVisibility, updatedAt: credential.updatedAt, }) .from(credential) @@ -589,6 +687,14 @@ export async function getAccessibleEnvCredentials( or( isNotNull(credentialMember.id), eq(credential.envOwnerUserId, userId), + // Non-secret workspace values are readable by every member, so they + // bypass the per-key credential ACL. Workspace membership itself is + // still enforced upstream by `checkWorkspaceAccess`; this clause only + // relaxes the per-key gate. The `env_workspace` predicate is load + // bearing — the schema check constraint keeps `env_personal` rows from + // ever being marked `variable`, and this keeps the query honest even + // if that constraint were ever relaxed. + and(eq(credential.type, 'env_workspace'), eq(credential.envVisibility, 'variable')), isWorkspaceAdmin ? eq(credential.type, 'env_workspace') : undefined ) ) @@ -603,6 +709,7 @@ export async function getAccessibleEnvCredentials( type: row.type, envKey: row.envKey, envOwnerUserId: row.envOwnerUserId, + envVisibility: row.envVisibility, updatedAt: row.updatedAt, })) } diff --git a/apps/sim/lib/environment/api.ts b/apps/sim/lib/environment/api.ts index af78dc71c26..d73e1f021ae 100644 --- a/apps/sim/lib/environment/api.ts +++ b/apps/sim/lib/environment/api.ts @@ -1,15 +1,12 @@ -import type { z } from 'zod' import { requestJson } from '@/lib/api/client/request' import { - type environmentVariableSchema, + type EnvironmentVariable, getPersonalEnvironmentContract, getWorkspaceEnvironmentContract, - type workspaceEnvironmentDataSchema, + type WorkspaceEnvironmentData, } from '@/lib/api/contracts' -export type EnvironmentVariable = z.output - -export type WorkspaceEnvironmentData = z.output +export type { EnvironmentVariable, WorkspaceEnvironmentData } export async function fetchPersonalEnvironment( signal?: AbortSignal @@ -32,9 +29,9 @@ export async function fetchWorkspaceEnvironment( signal, }) - return { - workspace: data.workspace || {}, - personal: data.personal || {}, - conflicts: data.conflicts || [], - } + // Returned whole rather than rebuilt field-by-field: `requestJson` has already + // validated against the contract and applied every `.default({})`, so + // reconstructing here only creates a place for new response fields to be + // silently dropped. + return data } diff --git a/apps/sim/lib/environment/utils.ts b/apps/sim/lib/environment/utils.ts index 65d9b89be0b..67adabe9a85 100644 --- a/apps/sim/lib/environment/utils.ts +++ b/apps/sim/lib/environment/utils.ts @@ -9,6 +9,7 @@ import { LRUCache } from 'lru-cache' import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' import { createWorkspaceEnvCredentials, + type EnvVisibility, getAccessibleEnvCredentials, getWorkspaceEnvKeyAdminAccess, syncPersonalEnvCredentialsForUser, @@ -51,6 +52,16 @@ export interface EnvironmentResolutionSnapshot { personalOwners: Record conflicts: string[] decryptionFailures: string[] + /** + * Workspace keys whose values are non-secret: readable by any member, kept + * verbatim in traces and logs, and exposed to the agent with their value. + * + * This is the single source every resolved-secret registry construction site + * derives its exemption set from, so it must only ever contain keys that are + * actually present in `workspaceEncrypted` — a stale credential row without a + * stored value must not exempt anything. + */ + workspaceVariableKeys: string[] } interface EffectiveEnvironmentCacheEntry { @@ -79,6 +90,7 @@ function cloneEnvironmentResolutionSnapshot( personalOwners: { ...snapshot.personalOwners }, conflicts: [...snapshot.conflicts], decryptionFailures: [...snapshot.decryptionFailures], + workspaceVariableKeys: [...snapshot.workspaceVariableKeys], } } @@ -262,6 +274,13 @@ export async function getPersonalAndWorkspaceEnv( const conflicts = Object.keys(personalEncrypted).filter((k) => k in workspaceEncrypted) + // Intersected with the values actually stored: a credential row that outlived + // its jsonb entry must never exempt a key from secret redaction. + const workspaceVariableKeys = accessibleEnvCredentials + .filter((row) => row.type === 'env_workspace' && row.envVisibility === 'variable') + .map((row) => row.envKey) + .filter((envKey) => envKey in workspaceEncrypted) + if (decryptionFailures.length > 0) { logger.warn('Some environment variables failed to decrypt', { userId, @@ -279,6 +298,7 @@ export async function getPersonalAndWorkspaceEnv( personalOwners, conflicts, decryptionFailures, + workspaceVariableKeys, } } @@ -361,7 +381,8 @@ export async function upsertPersonalEnvVars( export async function upsertWorkspaceEnvVars( workspaceId: string, newVars: Record, - actingUserId: string + actingUserId: string, + options?: { visibilityByKey?: Record } ): Promise { const updatedKeys = Object.keys(newVars) if (updatedKeys.length === 0) return [] @@ -445,7 +466,12 @@ export async function upsertWorkspaceEnvVars( // secret present in the jsonb map without a credential row is NOT new, and // minting an ACL for it would make the caller its secret-admin. const newKeys = updatedKeys.filter((key) => !(key in existingEncrypted)) - await createWorkspaceEnvCredentials({ workspaceId, newKeys, actingUserId }) + await createWorkspaceEnvCredentials({ + workspaceId, + newKeys, + actingUserId, + visibilityByKey: options?.visibilityByKey, + }) recordAudit({ workspaceId, diff --git a/apps/sim/lib/execution/durable-secret-provenance.ts b/apps/sim/lib/execution/durable-secret-provenance.ts index 334f4cfa07d..21b76f13c63 100644 --- a/apps/sim/lib/execution/durable-secret-provenance.ts +++ b/apps/sim/lib/execution/durable-secret-provenance.ts @@ -5,6 +5,7 @@ import { type PrivateSecretProvenanceBundleV1, } from '@/lib/execution/model-input-provenance' import { + EMPTY_NON_SECRET_NAMES, type ResolvedSecretTraceProvenanceV1, ResolvedSecretTraceRegistry, type ResolvedSecretTraceScopeV1, @@ -252,7 +253,7 @@ export async function createDurableSecretProvenanceRegistry( throw new Error('Durable secret provenance is unavailable') } if (provenance.entries.length === 0) return undefined - const registry = new ResolvedSecretTraceRegistry([], scope) + const registry = new ResolvedSecretTraceRegistry([], scope, EMPTY_NON_SECRET_NAMES) if (!(await importDurableSecretProvenance(registry, provenance))) { throw new Error('Durable secret provenance is unavailable') } diff --git a/apps/sim/lib/execution/model-input-provenance.test.ts b/apps/sim/lib/execution/model-input-provenance.test.ts index 61492a980b9..10ea6015219 100644 --- a/apps/sim/lib/execution/model-input-provenance.test.ts +++ b/apps/sim/lib/execution/model-input-provenance.test.ts @@ -12,7 +12,10 @@ import { RESOLVED_SECRET_PROVENANCE_FIELD, RESOLVED_SECRET_PROVENANCE_METADATA_V1, } from '@/lib/execution/private-tool-metadata' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const ENTRY = { name: 'TOKEN', @@ -22,7 +25,7 @@ const ENTRY = { describe('model input provenance transport', () => { it('exports only committed provenance present in the selected model input', () => { - const registry = new ResolvedSecretTraceRegistry([ENTRY]) + const registry = new ResolvedSecretTraceRegistry([ENTRY], undefined, EMPTY_NON_SECRET_NAMES) registry.recordResolved(ENTRY.name, ENTRY.plaintext) const metadata = createModelInputProvenanceRequestMetadata(registry, { @@ -43,9 +46,11 @@ describe('model input provenance transport', () => { it('preserves provenance through a JSON-encoded model-input field', () => { const secret = 'quote" slash\\ newline\n' - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'encrypted-token' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'encrypted-token' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) const metadata = createModelInputProvenanceRequestMetadata( diff --git a/apps/sim/lib/guardrails/validate_hallucination.test.ts b/apps/sim/lib/guardrails/validate_hallucination.test.ts index e522db31b2e..e734cb89c9b 100644 --- a/apps/sim/lib/guardrails/validate_hallucination.test.ts +++ b/apps/sim/lib/guardrails/validate_hallucination.test.ts @@ -39,7 +39,10 @@ vi.mock('@/providers/utils', () => ({ })) import { validateHallucination } from '@/lib/guardrails/validate_hallucination' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const BILLING_ATTRIBUTION: BillingAttributionSnapshot = { actorUserId: 'user-1', @@ -104,9 +107,11 @@ describe('validateHallucination', () => { }) it('uses authenticated private Knowledge transport and carries result provenance into the provider boundary', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) expect(registry.recordResolved('TOKEN', 'secret-value')).toBe(true) const knowledgeBody = { data: { results: [{ content: 'reference-secret' }] }, @@ -167,7 +172,7 @@ describe('validateHallucination', () => { }) it('fails only the hallucination model-bound leg when Knowledge omits private provenance', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) vi.stubGlobal( 'fetch', vi.fn(async () => Response.json({ data: { results: [{ content: 'public context' }] } })) @@ -190,7 +195,7 @@ describe('validateHallucination', () => { * consumer from the model actually hallucinating. */ it('surfaces a cancelled run as cancellation, not as a failed guardrail', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const fetchMock = vi.fn(async () => createPrivateKnowledgeResponse({ data: { results: [{ content: 'reference' }] } }) ) diff --git a/apps/sim/lib/knowledge/documents/document-processor-secret-provenance.test.ts b/apps/sim/lib/knowledge/documents/document-processor-secret-provenance.test.ts index 345e01f3002..537679cd684 100644 --- a/apps/sim/lib/knowledge/documents/document-processor-secret-provenance.test.ts +++ b/apps/sim/lib/knowledge/documents/document-processor-secret-provenance.test.ts @@ -36,7 +36,10 @@ import { env } from '@/lib/core/config/env' import { RESOLVED_SECRET_PROVENANCE_FIELD } from '@/lib/execution/private-tool-metadata' import { processDocument } from '@/lib/knowledge/documents/document-processor' import { runWithKnowledgeModelInputProvenance } from '@/lib/knowledge/model-input-provenance' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' describe('knowledge document model-input provenance', () => { beforeEach(() => { @@ -57,9 +60,11 @@ describe('knowledge document model-input provenance', () => { }) it('parses tracked workspace-file bytes locally without treating parsing as model egress', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'tracked-secret', encryptedValue: 'encrypted-token' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'tracked-secret', encryptedValue: 'encrypted-token' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'tracked-secret') mockDownloadFileFromUrl.mockResolvedValue( Buffer.from('Locally parsed content containing tracked-secret.') diff --git a/apps/sim/lib/knowledge/model-input-provenance.test.ts b/apps/sim/lib/knowledge/model-input-provenance.test.ts index 3ae612a015d..cf4ec2a541e 100644 --- a/apps/sim/lib/knowledge/model-input-provenance.test.ts +++ b/apps/sim/lib/knowledge/model-input-provenance.test.ts @@ -13,7 +13,10 @@ import { projectKnowledgeModelInput, runWithKnowledgeModelInputProvenance, } from '@/lib/knowledge/model-input-provenance' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' function verifiedHeaders(): Headers { return new Headers({ @@ -118,12 +121,16 @@ describe('Knowledge model input provenance', () => { }) it('projects exact active values only inside the matching asynchronous context', async () => { - const first = new ResolvedSecretTraceRegistry([ - { name: 'FIRST', plaintext: 'first-secret', encryptedValue: 'encrypted-first' }, - ]) - const second = new ResolvedSecretTraceRegistry([ - { name: 'SECOND', plaintext: 'second-secret', encryptedValue: 'encrypted-second' }, - ]) + const first = new ResolvedSecretTraceRegistry( + [{ name: 'FIRST', plaintext: 'first-secret', encryptedValue: 'encrypted-first' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) + const second = new ResolvedSecretTraceRegistry( + [{ name: 'SECOND', plaintext: 'second-secret', encryptedValue: 'encrypted-second' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) first.recordResolved('FIRST', 'first-secret') second.recordResolved('SECOND', 'second-secret') @@ -146,7 +153,7 @@ describe('Knowledge model input provenance', () => { }) it('fails before model egress when an active request registry is incomplete', () => { - const registry = new ResolvedSecretTraceRegistry([]) + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() expect(() => diff --git a/apps/sim/lib/knowledge/model-input-provenance.ts b/apps/sim/lib/knowledge/model-input-provenance.ts index c7db243ca4b..cdb4c869cb8 100644 --- a/apps/sim/lib/knowledge/model-input-provenance.ts +++ b/apps/sim/lib/knowledge/model-input-provenance.ts @@ -3,6 +3,7 @@ import { prepareCopilotEnvironmentContext } from '@/lib/copilot/environment-cont import { inspectModelInputProvenanceRequest } from '@/lib/execution/model-input-provenance' import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection' import { + EMPTY_NON_SECRET_NAMES, isResolvedSecretTraceProvenanceV1, ResolvedSecretTraceRegistry, } from '@/executor/utils/resolved-secret-trace-registry' @@ -52,10 +53,14 @@ export async function prepareKnowledgeModelInputProvenance(options: { if (inspection.value.entries.length === 0) { return { success: true, - registry: new ResolvedSecretTraceRegistry([], { - userId: options.userId, - ...(options.workspaceId ? { workspaceId: options.workspaceId } : {}), - }), + registry: new ResolvedSecretTraceRegistry( + [], + { + userId: options.userId, + ...(options.workspaceId ? { workspaceId: options.workspaceId } : {}), + }, + EMPTY_NON_SECRET_NAMES + ), } } @@ -106,7 +111,7 @@ export function getKnowledgeOpaqueModelInputRegistry(): ResolvedSecretTraceRegis if (!context?.opaqueInputSafe) { throw new Error(MODEL_INPUT_PROJECTION_ERROR) } - return context.registry ?? new ResolvedSecretTraceRegistry() + return context.registry ?? new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) } /** Projects one string immediately before it enters an embedding or reranking request. */ diff --git a/apps/sim/lib/knowledge/reranker.test.ts b/apps/sim/lib/knowledge/reranker.test.ts index e299fe6a223..0ddf8efbb29 100644 --- a/apps/sim/lib/knowledge/reranker.test.ts +++ b/apps/sim/lib/knowledge/reranker.test.ts @@ -6,7 +6,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { env } from '@/lib/core/config/env' import { runWithKnowledgeModelInputProvenance } from '@/lib/knowledge/model-input-provenance' import { rerank } from '@/lib/knowledge/reranker' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const envSnapshot = { ...env } @@ -25,9 +28,11 @@ describe('Knowledge reranker model boundary', () => { }) it('projects query and documents at egress while returning the original item', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'encrypted-token' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'encrypted-token' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'secret-value') const item = { id: 'chunk-1', text: 'stored secret-value content' } diff --git a/apps/sim/lib/knowledge/secret-provenance.ts b/apps/sim/lib/knowledge/secret-provenance.ts index 03802733807..4337bdcb0c6 100644 --- a/apps/sim/lib/knowledge/secret-provenance.ts +++ b/apps/sim/lib/knowledge/secret-provenance.ts @@ -19,6 +19,7 @@ import { normalizeDurableSecretProvenanceEntries, } from '@/lib/execution/durable-secret-provenance' import { + EMPTY_NON_SECRET_NAMES, ResolvedSecretTraceRegistry, type ResolvedSecretTraceScopeV1, } from '@/executor/utils/resolved-secret-trace-registry' @@ -408,7 +409,7 @@ export async function loadKnowledgeDocumentSecretRegistry( provenance, tracked: row.secretProvenanceVersion === 1 || currentSourceFileProvenance !== undefined, } - const registry = new ResolvedSecretTraceRegistry([], scope) + const registry = new ResolvedSecretTraceRegistry([], scope, EMPTY_NON_SECRET_NAMES) if (!(await importDurableSecretProvenance(registry, provenance))) { throw new Error('Knowledge document secret provenance is unavailable') } diff --git a/apps/sim/lib/logs/execution/logging-session.test.ts b/apps/sim/lib/logs/execution/logging-session.test.ts index f9624ff79d2..7f1540d3d4d 100644 --- a/apps/sim/lib/logs/execution/logging-session.test.ts +++ b/apps/sim/lib/logs/execution/logging-session.test.ts @@ -112,6 +112,7 @@ vi.mock('@/lib/logs/execution/logging-factory', () => ({ import { calculateCostSummary } from '@/lib/logs/execution/logging-factory' import { + EMPTY_NON_SECRET_NAMES, type ResolvedSecretTraceMatch, ResolvedSecretTraceRegistry, } from '@/executor/utils/resolved-secret-trace-registry' @@ -150,9 +151,11 @@ describe('LoggingSession diagnostic projection', () => { const secret = 'logging-session-secret-7f3a91' const error = new Error(`failed ${secret} __var_API_KEY __sim_code_2_binding_1`) const session = new LoggingSession('workflow-1', 'execution-1', 'manual') - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-api-key' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-api-key' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('API_KEY', secret) session.setResolvedSecretTraceRegistry(registry) diff --git a/apps/sim/lib/logs/execution/logging-session.ts b/apps/sim/lib/logs/execution/logging-session.ts index b565fb2d2e6..23ae1ca6fe1 100644 --- a/apps/sim/lib/logs/execution/logging-session.ts +++ b/apps/sim/lib/logs/execution/logging-session.ts @@ -44,6 +44,7 @@ import type { SerializableExecutionState } from '@/executor/execution/types' import type { BlockLog } from '@/executor/types' import { projectResolvedSecretDiagnosticError } from '@/executor/utils/resolved-secret-content-projection' import { + EMPTY_NON_SECRET_NAMES, isResolvedSecretTraceProvenanceV1, RESOLVED_SECRET_TRACE_CHECKPOINT_VERSION, type ResolvedSecretTraceProvenanceV1, @@ -296,15 +297,16 @@ export class LoggingSession { private async createDisplayProjectionRegistry( provenance?: unknown ): Promise { - if (provenance === undefined) return new ResolvedSecretTraceRegistry() + if (provenance === undefined) + return new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) if (!isResolvedSecretTraceProvenanceV1(provenance)) { - const incomplete = new ResolvedSecretTraceRegistry() + const incomplete = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) incomplete.markIncomplete() return incomplete } - const registry = new ResolvedSecretTraceRegistry([], provenance.scope) + const registry = new ResolvedSecretTraceRegistry([], provenance.scope, EMPTY_NON_SECRET_NAMES) await registry.importProvenance(provenance, { trusted: true }) return registry } @@ -482,7 +484,8 @@ export class LoggingSession { ): Promise { const registry = sourceSpan.displayResolvedSecretTraceProvenance ? await this.createDisplayProjectionRegistry(sourceSpan.displayResolvedSecretTraceProvenance) - : (inheritedRegistry ?? new ResolvedSecretTraceRegistry()) + : (inheritedRegistry ?? + new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES)) if (registry) registryBySpanId.set(sourceSpan.id, registry) const { children, ...spanWithoutChildren } = sourceSpan @@ -765,7 +768,8 @@ export class LoggingSession { const scopeUserId = userId ?? this.actorUserId this.resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry( [], - scopeUserId ? { userId: scopeUserId, workspaceId } : undefined + scopeUserId ? { userId: scopeUserId, workspaceId } : undefined, + EMPTY_NON_SECRET_NAMES ) if (skipLogCreation) this.resolvedSecretTraceRegistry.markIncomplete() } diff --git a/apps/sim/lib/logs/execution/trace-secret-projection.test.ts b/apps/sim/lib/logs/execution/trace-secret-projection.test.ts index ccc8969e2ba..1c1373f809c 100644 --- a/apps/sim/lib/logs/execution/trace-secret-projection.test.ts +++ b/apps/sim/lib/logs/execution/trace-secret-projection.test.ts @@ -30,6 +30,7 @@ import { } from '@/lib/logs/execution/trace-secret-projection' import type { TraceSpan } from '@/lib/logs/types' import { + EMPTY_NON_SECRET_NAMES, type ResolvedSecretTraceMatch, ResolvedSecretTraceRegistry, } from '@/executor/utils/resolved-secret-trace-registry' @@ -72,13 +73,17 @@ beforeEach(() => { describe('projectTraceSpansForSecrets', () => { it('removes compiler and legacy runtime aliases from projected trace content', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'API_SECRET', - plaintext: 'trace-secret', - encryptedValue: 'encrypted-api-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'API_SECRET', + plaintext: 'trace-secret', + encryptedValue: 'encrypted-api-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) expect(registry.recordResolved('API_SECRET', 'trace-secret')).toBe(true) const [projected] = await projectTraceSpansForSecrets( @@ -102,9 +107,11 @@ describe('projectTraceSpansForSecrets', () => { }) it('preserves named provenance when an exact secret name and value overlap', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'Test', plaintext: 'Test', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) expect(registry.recordResolved('Test', 'Test')).toBe(true) const source = createSpan({ input: { code: 'return {{Test}}' }, @@ -138,13 +145,17 @@ describe('projectTraceSpansForSecrets', () => { it('protects only literals activated by a successful Secrets-tab substitution', async () => { const plaintext = 'trace/secret+value' - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'API_SECRET', - plaintext, - encryptedValue: 'encrypted-api-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'API_SECRET', + plaintext, + encryptedValue: 'encrypted-api-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) const transformations = { urlEncoded: encodeURIComponent(plaintext), base64: Buffer.from(plaintext).toString('base64'), diff --git a/apps/sim/lib/logs/execution/trace-store.ts b/apps/sim/lib/logs/execution/trace-store.ts index 031d86216d0..332dade9ebb 100644 --- a/apps/sim/lib/logs/execution/trace-store.ts +++ b/apps/sim/lib/logs/execution/trace-store.ts @@ -6,6 +6,7 @@ import { materializeLargeValueRef, storeLargeValue } from '@/lib/execution/paylo import { projectTraceSpansForSecrets } from '@/lib/logs/execution/trace-secret-projection' import type { TraceSpan } from '@/lib/logs/types' import { + EMPTY_NON_SECRET_NAMES, isResolvedSecretTraceProvenanceV1, ResolvedSecretTraceRegistry, } from '@/executor/utils/resolved-secret-trace-registry' @@ -274,7 +275,7 @@ export async function projectExecutionDataForDisplay( let registry: ResolvedSecretTraceRegistry | undefined if (isResolvedSecretTraceProvenanceV1(provenance)) { - registry = new ResolvedSecretTraceRegistry([], provenance.scope) + registry = new ResolvedSecretTraceRegistry([], provenance.scope, EMPTY_NON_SECRET_NAMES) await registry.importProvenance(provenance, { trusted: true }) } @@ -298,8 +299,8 @@ export async function projectExecutionDataForDisplay( const exactProvenance = executionState[provenanceKey] const exactRegistry = isResolvedSecretTraceProvenanceV1(exactProvenance) - ? new ResolvedSecretTraceRegistry([], exactProvenance.scope) - : new ResolvedSecretTraceRegistry() + ? new ResolvedSecretTraceRegistry([], exactProvenance.scope, EMPTY_NON_SECRET_NAMES) + : new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) if (isResolvedSecretTraceProvenanceV1(exactProvenance)) { await exactRegistry.importProvenance(exactProvenance, { trusted: true }) } else { diff --git a/apps/sim/lib/mcp/resolve-config.ts b/apps/sim/lib/mcp/resolve-config.ts index c3c21a104d0..7c5756fad81 100644 --- a/apps/sim/lib/mcp/resolve-config.ts +++ b/apps/sim/lib/mcp/resolve-config.ts @@ -75,6 +75,7 @@ export async function resolveMcpConfigEnvVars( personalDecrypted: env.personalDecrypted, workspaceDecrypted: env.workspaceDecrypted, decryptionFailures: env.decryptionFailures, + nonSecretNames: new Set(env.workspaceVariableKeys), scope, }) } catch (error) { diff --git a/apps/sim/lib/model-router/resolve.test.ts b/apps/sim/lib/model-router/resolve.test.ts index 094e1293a86..50e5a5aac35 100644 --- a/apps/sim/lib/model-router/resolve.test.ts +++ b/apps/sim/lib/model-router/resolve.test.ts @@ -53,14 +53,21 @@ import { resolveAutoModel, } from '@/lib/model-router/resolve' import type { ExecutionContext } from '@/executor/types' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' const ctx = { userId: 'user-1', workspaceId: 'ws-1', workflowId: 'wf-1', executionId: 'exec-1', - resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ), } as unknown as ExecutionContext /** Distinct-by-default signals so the module-level decision cache never collides across tests. */ @@ -166,18 +173,22 @@ describe('resolveAutoModel', () => { }) it('projects active secrets from model-visible signals without changing routing controls', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'NUMBER_SECRET', - plaintext: '123', - encryptedValue: 'encrypted-number-secret', - }, - { - name: 'BOOLEAN_SECRET', - plaintext: 'true', - encryptedValue: 'encrypted-boolean-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'NUMBER_SECRET', + plaintext: '123', + encryptedValue: 'encrypted-number-secret', + }, + { + name: 'BOOLEAN_SECRET', + plaintext: 'true', + encryptedValue: 'encrypted-boolean-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('NUMBER_SECRET', '123') registry.recordResolved('BOOLEAN_SECRET', 'true') mockFetchGo.mockResolvedValue(routerResponse({ choice: '1' })) @@ -209,13 +220,17 @@ describe('resolveAutoModel', () => { }) it('does not infer provenance from dormant catalog values in routing signals', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'DORMANT_SECRET', - plaintext: 'ordinary-tool-name', - encryptedValue: 'encrypted-dormant-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'DORMANT_SECRET', + plaintext: 'ordinary-tool-name', + encryptedValue: 'encrypted-dormant-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockFetchGo.mockResolvedValue(routerResponse({ choice: '1' })) await resolveAutoModel({ @@ -230,7 +245,7 @@ describe('resolveAutoModel', () => { }) it('falls back without calling mothership when signal provenance is incomplete', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() const result = await resolveAutoModel({ diff --git a/apps/sim/lib/table/backfill-runner.ts b/apps/sim/lib/table/backfill-runner.ts index c9041f84b87..61cd1562d1b 100644 --- a/apps/sim/lib/table/backfill-runner.ts +++ b/apps/sim/lib/table/backfill-runner.ts @@ -31,6 +31,7 @@ import type { WorkflowGroupOutput, } from '@/lib/table/types' import { + EMPTY_NON_SECRET_NAMES, isResolvedSecretTraceProvenanceV1, RESOLVED_SECRET_TRACE_CHECKPOINT_VERSION, ResolvedSecretTraceRegistry, @@ -77,7 +78,11 @@ export async function createBackfillExecutionSecretRegistry(options: { state?.sourceExecutionId === options.executionId && isResolvedSecretTraceProvenanceV1(provenance) && provenance.scope?.workspaceId === options.workspaceId - const registry = new ResolvedSecretTraceRegistry([], valid ? provenance.scope : undefined) + const registry = new ResolvedSecretTraceRegistry( + [], + valid ? provenance.scope : undefined, + EMPTY_NON_SECRET_NAMES + ) if (!valid) { registry.markIncomplete() return registry diff --git a/apps/sim/lib/table/rows/secret-provenance.ts b/apps/sim/lib/table/rows/secret-provenance.ts index cb75cb6abec..73f7c3f1d40 100644 --- a/apps/sim/lib/table/rows/secret-provenance.ts +++ b/apps/sim/lib/table/rows/secret-provenance.ts @@ -9,6 +9,7 @@ import { and, asc, eq, gt, inArray, type SQL, sql } from 'drizzle-orm' import type { DbExecutor, DbTransaction } from '@/lib/table/planner' import type { RowData, TableRowSecretProvenanceWrite } from '@/lib/table/types' import { + EMPTY_NON_SECRET_NAMES, isResolvedSecretTraceProvenanceV1, type ResolvedSecretTraceProvenanceEntryV1, type ResolvedSecretTraceProvenanceV1, @@ -251,7 +252,7 @@ export async function createTableRowSecretProvenanceFromEncryptedExecution( if (!isResolvedSecretTraceProvenanceV1(provenance)) { return createUnknownTableRowSecretProvenance() } - const registry = new ResolvedSecretTraceRegistry([], provenance.scope) + const registry = new ResolvedSecretTraceRegistry([], provenance.scope, EMPTY_NON_SECRET_NAMES) if (!(await registry.importProvenance(provenance, { trusted: true })) || !registry.isComplete()) { return createUnknownTableRowSecretProvenance() } diff --git a/apps/sim/lib/table/secret-provenance-selection.test.ts b/apps/sim/lib/table/secret-provenance-selection.test.ts index f54a59a835e..d42091934cd 100644 --- a/apps/sim/lib/table/secret-provenance-selection.test.ts +++ b/apps/sim/lib/table/secret-provenance-selection.test.ts @@ -4,7 +4,10 @@ import { describe, expect, it } from 'vitest' import { PRIVATE_SECRET_PROVENANCE_FIELD } from '@/lib/execution/private-tool-metadata' import { selectTableRowSecretProvenance } from '@/lib/table/secret-provenance-selection' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { prepareToolRequest } from '@/tools/request-transport' import { tableBatchInsertRowsTool } from '@/tools/table/batch_insert_rows' @@ -38,10 +41,14 @@ describe('selectTableRowSecretProvenance', () => { rows: [{ email: 'user@example.com', status: 'queued', processed_at: undefined }], _context: { workspaceId: 'workspace-1' }, }, - new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) ) const body = JSON.parse(request.body ?? '') as TableWriteRequestBody const wireSelectionKeys = body.rows.flatMap((row, rowIndex) => diff --git a/apps/sim/lib/workflows/executor/execution-core.test.ts b/apps/sim/lib/workflows/executor/execution-core.test.ts index 2be8bdd0fd9..39f4d792942 100644 --- a/apps/sim/lib/workflows/executor/execution-core.test.ts +++ b/apps/sim/lib/workflows/executor/execution-core.test.ts @@ -357,6 +357,41 @@ describe('executeWorkflowCore terminal finalization sequencing', () => { ) }) + /** + * The persisted `environment` object used to be safe BY CONSTRUCTION — it was + * the encrypted map, so no downstream bug could put a secret plaintext on a + * log row. Non-secret values make it safe BY POLICY instead, and log rows are + * immutable and exportable, so this asserts BOTH halves in one run: a test + * that only checked the non-secret value would still pass if the substitution + * accidentally widened to every key. + */ + it('logs non-secret values in plaintext while secrets stay ciphertext', async () => { + getPersonalAndWorkspaceEnvMock.mockResolvedValue({ + personalEncrypted: {}, + workspaceEncrypted: { + SUPPORT_EMAIL: 'iv:cipher-email:tag', + STRIPE_KEY: 'iv:cipher-stripe:tag', + }, + personalDecrypted: {}, + workspaceDecrypted: { + SUPPORT_EMAIL: 'help@acme.com', + STRIPE_KEY: 'sk-live-do-not-log', + }, + workspaceVariableKeys: ['SUPPORT_EMAIL'], + }) + + await executeWorkflowCore({ + snapshot: createSnapshot() as any, + callbacks: {}, + loggingSession: loggingSession as any, + }) + + const loggedVariables = safeStartMock.mock.calls[0]?.[0]?.variables + expect(loggedVariables.SUPPORT_EMAIL).toBe('help@acme.com') + expect(loggedVariables.STRIPE_KEY).toBe('iv:cipher-stripe:tag') + expect(JSON.stringify(loggedVariables)).not.toContain('sk-live-do-not-log') + }) + it('starts logging with the workflow state that will be executed', async () => { const executedWorkflowState = { blocks: { diff --git a/apps/sim/lib/workflows/executor/execution-core.ts b/apps/sim/lib/workflows/executor/execution-core.ts index 4af75820f9d..81a44861955 100644 --- a/apps/sim/lib/workflows/executor/execution-core.ts +++ b/apps/sim/lib/workflows/executor/execution-core.ts @@ -510,10 +510,23 @@ async function executeWorkflowCoreImpl( personalDecrypted, workspaceDecrypted, decryptionFailures, + workspaceVariableKeys, } = env - // Use encrypted values for logging (don't log decrypted secrets) - const variables = EnvVarsSchema.parse({ ...personalEncrypted, ...workspaceEncrypted }) + const nonSecretNames = new Set(workspaceVariableKeys) + + // Secrets are logged as ciphertext so their plaintext cannot reach the log + // row by any downstream path. Keys explicitly marked non-secret are logged + // in the clear — being legible in a run's environment is the point of the + // flag, and this is the one place that decision is applied. + const variables = EnvVarsSchema.parse( + Object.fromEntries( + Object.entries({ ...personalEncrypted, ...workspaceEncrypted }).map(([key, encrypted]) => [ + key, + nonSecretNames.has(key) ? (workspaceDecrypted[key] ?? encrypted) : encrypted, + ]) + ) + ) // Use already-decrypted values for execution (no redundant decryption) const decryptedEnvVars: Record = { ...personalDecrypted, ...workspaceDecrypted } @@ -532,6 +545,7 @@ async function executeWorkflowCoreImpl( personalDecrypted, workspaceDecrypted, decryptionFailures, + nonSecretNames, restoredProvenance: restoreTrusted ? restoredState?.resolvedSecretTraceProvenance : undefined, restoredCheckpointVersion: restoredState?.resolvedSecretTraceCheckpointVersion, restoreTrusted, diff --git a/apps/sim/lib/workflows/executor/input-secret-provenance.ts b/apps/sim/lib/workflows/executor/input-secret-provenance.ts index dcb4c80a71d..9e3ba58c010 100644 --- a/apps/sim/lib/workflows/executor/input-secret-provenance.ts +++ b/apps/sim/lib/workflows/executor/input-secret-provenance.ts @@ -3,6 +3,7 @@ import { isPrivateSecretProvenanceBundleV1, } from '@/lib/execution/model-input-provenance' import { + EMPTY_NON_SECRET_NAMES, type ResolvedSecretTraceProvenanceV1, ResolvedSecretTraceRegistry, } from '@/executor/utils/resolved-secret-trace-registry' @@ -77,7 +78,11 @@ export async function resolveWorkflowInputSecretProvenance(options: { return { success: false, error: INVALID_WORKFLOW_INPUT_PROVENANCE_ERROR } } - const sourceRegistry = new ResolvedSecretTraceRegistry([], provenance.scope) + const sourceRegistry = new ResolvedSecretTraceRegistry( + [], + provenance.scope, + EMPTY_NON_SECRET_NAMES + ) const imported = await sourceRegistry.importProvenance(provenance, { trusted: true }) const inputProvenance = sourceRegistry.exportProvenanceForValue(options.input) if ( diff --git a/apps/sim/providers/anthropic/streaming-tool-loop.test.ts b/apps/sim/providers/anthropic/streaming-tool-loop.test.ts index a6942d81750..4375907d9b0 100644 --- a/apps/sim/providers/anthropic/streaming-tool-loop.test.ts +++ b/apps/sim/providers/anthropic/streaming-tool-loop.test.ts @@ -5,7 +5,10 @@ * text classified by turn_end, abort → cancelled, per-turn usage accumulation. */ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { anthropicThinkingTextToolExpectedThinking, anthropicThinkingTextToolStreamEvents, @@ -257,9 +260,11 @@ describe('createAnthropicStreamingToolLoopStream', () => { it('keeps raw tool results for execution records and projects only the model continuation', async () => { const secret = 'secret-value' - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) mockPrepareToolExecution.mockReturnValue({ toolParams: { token: secret }, diff --git a/apps/sim/providers/file-attachments.server.test.ts b/apps/sim/providers/file-attachments.server.test.ts index f95c295c649..f30563c6fb5 100644 --- a/apps/sim/providers/file-attachments.server.test.ts +++ b/apps/sim/providers/file-attachments.server.test.ts @@ -2,7 +2,10 @@ * @vitest-environment node */ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { buildOpenAIMessageContent, INLINE_ATTACHMENT_THRESHOLD_BYTES, @@ -126,9 +129,11 @@ describe('OpenAI large-file attachment lifecycle', () => { it('projects the multipart filename without mutating upload preparation metadata', async () => { const request = makeRequest(CSV_BYTES) - const registry = new ResolvedSecretTraceRegistry([ - { name: 'FILE_NAME', plaintext: 'data_10mb.csv', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'FILE_NAME', plaintext: 'data_10mb.csv', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('FILE_NAME', 'data_10mb.csv') await runWithProviderRuntimeContext({ resolvedSecretTraceRegistry: registry }, async () => { diff --git a/apps/sim/providers/index.test.ts b/apps/sim/providers/index.test.ts index a5a72e386d5..9d5f3a67284 100644 --- a/apps/sim/providers/index.test.ts +++ b/apps/sim/providers/index.test.ts @@ -39,7 +39,10 @@ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-secret-provenance', () mockFilterModelSafeWorkspaceFileAttachments(...args), })) -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { executeProviderRequest } from '@/providers' import type { ProviderResponse } from '@/providers/types' @@ -453,9 +456,11 @@ describe('executeProviderRequest — model secret projection', () => { it('projects only model-visible request content before provider execution', async () => { const secret = 'quoted"secret\\with\nnewline' - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) await executeProviderRequest( @@ -574,7 +579,7 @@ describe('executeProviderRequest — model secret projection', () => { }) it('does not infer provenance from a dormant request environment map', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) await executeProviderRequest( 'anthropic', @@ -591,10 +596,14 @@ describe('executeProviderRequest — model secret projection', () => { }) it('does not let dormant low-entropy secrets invalidate ordinary prompts or JSON Schema', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TYPE_SECRET', plaintext: 'string', encryptedValue: 'encrypted-type' }, - { name: 'BOOLEAN_SECRET', plaintext: 'true', encryptedValue: 'encrypted-boolean' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'TYPE_SECRET', plaintext: 'string', encryptedValue: 'encrypted-type' }, + { name: 'BOOLEAN_SECRET', plaintext: 'true', encryptedValue: 'encrypted-boolean' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) await executeProviderRequest( 'openai', @@ -629,9 +638,11 @@ describe('executeProviderRequest — model secret projection', () => { }) it('does not carry an earlier active secret into unrelated public schema grammar', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TYPE_SECRET', plaintext: 'string', encryptedValue: 'encrypted-type' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TYPE_SECRET', plaintext: 'string', encryptedValue: 'encrypted-type' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TYPE_SECRET', 'string') await executeProviderRequest( @@ -667,9 +678,11 @@ describe('executeProviderRequest — model secret projection', () => { }) it('omits only the response format when an active secret collides with its semantic keys', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'SCHEMA_KEY', plaintext: 'messages', encryptedValue: 'encrypted-schema-key' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'SCHEMA_KEY', plaintext: 'messages', encryptedValue: 'encrypted-schema-key' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('SCHEMA_KEY', 'messages') await executeProviderRequest( @@ -700,9 +713,11 @@ describe('executeProviderRequest — model secret projection', () => { }) it('omits a safe schema when no deterministic response-format name avoids an active secret', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'UNDERSCORE', plaintext: '_', encryptedValue: 'encrypted-underscore' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'UNDERSCORE', plaintext: '_', encryptedValue: 'encrypted-underscore' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('UNDERSCORE', '_') await executeProviderRequest( @@ -725,7 +740,7 @@ describe('executeProviderRequest — model secret projection', () => { }) it('omits malformed and oversized optional schemas without failing the model call', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const oversizedSchema = { allOf: new Array(100_001) } for (const schema of [{ properties: { field: 'not-a-schema' } }, oversizedSchema]) { @@ -760,9 +775,11 @@ describe('executeProviderRequest — model secret projection', () => { it('keeps attachment metadata raw through storage resolution and provider upload', async () => { const secret = 'attachment-secret' const rawStorageKey = `workspace/raw-${secret}/document.pdf` - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) await executeProviderRequest( @@ -864,9 +881,11 @@ describe('executeProviderRequest — model secret projection', () => { }) it('projects JSON arguments without mutating attachment metadata before serialization', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'TOKEN', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'TOKEN', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'TOKEN') await executeProviderRequest( @@ -936,9 +955,11 @@ describe('executeProviderRequest — model secret projection', () => { it.each(['123', 'true'])( 'keeps low-entropy JSON valid, projects typed conversions, and preserves transport IDs (%s)', async (secret) => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) const converted = secret === '123' ? 123 : true @@ -1112,9 +1133,11 @@ describe('executeProviderRequest — model secret projection', () => { 'guards arbitrary %s schema controls while omitting only the unsafe model capability', async (controlKey) => { const secret = `schema-control-secret-${controlKey}` - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) const unsafeSchema = { type: 'object', @@ -1176,9 +1199,11 @@ describe('executeProviderRequest — model secret projection', () => { ])( 'preserves validated schema controls when they equal active secret bytes (%s)', async (secret, schema) => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) await executeProviderRequest( @@ -1224,9 +1249,11 @@ describe('executeProviderRequest — model secret projection', () => { ) it('forwards safe canonical schema controls byte-for-byte', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'unrelated-secret', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'unrelated-secret', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) const schema = { type: ['object', 'null'], nullable: true, @@ -1249,9 +1276,11 @@ describe('executeProviderRequest — model secret projection', () => { it.each(['123', 'true'])( 'omits a response schema whose semantic value equals a secret (%s)', async (secret) => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', secret) const semanticValue = secret === '123' ? 123 : true @@ -1278,7 +1307,7 @@ describe('executeProviderRequest — model secret projection', () => { ) it('fails before invoking a provider when an expected registry is incomplete or missing', async () => { - const incomplete = new ResolvedSecretTraceRegistry() + const incomplete = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) incomplete.markIncomplete() await expect( diff --git a/apps/sim/providers/openai/utils.test.ts b/apps/sim/providers/openai/utils.test.ts index b6e787feea4..ee40ed26dea 100644 --- a/apps/sim/providers/openai/utils.test.ts +++ b/apps/sim/providers/openai/utils.test.ts @@ -3,7 +3,10 @@ */ import type OpenAI from 'openai' import { describe, expect, it } from 'vitest' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { buildResponsesInputFromMessages, parseResponsesUsage, @@ -116,9 +119,11 @@ describe('buildResponsesInputFromMessages', () => { }) it('projects a document filename at the Responses serialization boundary', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'FILE_NAME', plaintext: 'report.pdf', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'FILE_NAME', plaintext: 'report.pdf', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('FILE_NAME', 'report.pdf') const input = runWithProviderRuntimeContext({ resolvedSecretTraceRegistry: registry }, () => diff --git a/apps/sim/providers/runtime-context.test.ts b/apps/sim/providers/runtime-context.test.ts index ea5d305a528..3f213a5490c 100644 --- a/apps/sim/providers/runtime-context.test.ts +++ b/apps/sim/providers/runtime-context.test.ts @@ -11,7 +11,10 @@ vi.mock('@/tools', () => ({ executeTool: mockExecuteTool, })) -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { type ExecuteProviderToolOptions, executeProviderTool as executeProviderToolWithInput, @@ -40,9 +43,11 @@ describe('provider runtime context', () => { }) it('projects a provider-bound filename while preserving its inferred extension', () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'FILE_NAME', plaintext: 'report.pdf', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'FILE_NAME', plaintext: 'report.pdf', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('FILE_NAME', 'report.pdf') const projected = runWithProviderRuntimeContext({ resolvedSecretTraceRegistry: registry }, () => @@ -54,8 +59,8 @@ describe('provider runtime context', () => { }) it('isolates concurrent tool executions without adding registry data to params', async () => { - const registryA = new ResolvedSecretTraceRegistry() - const registryB = new ResolvedSecretTraceRegistry() + const registryA = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) + const registryB = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) await Promise.all([ runWithProviderRuntimeContext({ resolvedSecretTraceRegistry: registryA }, async () => { @@ -83,7 +88,7 @@ describe('provider runtime context', () => { }) it('preserves runtime context in a stream consumed after the provider call returns', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const stream = runWithProviderRuntimeContext( { resolvedSecretTraceRegistry: registry }, () => @@ -105,9 +110,11 @@ describe('provider runtime context', () => { }) it('does not treat an arbitrary tool-result collision as secret provenance', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) const rawResult = { success: true, output: { direct: 'secret-value', quoted: 'line\n"secret-value"', alias: '__var_TOKEN' }, @@ -129,11 +136,15 @@ describe('provider runtime context', () => { }) it('does not seed provenance from ambient execution context', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TEXT', plaintext: 'Test', encryptedValue: 'encrypted-text' }, - { name: 'BOOLEAN', plaintext: 'true', encryptedValue: 'encrypted-boolean' }, - { name: 'NUMBER', plaintext: '123', encryptedValue: 'encrypted-number' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { name: 'TEXT', plaintext: 'Test', encryptedValue: 'encrypted-text' }, + { name: 'BOOLEAN', plaintext: 'true', encryptedValue: 'encrypted-boolean' }, + { name: 'NUMBER', plaintext: '123', encryptedValue: 'encrypted-number' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TEXT', 'Test') registry.recordResolved('BOOLEAN', 'true') registry.recordResolved('NUMBER', '123') @@ -174,9 +185,11 @@ describe('provider runtime context', () => { }) it('does not treat a static tool parameter name as secret-bearing input', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'PARAM_NAME', plaintext: 'prompt', encryptedValue: 'encrypted-param-name' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'PARAM_NAME', plaintext: 'prompt', encryptedValue: 'encrypted-param-name' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('PARAM_NAME', 'prompt') const rawResult = { success: true, output: { value: 'prompt' } } mockExecuteTool.mockResolvedValueOnce(rawResult) @@ -191,9 +204,11 @@ describe('provider runtime context', () => { }) it('projects a secret inherited through the exact current tool input', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('TOKEN', 'secret-value') const rawResult = { success: true, @@ -227,7 +242,7 @@ describe('provider runtime context', () => { }) it('serializes dates for provider continuations while preserving the raw tool response', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const createdAt = new Date('2026-08-05T12:34:56.789Z') const rawResult = { success: true, @@ -256,7 +271,7 @@ describe('provider runtime context', () => { ])( 'preserves safe primitive %s tool output for provider continuations', async (_, output, expected) => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) mockExecuteTool.mockResolvedValueOnce({ success: true, output }) const result = await runWithProviderRuntimeContext( @@ -269,9 +284,11 @@ describe('provider runtime context', () => { ) it('projects a primitive secret-bearing tool output for provider continuations', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => { options.resolvedSecretTraceRegistry?.recordResolved('TOKEN', 'secret-value') return { success: true, output: 'secret-value' } @@ -288,9 +305,11 @@ describe('provider runtime context', () => { it.each(['123', 'true'])( 'leaves non-model resource metadata untouched while projecting content (%s)', async (secret) => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: secret, encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => { options.resolvedSecretTraceRegistry?.recordResolved('TOKEN', secret) return { @@ -331,9 +350,11 @@ describe('provider runtime context', () => { ) it('does not project resource metadata that is not serialized into the model continuation', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => { options.resolvedSecretTraceRegistry?.recordResolved('TOKEN', 'secret-value') return { @@ -378,10 +399,18 @@ describe('provider runtime context', () => { }) it('projects and merges a completed tool while a parallel sibling activation remains pending', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'COMPLETED', plaintext: 'completed-secret', encryptedValue: 'completed-ciphertext' }, - { name: 'SIBLING', plaintext: 'sibling-secret', encryptedValue: 'sibling-ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'COMPLETED', + plaintext: 'completed-secret', + encryptedValue: 'completed-ciphertext', + }, + { name: 'SIBLING', plaintext: 'sibling-secret', encryptedValue: 'sibling-ciphertext' }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) let releaseSibling: (() => void) | undefined const siblingGate = new Promise((resolve) => { releaseSibling = resolve @@ -415,9 +444,11 @@ describe('provider runtime context', () => { }) it('omits an incomplete model result and marks parent provenance incomplete', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => { options.resolvedSecretTraceRegistry?.markIncomplete() return { success: true, output: { value: 'secret-value' } } @@ -434,9 +465,11 @@ describe('provider runtime context', () => { }) it('structurally omits an incomplete failed model result and marks provenance incomplete', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => { options.resolvedSecretTraceRegistry?.markIncomplete() return { @@ -457,9 +490,11 @@ describe('provider runtime context', () => { }) it('retains child provenance for raw traces when model projection fails', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => { const toolCallRegistry = options.resolvedSecretTraceRegistry if (!toolCallRegistry) throw new Error('Missing tool-call registry') @@ -483,9 +518,11 @@ describe('provider runtime context', () => { }) it('keeps a raw thrown error separate from the omitted model error', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => { options.resolvedSecretTraceRegistry?.markIncomplete() throw new Error('secret-value') @@ -507,9 +544,11 @@ describe('provider runtime context', () => { }) it('fails closed for an incomplete registry', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.markIncomplete() mockExecuteTool.mockResolvedValueOnce({ success: true, @@ -533,9 +572,11 @@ describe('provider runtime context', () => { }) it('clears an inherited workflow context for an explicitly context-free provider call', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) const rawResult = { success: true, output: { value: 'secret-value' } } mockExecuteTool.mockResolvedValueOnce(rawResult) @@ -549,9 +590,11 @@ describe('provider runtime context', () => { }) it('preserves raw abort semantics without creating a model continuation', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'TOKEN', plaintext: 'secret-value', encryptedValue: 'ciphertext' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockExecuteTool.mockImplementationOnce(async (_toolId, _params, options) => { options.resolvedSecretTraceRegistry?.recordResolved('TOKEN', 'secret-value') throw new DOMException('secret-value', 'AbortError') diff --git a/apps/sim/tools/index.test.ts b/apps/sim/tools/index.test.ts index a0e4f2d90cc..297ac45f19e 100644 --- a/apps/sim/tools/index.test.ts +++ b/apps/sim/tools/index.test.ts @@ -32,6 +32,7 @@ import { createTimeoutAbortController } from '@/lib/core/execution-limits' import { INTERNAL_EXECUTION_DEADLINE_HEADER } from '@/lib/execution/execution-deadline-header' import { ANONYMOUS_SECRET_TRACE_REPLACEMENT, + EMPTY_NON_SECRET_NAMES, ResolvedSecretTraceRegistry, } from '@/executor/utils/resolved-secret-trace-registry' import { fileGetContentTool } from '@/tools/file/get' @@ -708,10 +709,14 @@ describe('executeTool Function', () => { }) it('imports File Get Content provenance without exposing private transport metadata', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) encryptionMockFns.mockDecryptSecret.mockResolvedValue({ decrypted: 'secret-value' }) global.fetch = Object.assign( vi.fn().mockResolvedValue( @@ -784,10 +789,14 @@ describe('executeTool Function', () => { ])( 'preserves an unverified error status for the $name without exposing its body or headers', async ({ toolId, status, params }) => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) const untrustedDetail = 'route-secret-plaintext' const untrustedHeader = 'route-secret-header-value' global.fetch = Object.assign( @@ -820,7 +829,7 @@ describe('executeTool Function', () => { ) it('maps an unverified non-error HTTP status to a metadata failure', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) global.fetch = Object.assign(vi.fn().mockResolvedValue(new Response(null, { status: 304 })), { preconnect: vi.fn(), }) as typeof fetch @@ -846,10 +855,14 @@ describe('executeTool Function', () => { }) it('contains incomplete File Get Content provenance within that tool call', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -895,10 +908,14 @@ describe('executeTool Function', () => { }) it('fails one legacy File Get Content response without poisoning the parent registry', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'user-1', - workspaceId: 'workspace-1', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'user-1', + workspaceId: 'workspace-1', + }, + EMPTY_NON_SECRET_NAMES + ) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response(JSON.stringify({ success: true, data: { contents: ['legacy content'] } }), { @@ -925,13 +942,17 @@ describe('executeTool Function', () => { }) it('consumes Function secret provenance without exposing private transport metadata', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'API_KEY', - plaintext: 'secret-value', - encryptedValue: 'encrypted-value', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'API_KEY', + plaintext: 'secret-value', + encryptedValue: 'encrypted-value', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -976,7 +997,7 @@ describe('executeTool Function', () => { }) it('marks legacy Function file exports unknown before exposing their receipt', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -1025,13 +1046,17 @@ describe('executeTool Function', () => { it('does not log plaintext or runtime aliases from Function errors', async () => { const secret = 'function-error-secret-value' const runtimeAlias = '__var_API_KEY' - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'API_KEY', - plaintext: secret, - encryptedValue: 'encrypted-value', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'API_KEY', + plaintext: secret, + encryptedValue: 'encrypted-value', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -1141,13 +1166,17 @@ describe('executeTool Function', () => { it('does not let pending custom-tool provenance affect an unrelated result', async () => { const secret = 'custom-tool-secret-value' - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'API_KEY', - plaintext: secret, - encryptedValue: 'encrypted-value', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'API_KEY', + plaintext: secret, + encryptedValue: 'encrypted-value', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) mockGetToolAsync.mockResolvedValueOnce({ id: 'custom_pending-provenance', name: 'Pending provenance custom tool', @@ -1222,7 +1251,7 @@ describe('executeTool Function', () => { }) it('isolates invalid custom-tool provenance and allows a later call to proceed', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) mockGetToolAsync.mockResolvedValue({ id: 'custom_invalid-provenance', name: 'Invalid provenance custom tool', @@ -1312,7 +1341,7 @@ describe('executeTool Function', () => { }) it('accepts a legacy Function response after reconstructing its local provenance', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) global.fetch = Object.assign( vi .fn() @@ -1340,9 +1369,11 @@ describe('executeTool Function', () => { }) it('reconstructs a crossing secret from a legacy Function response', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: 'legacy-secret', encryptedValue: 'encrypted-value' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: 'legacy-secret', encryptedValue: 'encrypted-value' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) encryptionMockFns.mockDecryptSecret.mockResolvedValueOnce({ decrypted: 'legacy-secret' }) global.fetch = Object.assign( vi.fn().mockResolvedValue( @@ -1379,7 +1410,8 @@ describe('executeTool Function', () => { encryptedValue: 'encrypted-input-secret', }, ], - { userId: 'parent-owner', workspaceId: 'workspace-456' } + { userId: 'parent-owner', workspaceId: 'workspace-456' }, + EMPTY_NON_SECRET_NAMES ) expect(registry.recordResolved('INPUT_SECRET', 'secret-value')).toBe(true) global.fetch = Object.assign( @@ -1444,10 +1476,14 @@ describe('executeTool Function', () => { }) it('filters cross-scope workflow provenance to literals present in the unchanged result', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'parent-user', - workspaceId: 'workspace-456', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'parent-user', + workspaceId: 'workspace-456', + }, + EMPTY_NON_SECRET_NAMES + ) encryptionMockFns.mockDecryptSecret.mockImplementation(async (encryptedValue: string) => ({ decrypted: encryptedValue === 'crossed-encrypted' ? 'crossed-secret' : 'unrelated-secret', })) @@ -1520,10 +1556,14 @@ describe('executeTool Function', () => { }) it('drops a legacy workflow response without poisoning later provenance', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'parent-user', - workspaceId: 'workspace-456', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'parent-user', + workspaceId: 'workspace-456', + }, + EMPTY_NON_SECRET_NAMES + ) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -1592,10 +1632,14 @@ describe('executeTool Function', () => { }) it('contains incomplete workflow provenance within that tool call', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'parent-user', - workspaceId: 'workspace-456', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'parent-user', + workspaceId: 'workspace-456', + }, + EMPTY_NON_SECRET_NAMES + ) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -1641,10 +1685,14 @@ describe('executeTool Function', () => { }) it('does not charge private provenance against the functional response limit', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'parent-user', - workspaceId: 'workspace-456', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'parent-user', + workspaceId: 'workspace-456', + }, + EMPTY_NON_SECRET_NAMES + ) const functionalValue = 'f'.repeat(9 * 1024 * 1024) const encryptedValue = 'e'.repeat(2 * 1024 * 1024) encryptionMockFns.mockDecryptSecret.mockResolvedValueOnce({ decrypted: 'secret-value' }) @@ -1693,7 +1741,7 @@ describe('executeTool Function', () => { }) it('strips private metadata even when an internal endpoint returns the wrong marker', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -1744,7 +1792,7 @@ describe('executeTool Function', () => { }) it('fails closed when a marked private response envelope is malformed', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response('{"__resolvedSecretNames":["API_KEY"],"value":"secret-value"', { @@ -1775,9 +1823,11 @@ describe('executeTool Function', () => { it('projects a thrown error with complete provenance before committing it', async () => { const secret = 'transaction-throw-secret' - const registry = new ResolvedSecretTraceRegistry([ - { name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-value' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-value' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -1813,7 +1863,7 @@ describe('executeTool Function', () => { }) it('contains an incomplete thrown settlement and leaves later calls available', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -1868,7 +1918,7 @@ describe('executeTool Function', () => { }) it('does not start a private-provenance call from a permanently incomplete parent', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() const fetchMock = vi.mocked(global.fetch) @@ -1887,7 +1937,7 @@ describe('executeTool Function', () => { }) it('does not start a private-provenance call when its input cannot be bounded', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const incompleteToolRegistry = registry.forkForToolCall() incompleteToolRegistry.markIncomplete() vi.spyOn(registry, 'forkForToolInputValues').mockReturnValue(incompleteToolRegistry) @@ -2090,18 +2140,22 @@ describe('Automatic Internal Route Detection', () => { }) it('transports only active provenance selected for an internal model input', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'PROMPT_TOKEN', - plaintext: 'prompt-secret', - encryptedValue: 'encrypted-prompt-secret', - }, - { - name: 'UNUSED_TOKEN', - plaintext: 'unused-secret', - encryptedValue: 'encrypted-unused-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'PROMPT_TOKEN', + plaintext: 'prompt-secret', + encryptedValue: 'encrypted-prompt-secret', + }, + { + name: 'UNUSED_TOKEN', + plaintext: 'unused-secret', + encryptedValue: 'encrypted-unused-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('PROMPT_TOKEN', 'prompt-secret') const mockTool = { id: 'test_internal_model_tool', @@ -2202,7 +2256,13 @@ describe('Automatic Internal Route Detection', () => { const result = await executeTool( 'test_internal_optional_model_tool', {}, - { resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry() } + { + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ), + } ) expect(result.success).toBe(true) @@ -2212,23 +2272,27 @@ describe('Automatic Internal Route Detection', () => { }) it('projects only selected values without treating declared param keys as secret data', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'MODEL_SECRET', - plaintext: 'prompt', - encryptedValue: 'encrypted-model-secret', - }, - { - name: 'UNRELATED_SECRET', - plaintext: 'unrelated-secret', - encryptedValue: 'encrypted-unrelated-secret', - }, - { - name: 'SYNTHETIC_INDEX_COLLISION', - plaintext: '0', - encryptedValue: 'encrypted-synthetic-index-collision', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'MODEL_SECRET', + plaintext: 'prompt', + encryptedValue: 'encrypted-model-secret', + }, + { + name: 'UNRELATED_SECRET', + plaintext: 'unrelated-secret', + encryptedValue: 'encrypted-unrelated-secret', + }, + { + name: 'SYNTHETIC_INDEX_COLLISION', + plaintext: '0', + encryptedValue: 'encrypted-synthetic-index-collision', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('MODEL_SECRET', 'prompt') registry.recordResolved('UNRELATED_SECRET', 'unrelated-secret') registry.recordResolved('SYNTHETIC_INDEX_COLLISION', '0') @@ -2295,18 +2359,22 @@ describe('Automatic Internal Route Detection', () => { }) it('projects text while transporting opaque model-input provenance out of band', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'PROMPT_SECRET', - plaintext: 'prompt-secret', - encryptedValue: 'encrypted-prompt-secret', - }, - { - name: 'FILE_SECRET', - plaintext: 'file-secret', - encryptedValue: 'encrypted-file-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'PROMPT_SECRET', + plaintext: 'prompt-secret', + encryptedValue: 'encrypted-prompt-secret', + }, + { + name: 'FILE_SECRET', + plaintext: 'file-secret', + encryptedValue: 'encrypted-file-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('PROMPT_SECRET', 'prompt-secret') registry.recordResolved('FILE_SECRET', 'file-secret') const mockTool = { @@ -2379,13 +2447,17 @@ describe('Automatic Internal Route Detection', () => { }) it('projects only selected nested model fields and preserves sibling values', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'NESTED_SECRET', - plaintext: 'nested-secret', - encryptedValue: 'encrypted-nested-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'NESTED_SECRET', + plaintext: 'nested-secret', + encryptedValue: 'encrypted-nested-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('NESTED_SECRET', 'nested-secret') const mockTool = { id: 'test_nested_projected_model_tool', @@ -2467,13 +2539,17 @@ describe('Automatic Internal Route Detection', () => { }) it('fails closed when a nested projection adapter does not reapply projected values', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'NESTED_SECRET', - plaintext: 'nested-secret', - encryptedValue: 'encrypted-nested-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'NESTED_SECRET', + plaintext: 'nested-secret', + encryptedValue: 'encrypted-nested-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('NESTED_SECRET', 'nested-secret') const body = vi.fn() const mockTool = { @@ -2521,13 +2597,17 @@ describe('Automatic Internal Route Detection', () => { }) it('projects selected params before formatting an external JSON request', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'PROMPT_SECRET', - plaintext: 'external-secret', - encryptedValue: 'encrypted-external-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'PROMPT_SECRET', + plaintext: 'external-secret', + encryptedValue: 'encrypted-external-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('PROMPT_SECRET', 'external-secret') const mockTool = { id: 'test_external_projected_model_tool', @@ -2569,9 +2649,11 @@ describe('Automatic Internal Route Detection', () => { it('rejects secret-derived opaque input before request formatting or network I/O', async () => { const secret = 'quote" slash\\ newline\n123 true' - const registry = new ResolvedSecretTraceRegistry([ - { name: 'OPAQUE_URL', plaintext: secret, encryptedValue: 'encrypted-opaque-secret' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'OPAQUE_URL', plaintext: secret, encryptedValue: 'encrypted-opaque-secret' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('OPAQUE_URL', secret) const url = vi.fn(() => 'https://api.example.com/opaque') const headers = vi.fn(() => ({ 'Content-Type': 'application/json' })) @@ -2617,7 +2699,7 @@ describe('Automatic Internal Route Detection', () => { }) it('rejects opaque input with incomplete provenance before direct execution', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() const directExecution = vi.fn().mockResolvedValue({ success: true, output: {} }) const mockTool = { @@ -2691,7 +2773,7 @@ describe('Automatic Internal Route Detection', () => { }) it('skips an inactive opaque boundary even when unrelated provenance is incomplete', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) registry.markIncomplete() const directExecution = vi.fn().mockResolvedValue({ success: true, output: {} }) const mockTool = { @@ -2728,9 +2810,11 @@ describe('Automatic Internal Route Detection', () => { }) it('preserves safe opaque bytes and sends no provenance metadata externally', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { name: 'LOW_ENTROPY', plaintext: 'true', encryptedValue: 'encrypted-low-entropy' }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [{ name: 'LOW_ENTROPY', plaintext: 'true', encryptedValue: 'encrypted-low-entropy' }], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('LOW_ENTROPY', 'true') const opaquePayload = 'quote" slash\\ newline\n123' const mockTool = { @@ -2783,13 +2867,17 @@ describe('Automatic Internal Route Detection', () => { }) it('projects only selected model input before direct execution', async () => { - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'PROMPT_SECRET', - plaintext: 'direct-secret', - encryptedValue: 'encrypted-direct-secret', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'PROMPT_SECRET', + plaintext: 'direct-secret', + encryptedValue: 'encrypted-direct-secret', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) registry.recordResolved('PROMPT_SECRET', 'direct-secret') const directExecution = vi.fn().mockResolvedValue({ success: true, output: { ok: true } }) const postProcess = vi.fn( @@ -2888,7 +2976,7 @@ describe('Automatic Internal Route Detection', () => { }) it('blocks the request with a stable error when selected model input cannot be projected', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) const cyclicPrompt: Record = {} cyclicPrompt.self = cyclicPrompt const body = vi.fn((params: { prompt: unknown }) => ({ prompt: params.prompt })) @@ -2957,7 +3045,13 @@ describe('Automatic Internal Route Detection', () => { const result = await executeTool( 'test_external_private_model_tool', { prompt: 'plaintext' }, - { resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry() } + { + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry( + [], + undefined, + EMPTY_NON_SECRET_NAMES + ), + } ) expect(result).toMatchObject({ @@ -3659,13 +3753,17 @@ describe('Copilot Env Variable Reference Resolution', () => { it('does not let a pending user-only reference affect an unrelated result', async () => { const secret = 'sntrys_real_token' - const registry = new ResolvedSecretTraceRegistry([ - { - name: 'SENTRY_AUTH_TOKEN', - plaintext: secret, - encryptedValue: 'encrypted-token', - }, - ]) + const registry = new ResolvedSecretTraceRegistry( + [ + { + name: 'SENTRY_AUTH_TOKEN', + plaintext: secret, + encryptedValue: 'encrypted-token', + }, + ], + undefined, + EMPTY_NON_SECRET_NAMES + ) let resolveEnvironment!: (variables: Record) => void let markResolutionStarted!: () => void const resolutionStarted = new Promise((resolve) => { @@ -4114,10 +4212,14 @@ describe('MCP Tool Execution', () => { }) it('consumes marker-gated MCP provenance while leaving the tool result unchanged', async () => { - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'test-user', - workspaceId: 'workspace-456', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'test-user', + workspaceId: 'workspace-456', + }, + EMPTY_NON_SECRET_NAMES + ) encryptionMockFns.mockDecryptSecret.mockResolvedValue({ decrypted: 'secret-value' }) global.fetch = Object.assign( vi.fn().mockResolvedValue( @@ -4168,10 +4270,14 @@ describe('MCP Tool Execution', () => { it('does not let pending MCP provenance affect an unrelated result', async () => { const secret = 'mcp-secret-value' - const registry = new ResolvedSecretTraceRegistry([], { - userId: 'test-user', - workspaceId: 'workspace-456', - }) + const registry = new ResolvedSecretTraceRegistry( + [], + { + userId: 'test-user', + workspaceId: 'workspace-456', + }, + EMPTY_NON_SECRET_NAMES + ) encryptionMockFns.mockDecryptSecret.mockResolvedValue({ decrypted: secret }) let resolveRequest!: (response: Response) => void @@ -4235,7 +4341,7 @@ describe('MCP Tool Execution', () => { }) it('rejects unmarked MCP provenance instead of trusting a response body field', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -4304,7 +4410,7 @@ describe('MCP Tool Execution', () => { }) it('drops a legacy MCP response without poisoning later provenance', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -4336,7 +4442,7 @@ describe('MCP Tool Execution', () => { }) it('preserves MCP error semantics while stripping marked private provenance', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( @@ -4371,7 +4477,7 @@ describe('MCP Tool Execution', () => { }) it('accepts MCP responses above the generic tool cap while stripping private metadata', async () => { - const registry = new ResolvedSecretTraceRegistry() + const registry = new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) global.fetch = Object.assign( vi.fn().mockResolvedValue( new Response( diff --git a/apps/sim/tools/request-transport.test.ts b/apps/sim/tools/request-transport.test.ts index efb7923d66f..aeece14e7d5 100644 --- a/apps/sim/tools/request-transport.test.ts +++ b/apps/sim/tools/request-transport.test.ts @@ -4,7 +4,10 @@ import { RESOLVED_SECRET_PROVENANCE_FIELD, RESOLVED_SECRET_PROVENANCE_METADATA_V1, } from '@/lib/execution/private-tool-metadata' -import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +import { + EMPTY_NON_SECRET_NAMES, + ResolvedSecretTraceRegistry, +} from '@/executor/utils/resolved-secret-trace-registry' import { tools } from '@/tools/registry' import { prepareToolRequest } from '@/tools/request-transport' import type { ToolConfig } from '@/tools/types' @@ -45,7 +48,11 @@ describe('private-provenance tool registry invariant', () => { }, }, } - const prepared = prepareToolRequest(transportProbe, {}, new ResolvedSecretTraceRegistry()) + const prepared = prepareToolRequest( + transportProbe, + {}, + new ResolvedSecretTraceRegistry([], undefined, EMPTY_NON_SECRET_NAMES) + ) const body = JSON.parse(prepared.body ?? '{}') as Record expect(prepared.headers.get(PRIVATE_MODEL_INPUT_PROVENANCE_HEADER)).toBe( diff --git a/packages/db/migrations/0285_brainy_mauler.sql b/packages/db/migrations/0285_brainy_mauler.sql new file mode 100644 index 00000000000..cfbd8f5496d --- /dev/null +++ b/packages/db/migrations/0285_brainy_mauler.sql @@ -0,0 +1,3 @@ +CREATE TYPE "public"."credential_env_visibility" AS ENUM('secret', 'variable');--> statement-breakpoint +ALTER TABLE "credential" ADD COLUMN "env_visibility" "credential_env_visibility" DEFAULT 'secret' NOT NULL;--> statement-breakpoint +ALTER TABLE "credential" ADD CONSTRAINT "credential_env_visibility_scope_check" CHECK ((env_visibility = 'secret') OR (type = 'env_workspace')); \ No newline at end of file diff --git a/packages/db/migrations/meta/0285_snapshot.json b/packages/db/migrations/meta/0285_snapshot.json new file mode 100644 index 00000000000..53aabd5301b --- /dev/null +++ b/packages/db/migrations/meta/0285_snapshot.json @@ -0,0 +1,18807 @@ +{ + "id": "3969ca2c-b0ed-4c01-8de1-73391d19d430", + "prevId": "9943bd0b-ca7d-4903-ac02-8ef292d2a00d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "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()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "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()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_decision": { + "name": "permission_decision", + "type": "copilot_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "permission_decided_at": { + "name": "permission_decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_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": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "auto_allowed_tools": { + "name": "auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_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": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "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": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_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": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_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()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "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": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_visibility": { + "name": "env_visibility", + "type": "credential_env_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'secret'" + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "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": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + }, + "credential_env_visibility_scope_check": { + "name": "credential_env_visibility_scope_check", + "value": "(env_visibility = 'secret') OR (type = 'env_workspace')" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "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": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "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": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "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": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "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": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "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": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_secret_provenance": { + "name": "document_secret_provenance", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_secret_provenance_document_id_document_id_fk": { + "name": "document_secret_provenance_document_id_document_id_fk", + "tableFrom": "document_secret_provenance", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_secret_provenance_status_check": { + "name": "document_secret_provenance_status_check", + "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "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": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.embedding_secret_provenance": { + "name": "embedding_secret_provenance", + "schema": "", + "columns": { + "embedding_id": { + "name": "embedding_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "embedding_secret_provenance_embedding_id_embedding_id_fk": { + "name": "embedding_secret_provenance_embedding_id_embedding_id_fk", + "tableFrom": "embedding_secret_provenance", + "tableTo": "embedding", + "columnsFrom": ["embedding_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_secret_provenance_status_check": { + "name": "embedding_secret_provenance_status_check", + "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_references_workflow_id_idx": { + "name": "execution_large_value_references_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workflow_id_idx": { + "name": "execution_large_values_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder": { + "name": "folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "folder_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "folder_user_idx": { + "name": "folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_idx": { + "name": "folder_workspace_resource_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_parent_sort_idx": { + "name": "folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_deleted_at_idx": { + "name": "folder_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_deleted_partial_idx": { + "name": "folder_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"folder\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_name_active_unique": { + "name": "folder_workspace_resource_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_user_id_user_id_fk": { + "name": "folder_user_id_user_id_fk", + "tableFrom": "folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_workspace_id_workspace_id_fk": { + "name": "folder_workspace_id_workspace_id_fk", + "tableFrom": "folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_parent_id_folder_id_fk": { + "name": "folder_parent_id_folder_id_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "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": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "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": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_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": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_folder_id_idx": { + "name": "kb_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_folder_id_folder_id_fk": { + "name": "knowledge_base_folder_id_folder_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "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": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_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": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_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": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "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()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_secret_provenance": { + "name": "memory_secret_provenance", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_secret_provenance_memory_id_memory_id_fk": { + "name": "memory_secret_provenance_memory_id_memory_id_fk", + "tableFrom": "memory_secret_provenance", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_secret_provenance_status_check": { + "name": "memory_secret_provenance_status_check", + "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "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 + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "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.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "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": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_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 + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "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()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "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": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pinned_item": { + "name": "pinned_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pinned_item_user_workspace_idx": { + "name": "pinned_item_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_resource_idx": { + "name": "pinned_item_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_user_resource_unique": { + "name": "pinned_item_user_resource_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_item_user_id_user_id_fk": { + "name": "pinned_item_user_id_user_id_fk", + "tableFrom": "pinned_item", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_item_workspace_id_workspace_id_fk": { + "name": "pinned_item_workspace_id_workspace_id_fk", + "tableFrom": "pinned_item", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "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": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandbox_image": { + "name": "sandbox_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sandbox_image_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_image_id": { + "name": "provider_image_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialization_generation": { + "name": "materialization_generation", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_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": { + "sandbox_image_provider_spec_unique": { + "name": "sandbox_image_provider_spec_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_status_idx": { + "name": "sandbox_image_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_last_used_idx": { + "name": "sandbox_image_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "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": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "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": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_domain": { + "name": "sso_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "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": { + "sso_domain_organization_id_idx": { + "name": "sso_domain_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_domain_idx": { + "name": "sso_domain_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_org_domain_unique": { + "name": "sso_domain_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_verified_unique": { + "name": "sso_domain_verified_unique", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_domain_organization_id_organization_id_fk": { + "name": "sso_domain_organization_id_organization_id_fk", + "tableFrom": "sso_domain", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_domain_created_by_user_id_fk": { + "name": "sso_domain_created_by_user_id_fk", + "tableFrom": "sso_domain", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_views": { + "name": "table_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "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": { + "table_views_table_created_idx": { + "name": "table_views_table_created_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_table_default_unique": { + "name": "table_views_table_default_unique", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_views_table_id_user_table_definitions_id_fk": { + "name": "table_views_table_id_user_table_definitions_id_fk", + "tableFrom": "table_views", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_workspace_id_workspace_id_fk": { + "name": "table_views_workspace_id_workspace_id_fk", + "tableFrom": "table_views", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_created_by_user_id_fk": { + "name": "table_views_created_by_user_id_fk", + "tableFrom": "table_views", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_period_cost_idx": { + "name": "usage_log_billing_period_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "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 + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "schema_locked": { + "name": "schema_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "insert_locked": { + "name": "insert_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "update_locked": { + "name": "update_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "delete_locked": { + "name": "delete_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "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": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_folder_id_idx": { + "name": "user_table_def_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_folder_id_folder_id_fk": { + "name": "user_table_definitions_folder_id_folder_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_secret_provenance": { + "name": "user_table_row_secret_provenance", + "schema": "", + "columns": { + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": { + "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk", + "tableFrom": "user_table_row_secret_provenance", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_table_row_secret_provenance_status_check": { + "name": "user_table_row_secret_provenance_status_check", + "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "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()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "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": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_tiktok_credential_id_idx": { + "name": "webhook_tiktok_credential_id_idx", + "columns": [ + { + "expression": "((\"provider_config\")::jsonb ->> 'credentialId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"provider\" = 'tiktok' AND \"webhook\".\"is_active\" = true AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_folder_id_fk": { + "name": "workflow_folder_id_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "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_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "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": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_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_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_operation": { + "name": "workflow_deployment_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_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": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_deadline_idx": { + "name": "workflow_execution_logs_running_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "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_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_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": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_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": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "mounted_secrets": { + "name": "mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_secret_scope": { + "name": "inbox_secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "inbox_mounted_secrets": { + "name": "inbox_mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_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": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "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": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "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": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_collab_state": { + "name": "workspace_file_collab_state", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "doc_state": { + "name": "doc_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_collab_state_file_id_workspace_files_id_fk": { + "name": "workspace_file_collab_state_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_collab_state", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_secret_provenance": { + "name": "workspace_file_secret_provenance", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_secret_provenance_file_id_workspace_files_id_fk": { + "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_secret_provenance", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_secret_provenance_status_check": { + "name": "workspace_file_secret_provenance_status_check", + "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_folder_id_fk": { + "name": "workspace_files_folder_id_folder_id_fk", + "tableFrom": "workspace_files", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "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": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "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": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_sandbox": { + "name": "workspace_sandbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "sandbox_language", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dependencies": { + "name": "dependencies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cli_tools": { + "name": "cli_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "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": { + "workspace_sandbox_workspace_name_unique": { + "name": "workspace_sandbox_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_workspace_idx": { + "name": "workspace_sandbox_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_spec_hash_idx": { + "name": "workspace_sandbox_spec_hash_idx", + "columns": [ + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_sandbox_workspace_id_workspace_id_fk": { + "name": "workspace_sandbox_workspace_id_workspace_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_sandbox_created_by_user_id_fk": { + "name": "workspace_sandbox_created_by_user_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.copilot_tool_permission_decision": { + "name": "copilot_tool_permission_decision", + "schema": "public", + "values": ["allow", "allow_chat", "always_allow", "skip"] + }, + "public.credential_env_visibility": { + "name": "credential_env_visibility", + "schema": "public", + "values": ["secret", "variable"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.folder_resource_type": { + "name": "folder_resource_type", + "schema": "public", + "values": ["workflow", "file", "knowledge_base", "table"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.sandbox_image_status": { + "name": "sandbox_image_status", + "schema": "public", + "values": ["pending", "building", "ready", "failed"] + }, + "public.sandbox_language": { + "name": "sandbox_language", + "schema": "public", + "values": ["javascript", "python"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment", + "voice-output" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "workflow_mcp_server", + "custom_tool", + "skill" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "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 6b4e7b5c90a..54320e42d01 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1989,6 +1989,13 @@ "when": 1785986974579, "tag": "0284_sso_provider_domain_verified", "breakpoints": true + }, + { + "idx": 285, + "version": "7", + "when": 1786089473939, + "tag": "0285_brainy_mauler", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 9100b4ffe7b..c88713a37b7 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -3614,6 +3614,22 @@ export const credentialTypeEnum = pgEnum('credential_type', [ 'service_account', ]) +/** + * Disclosure policy for an environment-variable credential. + * + * `secret` (the default) is the existing behavior: the value is masked from + * non-admins, redacted out of traces by the resolved-secret projection, and + * exposed to the agent by name only. `variable` marks a value as non-sensitive + * — readable by every workspace member, retained verbatim in traces and logs, + * and surfaced to the agent with its value. + * + * This is a read-side policy only; write authorization is identical for both. + */ +export const credentialEnvVisibilityEnum = pgEnum('credential_env_visibility', [ + 'secret', + 'variable', +]) + export const credential = pgTable( 'credential', { @@ -3628,6 +3644,7 @@ export const credential = pgTable( accountId: text('account_id').references(() => account.id, { onDelete: 'cascade' }), envKey: text('env_key'), envOwnerUserId: text('env_owner_user_id').references(() => user.id, { onDelete: 'cascade' }), + envVisibility: credentialEnvVisibilityEnum('env_visibility').notNull().default('secret'), encryptedServiceAccountKey: text('encrypted_service_account_key'), createdBy: text('created_by') .notNull() @@ -3662,6 +3679,13 @@ export const credential = pgTable( 'credential_personal_env_source_check', sql`(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)` ), + // A non-secret value is only ever a workspace-scoped concept. Enforced in + // the database so no route, backfill, or future caller can mark a personal + // secret (or an OAuth/service-account row) readable by the whole workspace. + envVisibilityScopeConstraint: check( + 'credential_env_visibility_scope_check', + sql`(env_visibility = 'secret') OR (type = 'env_workspace')` + ), }) ) diff --git a/packages/testing/src/mocks/environment-utils.mock.test.ts b/packages/testing/src/mocks/environment-utils.mock.test.ts index c36272aad58..6051727f4e5 100644 --- a/packages/testing/src/mocks/environment-utils.mock.test.ts +++ b/packages/testing/src/mocks/environment-utils.mock.test.ts @@ -23,6 +23,7 @@ describe('environment-utils mock', () => { workspaceDecrypted: {}, conflicts: [], decryptionFailures: [], + workspaceVariableKeys: [], }) await expect(environmentUtilsMock.getEffectiveEnvironmentSnapshot('user-1')).resolves.toEqual({ personalEncrypted: {}, @@ -31,6 +32,7 @@ describe('environment-utils mock', () => { workspaceDecrypted: {}, conflicts: [], decryptionFailures: [], + workspaceVariableKeys: [], }) await expect(environmentUtilsMock.upsertPersonalEnvVars('user-1', {})).resolves.toEqual({ added: [], diff --git a/packages/testing/src/mocks/environment-utils.mock.ts b/packages/testing/src/mocks/environment-utils.mock.ts index 69b12c37b32..48411448ace 100644 --- a/packages/testing/src/mocks/environment-utils.mock.ts +++ b/packages/testing/src/mocks/environment-utils.mock.ts @@ -7,6 +7,7 @@ function emptyPersonalAndWorkspaceEnv(): { workspaceDecrypted: Record conflicts: string[] decryptionFailures: string[] + workspaceVariableKeys: string[] } { return { personalEncrypted: {}, @@ -15,6 +16,7 @@ function emptyPersonalAndWorkspaceEnv(): { workspaceDecrypted: {}, conflicts: [], decryptionFailures: [], + workspaceVariableKeys: [], } } From 1ae10330b522d88544a88df35fcdaa1ab9f96d19 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 02:44:07 -0700 Subject: [PATCH 02/14] feat(copilot): consume the non-secret env var contract Sim's half of the mothership change. Regenerates the contracts, populates the new snapshot kind, and lets the agent create non-secret variables. nonSecretEnvVars carries {name, value}, so WORKSPACE.md now renders `- NAME = value` for non-secret env vars and a bare `- NAME` for secrets, inside the one Environment Variables section rather than under a second heading that would give the model a third thing called a "variable". That needs values here, so buildWorkspaceMdData now resolves the environment snapshot alongside the credential rows. It sits behind the 2s effective-env LRU, so it usually shares the decrypt the VFS materialization already did for the same turn. Values are filtered by the same mount policy that gates name discovery: a workspace that restricts what the agent can see must not have that widened just because a key is non-secret. set_environment_variables maps `kind` onto the visibility map, defaulting to secret on anything unrecognized. It applies to new names only - upsert never flips an existing key - so the agent cannot disclose a secret in passing. Regenerating also dropped VfsSnapshotV1Job: the kind was retired on the Go side but sim's committed mirror still carried it. Nothing referenced it. There is no CI drift check between the repos, which is how it survived. Also replaces nine hardcoded `../copilot/copilot/contracts/...` paths with a shared resolver that probes both sibling layouts and honors MOTHERSHIP_REPO. The scripts assumed the Go repo is cloned as `copilot`; a clone named `mothership` failed the whole mship:generate pipeline with a missing-file error that named neither the assumption nor the fix. --- .../copilot/chat/workspace-context.test.ts | 26 ++++++ .../sim/lib/copilot/chat/workspace-context.ts | 81 +++++++++++++++---- .../lib/copilot/generated/tool-catalog-v1.ts | 7 ++ .../lib/copilot/generated/tool-schemas-v1.ts | 7 ++ .../lib/copilot/generated/vfs-snapshot-v1.ts | 23 +++--- .../user/set-environment-variables.test.ts | 13 ++- .../server/user/set-environment-variables.ts | 19 ++++- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 11 ++- scripts/mothership-contracts-path.ts | 51 ++++++++++++ scripts/sync-billing-protocol-contract.ts | 8 +- scripts/sync-metrics-contract.ts | 5 +- scripts/sync-mothership-stream-contract.ts | 8 +- scripts/sync-tool-catalog.ts | 5 +- .../sync-trace-attribute-values-contract.ts | 9 +-- scripts/sync-trace-attributes-contract.ts | 8 +- scripts/sync-trace-events-contract.ts | 8 +- scripts/sync-trace-spans-contract.ts | 8 +- scripts/sync-vfs-snapshot-contract.ts | 8 +- 18 files changed, 228 insertions(+), 77 deletions(-) create mode 100644 scripts/mothership-contracts-path.ts diff --git a/apps/sim/lib/copilot/chat/workspace-context.test.ts b/apps/sim/lib/copilot/chat/workspace-context.test.ts index 3e090f01943..c3678ce417e 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.test.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.test.ts @@ -127,6 +127,32 @@ describe('buildWorkspaceMd - connected integrations / credentials', () => { expect(md).toContain('- STRIPE_SECRET_KEY') expect(buildVfsSnapshot(data).envVars).toEqual(['OPENAI_API_KEY', 'STRIPE_SECRET_KEY']) }) + + it('shows values for non-secret env vars and only names for secrets', () => { + const data = baseData({ + envVariables: ['OPENAI_API_KEY', 'SUPPORT_EMAIL'], + nonSecretEnvVariables: [{ name: 'SUPPORT_EMAIL', value: 'help@acme.com' }], + }) + + const md = buildWorkspaceMd(data) + expect(md).toContain('- SUPPORT_EMAIL = help@acme.com') + // Both halves in one assertion set: a secret must stay a bare name even + // when a sibling key in the same section is non-secret. + expect(md).toContain('- OPENAI_API_KEY\n') + expect(md).not.toContain('OPENAI_API_KEY =') + + // Carried as a struct kind so Go can diff a VALUE change, not just a name. + expect(buildVfsSnapshot(data).nonSecretEnvVars).toEqual([ + { name: 'SUPPORT_EMAIL', value: 'help@acme.com' }, + ]) + }) + + it('renders every name bare and omits the kind when nothing is non-secret', () => { + const data = baseData({ envVariables: ['OPENAI_API_KEY'] }) + + expect(buildWorkspaceMd(data)).not.toContain('non-secret') + expect(buildVfsSnapshot(data).nonSecretEnvVars).toEqual([]) + }) }) describe('buildWorkspaceMd - Sim sandbox entitlement projection', () => { diff --git a/apps/sim/lib/copilot/chat/workspace-context.ts b/apps/sim/lib/copilot/chat/workspace-context.ts index 419feec9683..9ea5973a95f 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.ts @@ -22,6 +22,7 @@ import { getAccessibleEnvCredentials, getAccessibleOAuthCredentials, } from '@/lib/credentials/environment' +import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils' import { listWorkspaceSandboxes } from '@/lib/execution/remote-sandbox/workspace-sandboxes' import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace' import { listCustomBlockSummariesForWorkspace } from '@/lib/workflows/custom-blocks/operations' @@ -79,13 +80,19 @@ export interface WorkspaceMdData { }> envVariables: string[] /** - * Subset of {@link envVariables} explicitly marked non-secret. Rendered as a - * marker rather than a separate section: env vars are secret or non-secret, - * not a third kind of thing, and a `## Workspace Variables` heading would - * collide with workflow variables (``) in the agent's model. - * Values live in `environment/variables.json` under `nonSecretValues`. + * Subset of {@link envVariables} explicitly marked non-secret, with values. + * + * Rendered inside the Environment Variables section rather than under its own + * heading: env vars are secret or non-secret, not a third kind of thing, and a + * separate `## Workspace Variables` heading would collide with workflow + * variables (``) in the agent's model. + * + * Optional like the other late-added collections here: an absent list reads + * as "no non-secret vars", which fails toward showing less, not more. (The + * exempt set handed to the resolved-secret registry is required for the + * opposite reason — there, omission would fail toward disclosure.) */ - nonSecretEnvVariables: string[] + nonSecretEnvVariables?: Array<{ name: string; value: string }> customTools?: Array<{ id: string; name: string }> customBlocks?: Array<{ type: string; name: string; description?: string }> mcpServers?: Array<{ id: string; name: string; url?: string | null; enabled: boolean }> @@ -111,6 +118,24 @@ function stableCompare(a: string, b: string): number { return a.localeCompare(b, 'en') } +/** + * Narrows non-secret env vars to those a mount policy already exposes. + * `filterSecretNamesByMountPolicy` takes bare names, so this maps through it and + * keeps the paired values rather than duplicating the policy logic. + */ +function filterNonSecretEnvVarsByMountPolicy( + entries: Array<{ name: string; value: string }>, + policy?: SecretMountPolicy +): Array<{ name: string; value: string }> { + const visible = new Set( + filterSecretNamesByMountPolicy( + entries.map((entry) => entry.name), + policy + ) + ) + return entries.filter((entry) => visible.has(entry.name)) +} + /** Stable order by display name, tie-broken by id, for inventory listings. */ function byNameThenId(a: { name: string; id: string }, b: { name: string; id: string }): number { return stableCompare(a.name, b.name) || stableCompare(a.id, b.id) @@ -269,12 +294,14 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { } if (data.envVariables.length > 0) { - const nonSecret = new Set(data.nonSecretEnvVariables) + const nonSecretValues = new Map( + (data.nonSecretEnvVariables ?? []).map((v) => [v.name, v.value]) + ) const lines = [...data.envVariables] .sort(stableCompare) - .map((v) => (nonSecret.has(v) ? `- ${v} (non-secret)` : `- ${v}`)) - const note = nonSecret.size - ? '\nValues for the names marked non-secret are in environment/variables.json under nonSecretValues; every other name is a secret whose value you cannot read.' + .map((v) => (nonSecretValues.has(v) ? `- ${v} = ${nonSecretValues.get(v)}` : `- ${v}`)) + const note = nonSecretValues.size + ? '\nNames shown with a value are non-secret: you may read and quote those values. Every other name is a secret — reference it as {{NAME}} and never print its value.' : '' sections.push( `## Environment Variables (${data.envVariables.length})${note}\n${lines.join('\n')}` @@ -372,6 +399,7 @@ async function buildWorkspaceMdData( mcpServerRows, skillRows, customBlockSummaries, + envSnapshot, sandboxResult, ] = await Promise.all([ getUsersWithPermissions(workspaceId), @@ -447,6 +475,11 @@ async function buildWorkspaceMdData( listCustomBlockSummariesForWorkspace(workspaceId), + // Values for non-secret env vars. Behind the 2s effective-environment LRU, + // so this usually shares the decrypt the VFS materialization already did + // for the same turn. + getPersonalAndWorkspaceEnv(userId, workspaceId, { workspaceAccess }), + hasWorkspaceSandboxAccess(workspaceId).then(async (entitled) => ({ entitled, rows: entitled ? await listWorkspaceSandboxes(workspaceId) : [], @@ -529,15 +562,22 @@ async function buildWorkspaceMdData( envVariables: [...new Set(envCredentials.map((credential) => credential.envKey))].sort( stableCompare ), - // Derived from the same credential rows, so this costs no extra query and - // no decryption — only the names are needed here. + // Names come from the credential rows (the visibility source of truth); + // values from the decrypted snapshot. A name whose value is missing is + // dropped rather than emitted empty, so a decryption failure cannot look + // like a variable that is legitimately blank. nonSecretEnvVariables: [ ...new Set( envCredentials .filter((credential) => credential.envVisibility === 'variable') .map((credential) => credential.envKey) ), - ].sort(stableCompare), + ] + .sort(stableCompare) + .flatMap((name) => { + const value = envSnapshot.workspaceDecrypted[name] + return value === undefined ? [] : [{ name, value }] + }), customTools: customTools.map((t) => ({ id: t.id, name: t.title })), customBlocks: customBlockSummaries, mcpServers: mcpServerRows, @@ -583,10 +623,10 @@ export async function generateWorkspaceContext( ...data, envVariables: filterSecretNamesByMountPolicy(data.envVariables, options?.secretMountPolicy), // Filtered by the same policy so a restricted mount list cannot be widened - // just because a key is non-secret, and so a name can never be marked - // non-secret in the inventory while being absent from the list above. - nonSecretEnvVariables: filterSecretNamesByMountPolicy( - data.nonSecretEnvVariables, + // just because a key is non-secret, and so a name can never be shown with a + // value in the inventory while being absent from the list above. + nonSecretEnvVariables: filterNonSecretEnvVarsByMountPolicy( + data.nonSecretEnvVariables ?? [], options?.secretMountPolicy ), }) @@ -664,6 +704,13 @@ export function buildVfsSnapshot(data: WorkspaceMdData): VfsSnapshotV1 { ...(c.role ? { role: c.role } : {}), })), envVars: data.envVariables, + // Carries values, unlike `envVars`. That is the point: a non-secret value + // edit changes no NAME, so a name-only kind would emit no delta and leave + // the model answering from a stale baseline. + nonSecretEnvVars: (data.nonSecretEnvVariables ?? []).map((v) => ({ + name: v.name, + value: v.value, + })), customTools: (data.customTools ?? []).map((t) => ({ id: t.id, name: t.name })), customBlocks: (data.customBlocks ?? []).map((b) => ({ type: b.type, diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index e67b7fd1761..86a92d964c1 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -4733,6 +4733,13 @@ export const SetEnvironmentVariables: ToolCatalogEntry = { parameters: { type: 'object', properties: { + kind: { + type: 'string', + description: + 'Whether these values are secret (masked from non-admins, hidden from you, redacted from logs and traces) or non-secret variables (readable by every workspace member and by you, and kept verbatim in logs). Defaults to secret. Only use variable for values that are genuinely not sensitive, such as a support email, a region, or a base URL. Workspace scope only. Applies to NEWLY created names only: this tool never changes the visibility of an env var that already exists, because turning an existing secret into a readable value is a disclosure the workspace owner must confirm in Settings.', + enum: ['secret', 'variable'], + default: 'secret', + }, scope: { type: 'string', description: diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 81188db33ab..8b2b2ada74e 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -4574,6 +4574,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { parameters: { type: 'object', properties: { + kind: { + type: 'string', + description: + 'Whether these values are secret (masked from non-admins, hidden from you, redacted from logs and traces) or non-secret variables (readable by every workspace member and by you, and kept verbatim in logs). Defaults to secret. Only use variable for values that are genuinely not sensitive, such as a support email, a region, or a base URL. Workspace scope only. Applies to NEWLY created names only: this tool never changes the visibility of an env var that already exists, because turning an existing secret into a readable value is a disclosure the workspace owner must confirm in Settings.', + enum: ['secret', 'variable'], + default: 'secret', + }, scope: { type: 'string', description: diff --git a/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts b/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts index ee8a2dc4f62..d1ae0288705 100644 --- a/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts +++ b/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts @@ -10,10 +10,10 @@ export interface VfsSnapshotV1 { envVars?: string[] files?: VfsSnapshotV1File[] integrations?: VfsSnapshotV1Integration[] - jobs?: VfsSnapshotV1Job[] knowledgeBases?: VfsSnapshotV1KnowledgeBase[] mcpServers?: VfsSnapshotV1McpServer[] members?: VfsSnapshotV1Member[] + nonSecretEnvVars?: VfsSnapshotV1NonSecretEnvVar[] sandboxes?: VfsSnapshotV1Sandbox[] skills?: VfsSnapshotV1Skill[] tables?: VfsSnapshotV1Table[] @@ -59,19 +59,6 @@ export interface VfsSnapshotV1Integration { providerId: string role?: string } -/** - * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema - * via the `definition` "VfsSnapshotV1Job". - */ -export interface VfsSnapshotV1Job { - cronExpression?: string - id: string - lifecycle?: string - prompt?: string - sourceTaskName?: string - status?: string - title?: string -} /** * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema * via the `definition` "VfsSnapshotV1KnowledgeBase". @@ -101,6 +88,14 @@ export interface VfsSnapshotV1Member { name?: string permissionType?: string } +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1NonSecretEnvVar". + */ +export interface VfsSnapshotV1NonSecretEnvVar { + name: string + value?: string +} /** * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema * via the `definition` "VfsSnapshotV1Sandbox". diff --git a/apps/sim/lib/copilot/tools/server/user/set-environment-variables.test.ts b/apps/sim/lib/copilot/tools/server/user/set-environment-variables.test.ts index e6b159a3da4..ab76ae1f2c7 100644 --- a/apps/sim/lib/copilot/tools/server/user/set-environment-variables.test.ts +++ b/apps/sim/lib/copilot/tools/server/user/set-environment-variables.test.ts @@ -51,7 +51,15 @@ describe('setEnvironmentVariablesServerTool', () => { ) expect(ensureWorkspaceAccessMock).toHaveBeenCalledWith('ws-1', 'user-1', 'write') - expect(upsertWorkspaceEnvVarsMock).toHaveBeenCalledWith('ws-1', { API_KEY: 'secret' }, 'user-1') + // The visibility map is asserted, not ignored: an unspecified `kind` must + // resolve to 'secret', so a future default flip fails here rather than + // silently publishing values. + expect(upsertWorkspaceEnvVarsMock).toHaveBeenCalledWith( + 'ws-1', + { API_KEY: 'secret' }, + 'user-1', + { visibilityByKey: { API_KEY: 'secret' } } + ) expect(upsertPersonalEnvVarsMock).not.toHaveBeenCalled() expect(result.scope).toBe('workspace') expect(result.workspaceId).toBe('ws-1') @@ -89,7 +97,8 @@ describe('setEnvironmentVariablesServerTool', () => { expect(upsertWorkspaceEnvVarsMock).toHaveBeenCalledWith( 'ws-default', { API_KEY: 'secret' }, - 'user-1' + 'user-1', + { visibilityByKey: { API_KEY: 'secret' } } ) }) }) diff --git a/apps/sim/lib/copilot/tools/server/user/set-environment-variables.ts b/apps/sim/lib/copilot/tools/server/user/set-environment-variables.ts index daa8c19fe80..df7be4d74c6 100644 --- a/apps/sim/lib/copilot/tools/server/user/set-environment-variables.ts +++ b/apps/sim/lib/copilot/tools/server/user/set-environment-variables.ts @@ -7,6 +7,7 @@ import { getDefaultWorkspaceId, } from '@/lib/copilot/tools/handlers/access' import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' +import type { EnvVisibility } from '@/lib/credentials/environment' import { upsertPersonalEnvVars, upsertWorkspaceEnvVars } from '@/lib/environment/utils' type EnvironmentVariableInputValue = string | number | boolean | null | undefined @@ -19,6 +20,12 @@ interface EnvironmentVariableInput { interface SetEnvironmentVariablesParams { variables: Record | EnvironmentVariableInput[] scope?: 'personal' | 'workspace' + /** + * Disclosure policy for the keys being set. Defaults to `secret`; `variable` + * marks them readable by every workspace member and by the agent. Workspace + * scope only — the schema forbids a non-secret personal secret. + */ + kind?: EnvVisibility workflowId?: string workspaceId?: string } @@ -98,6 +105,12 @@ export const setEnvironmentVariablesServerTool: BaseServerTool< const authenticatedUserId = context.userId const { variables } = params || ({} as SetEnvironmentVariablesParams) const scope = params.scope === 'personal' ? 'personal' : 'workspace' + // Fail closed on anything unrecognized: only the exact literal opts a key + // out of redaction. + const kind: EnvVisibility = params.kind === 'variable' ? 'variable' : 'secret' + if (params.kind === 'variable' && scope === 'personal') { + throw new Error('Only workspace environment variables can be marked non-secret') + } const normalized = normalizeVariables(variables || {}) const { variables: validatedVariables } = EnvVarSchema.parse({ variables: normalized }) @@ -112,7 +125,10 @@ export const setEnvironmentVariablesServerTool: BaseServerTool< workspaceUpdated = await upsertWorkspaceEnvVars( resolvedWorkspaceId, validatedVariables, - authenticatedUserId + authenticatedUserId, + { + visibilityByKey: Object.fromEntries(variableNames.map((name) => [name, kind])), + } ) } else { const result = await upsertPersonalEnvVars(authenticatedUserId, validatedVariables) @@ -125,6 +141,7 @@ export const setEnvironmentVariablesServerTool: BaseServerTool< logger.info('Saved environment variables', { userId: authenticatedUserId, scope, + kind, addedCount: added.length, updatedCount: updated.length, workspaceUpdatedCount: workspaceUpdated.length, diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 11c98d5f913..cfa5ccffe9f 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -2483,10 +2483,13 @@ export class WorkspaceVFS { role: c.role, })), envVariables: envKeys, - // Already narrowed to mount-policy-visible names by the filter above. - nonSecretEnvVariables: visibleEnvCredentials - .filter((credential) => credential.envVisibility === 'variable') - .map((credential) => credential.envKey), + // Reuses the same map written to environment/variables.json above, so + // the inventory and the VFS file can never disagree about which names + // are non-secret or what their values are. + nonSecretEnvVariables: Object.entries(nonSecretValues).map(([name, value]) => ({ + name, + value, + })), } } catch (err) { logger.warn('Failed to materialize environment data', { diff --git a/scripts/mothership-contracts-path.ts b/scripts/mothership-contracts-path.ts new file mode 100644 index 00000000000..1ba1d26d69e --- /dev/null +++ b/scripts/mothership-contracts-path.ts @@ -0,0 +1,51 @@ +import { existsSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) +const ROOT = resolve(SCRIPT_DIR, '..') + +/** + * Sibling checkout layouts we look for, in order. The Go service lives at + * `/copilot`, so the contracts directory is always `/copilot/contracts` + * — only the repo directory name differs between clones (`copilot` vs + * `mothership`). + */ +const CANDIDATE_REPO_DIRS = ['../copilot', '../mothership'] as const + +/** + * Resolves a generated mothership contract file. + * + * Every `sync-*-contract` script used to hardcode `../copilot/copilot/contracts/…`, + * which silently fails on a clone checked out as `mothership/` — the whole + * `bun run mship:generate` pipeline then reports a missing file with no hint + * that the path assumption, not the contract, is what's wrong. Probing both + * layouts (and honoring an explicit override) keeps a fresh clone working + * regardless of what the directory happens to be called. + * + * Precedence: `--input=` on the command line (handled by each caller) > + * `MOTHERSHIP_REPO` env var > the probed sibling layouts. + * + * @param fileName Contract file name, e.g. `vfs-snapshot-v1.schema.json`. + */ +export function resolveMothershipContract(fileName: string): string { + const override = process.env.MOTHERSHIP_REPO + if (override) { + return resolve(ROOT, override, 'copilot/contracts', fileName) + } + + const tried: string[] = [] + for (const repoDir of CANDIDATE_REPO_DIRS) { + const candidate = resolve(ROOT, repoDir, 'copilot/contracts', fileName) + if (existsSync(candidate)) return candidate + tried.push(candidate) + } + + // Returning the first candidate keeps the caller's own "file not found" error + // as the failure mode, with the tried paths named so the fix is obvious. + throw new Error( + `Could not find mothership contract "${fileName}". Tried:\n ${tried.join('\n ')}\n` + + 'Set MOTHERSHIP_REPO (relative to the sim repo root, e.g. MOTHERSHIP_REPO=../mothership) ' + + 'or pass --input=.' + ) +} diff --git a/scripts/sync-billing-protocol-contract.ts b/scripts/sync-billing-protocol-contract.ts index de681103b91..e676eb3bf3e 100644 --- a/scripts/sync-billing-protocol-contract.ts +++ b/scripts/sync-billing-protocol-contract.ts @@ -2,13 +2,11 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { formatGeneratedSource } from './format-generated-source' +import { resolveMothershipContract } from './mothership-contracts-path' const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(SCRIPT_DIR, '..') -const DEFAULT_CONTRACT_PATH = resolve( - ROOT, - '../copilot/copilot/contracts/billing-protocol-v1.schema.json' -) +const DEFAULT_CONTRACT_PATH = () => resolveMothershipContract('billing-protocol-v1.schema.json') const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/billing-protocol-v1.ts') type SchemaNode = Record @@ -259,7 +257,7 @@ async function main() { const inputArg = process.argv.find((argument) => argument.startsWith('--input=')) const inputPath = inputArg ? resolve(ROOT, inputArg.slice('--input='.length)) - : DEFAULT_CONTRACT_PATH + : DEFAULT_CONTRACT_PATH() const schema = JSON.parse(await readFile(inputPath, 'utf8')) as SchemaNode const rendered = formatGeneratedSource(render(schema), OUTPUT_PATH, ROOT) diff --git a/scripts/sync-metrics-contract.ts b/scripts/sync-metrics-contract.ts index 4a71a9f7c19..8721c475130 100644 --- a/scripts/sync-metrics-contract.ts +++ b/scripts/sync-metrics-contract.ts @@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { formatGeneratedSource } from './format-generated-source' +import { resolveMothershipContract } from './mothership-contracts-path' /** * Generate `apps/sim/lib/copilot/generated/metrics-v1.ts` from the Go-side @@ -29,7 +30,7 @@ import { formatGeneratedSource } from './format-generated-source' */ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(SCRIPT_DIR, '..') -const DEFAULT_CONTRACT_PATH = resolve(ROOT, '../copilot/copilot/contracts/metrics-v1.schema.json') +const DEFAULT_CONTRACT_PATH = () => resolveMothershipContract('metrics-v1.schema.json') const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/metrics-v1.ts') function extractMetricNames(schema: Record): string[] { @@ -115,7 +116,7 @@ async function main() { const inputArg = process.argv.find((a) => a.startsWith('--input=')) const inputPath = inputArg ? resolve(ROOT, inputArg.slice('--input='.length)) - : DEFAULT_CONTRACT_PATH + : DEFAULT_CONTRACT_PATH() const raw = await readFile(inputPath, 'utf8') const schema = JSON.parse(raw) diff --git a/scripts/sync-mothership-stream-contract.ts b/scripts/sync-mothership-stream-contract.ts index 1e9641b0c83..9f9cf77f61a 100644 --- a/scripts/sync-mothership-stream-contract.ts +++ b/scripts/sync-mothership-stream-contract.ts @@ -3,13 +3,11 @@ import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { compile } from 'json-schema-to-typescript' import { formatGeneratedSource } from './format-generated-source' +import { resolveMothershipContract } from './mothership-contracts-path' const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(SCRIPT_DIR, '..') -const DEFAULT_CONTRACT_PATH = resolve( - ROOT, - '../copilot/copilot/contracts/mothership-stream-v1.schema.json' -) +const DEFAULT_CONTRACT_PATH = () => resolveMothershipContract('mothership-stream-v1.schema.json') const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/mothership-stream-v1.ts') const RUNTIME_SCHEMA_OUTPUT_PATH = resolve( ROOT, @@ -60,7 +58,7 @@ async function main() { const inputPathArg = process.argv.find((arg) => arg.startsWith('--input=')) const inputPath = inputPathArg ? resolve(ROOT, inputPathArg.slice('--input='.length)) - : DEFAULT_CONTRACT_PATH + : DEFAULT_CONTRACT_PATH() const raw = await readFile(inputPath, 'utf8') const schema = JSON.parse(raw) diff --git a/scripts/sync-tool-catalog.ts b/scripts/sync-tool-catalog.ts index 67f7727f889..0eb086ad17f 100644 --- a/scripts/sync-tool-catalog.ts +++ b/scripts/sync-tool-catalog.ts @@ -2,10 +2,11 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { formatGeneratedSource } from './format-generated-source' +import { resolveMothershipContract } from './mothership-contracts-path' const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(SCRIPT_DIR, '..') -const DEFAULT_CATALOG_PATH = resolve(ROOT, '../copilot/copilot/contracts/tool-catalog-v1.json') +const DEFAULT_CATALOG_PATH = () => resolveMothershipContract('tool-catalog-v1.json') const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/tool-catalog-v1.ts') const RUNTIME_SCHEMA_OUTPUT_PATH = resolve( ROOT, @@ -139,7 +140,7 @@ async function main() { const inputPathArg = process.argv.find((arg) => arg.startsWith('--input=')) const inputPath = inputPathArg ? resolve(ROOT, inputPathArg.slice('--input='.length)) - : DEFAULT_CATALOG_PATH + : DEFAULT_CATALOG_PATH() const raw = await readFile(inputPath, 'utf8') const catalog = JSON.parse(raw) as { version: string; tools: Record[] } diff --git a/scripts/sync-trace-attribute-values-contract.ts b/scripts/sync-trace-attribute-values-contract.ts index 762ba194a74..78c2a1fc78e 100644 --- a/scripts/sync-trace-attribute-values-contract.ts +++ b/scripts/sync-trace-attribute-values-contract.ts @@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { formatGeneratedSource } from './format-generated-source' +import { resolveMothershipContract } from './mothership-contracts-path' /** * Generate `apps/sim/lib/copilot/generated/trace-attribute-values-v1.ts` @@ -26,10 +27,8 @@ import { formatGeneratedSource } from './format-generated-source' */ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(SCRIPT_DIR, '..') -const DEFAULT_CONTRACT_PATH = resolve( - ROOT, - '../copilot/copilot/contracts/trace-attribute-values-v1.schema.json' -) +const DEFAULT_CONTRACT_PATH = () => + resolveMothershipContract('trace-attribute-values-v1.schema.json') const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/trace-attribute-values-v1.ts') interface ExtractedEnum { @@ -115,7 +114,7 @@ async function main() { const inputArg = process.argv.find((a) => a.startsWith('--input=')) const inputPath = inputArg ? resolve(ROOT, inputArg.slice('--input='.length)) - : DEFAULT_CONTRACT_PATH + : DEFAULT_CONTRACT_PATH() const raw = await readFile(inputPath, 'utf8') const schema = JSON.parse(raw) diff --git a/scripts/sync-trace-attributes-contract.ts b/scripts/sync-trace-attributes-contract.ts index 96d8f488570..b21d0b4bb50 100644 --- a/scripts/sync-trace-attributes-contract.ts +++ b/scripts/sync-trace-attributes-contract.ts @@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { formatGeneratedSource } from './format-generated-source' +import { resolveMothershipContract } from './mothership-contracts-path' /** * Generate `apps/sim/lib/copilot/generated/trace-attributes-v1.ts` @@ -31,10 +32,7 @@ import { formatGeneratedSource } from './format-generated-source' */ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(SCRIPT_DIR, '..') -const DEFAULT_CONTRACT_PATH = resolve( - ROOT, - '../copilot/copilot/contracts/trace-attributes-v1.schema.json' -) +const DEFAULT_CONTRACT_PATH = () => resolveMothershipContract('trace-attributes-v1.schema.json') const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/trace-attributes-v1.ts') function extractAttrKeys(schema: Record): string[] { @@ -127,7 +125,7 @@ async function main() { const inputArg = process.argv.find((a) => a.startsWith('--input=')) const inputPath = inputArg ? resolve(ROOT, inputArg.slice('--input='.length)) - : DEFAULT_CONTRACT_PATH + : DEFAULT_CONTRACT_PATH() const raw = await readFile(inputPath, 'utf8') const schema = JSON.parse(raw) diff --git a/scripts/sync-trace-events-contract.ts b/scripts/sync-trace-events-contract.ts index 8253fb59258..5ab5af1c297 100644 --- a/scripts/sync-trace-events-contract.ts +++ b/scripts/sync-trace-events-contract.ts @@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { formatGeneratedSource } from './format-generated-source' +import { resolveMothershipContract } from './mothership-contracts-path' /** * Generate `apps/sim/lib/copilot/generated/trace-events-v1.ts` from @@ -16,10 +17,7 @@ import { formatGeneratedSource } from './format-generated-source' */ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(SCRIPT_DIR, '..') -const DEFAULT_CONTRACT_PATH = resolve( - ROOT, - '../copilot/copilot/contracts/trace-events-v1.schema.json' -) +const DEFAULT_CONTRACT_PATH = () => resolveMothershipContract('trace-events-v1.schema.json') const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/trace-events-v1.ts') function extractEventNames(schema: Record): string[] { @@ -96,7 +94,7 @@ async function main() { const inputArg = process.argv.find((a) => a.startsWith('--input=')) const inputPath = inputArg ? resolve(ROOT, inputArg.slice('--input='.length)) - : DEFAULT_CONTRACT_PATH + : DEFAULT_CONTRACT_PATH() const raw = await readFile(inputPath, 'utf8') const schema = JSON.parse(raw) diff --git a/scripts/sync-trace-spans-contract.ts b/scripts/sync-trace-spans-contract.ts index 374898df261..8451977b1aa 100644 --- a/scripts/sync-trace-spans-contract.ts +++ b/scripts/sync-trace-spans-contract.ts @@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { formatGeneratedSource } from './format-generated-source' +import { resolveMothershipContract } from './mothership-contracts-path' /** * Generate `apps/sim/lib/copilot/generated/trace-spans-v1.ts` from the @@ -21,10 +22,7 @@ import { formatGeneratedSource } from './format-generated-source' */ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(SCRIPT_DIR, '..') -const DEFAULT_CONTRACT_PATH = resolve( - ROOT, - '../copilot/copilot/contracts/trace-spans-v1.schema.json' -) +const DEFAULT_CONTRACT_PATH = () => resolveMothershipContract('trace-spans-v1.schema.json') const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/trace-spans-v1.ts') function extractSpanNames(schema: Record): string[] { @@ -114,7 +112,7 @@ async function main() { const inputArg = process.argv.find((a) => a.startsWith('--input=')) const inputPath = inputArg ? resolve(ROOT, inputArg.slice('--input='.length)) - : DEFAULT_CONTRACT_PATH + : DEFAULT_CONTRACT_PATH() const raw = await readFile(inputPath, 'utf8') const schema = JSON.parse(raw) diff --git a/scripts/sync-vfs-snapshot-contract.ts b/scripts/sync-vfs-snapshot-contract.ts index e0e616ebdbe..dc59ed5c896 100644 --- a/scripts/sync-vfs-snapshot-contract.ts +++ b/scripts/sync-vfs-snapshot-contract.ts @@ -3,16 +3,14 @@ import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { compile } from 'json-schema-to-typescript' import { formatGeneratedSource } from './format-generated-source' +import { resolveMothershipContract } from './mothership-contracts-path' const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(SCRIPT_DIR, '..') // Matches the sibling sync scripts' canonical layout. In a repo where the Go // service lives at `mothership/copilot`, pass `--input=` (e.g. // `--input=../mothership/copilot/contracts/vfs-snapshot-v1.schema.json`). -const DEFAULT_CONTRACT_PATH = resolve( - ROOT, - '../copilot/copilot/contracts/vfs-snapshot-v1.schema.json' -) +const DEFAULT_CONTRACT_PATH = () => resolveMothershipContract('vfs-snapshot-v1.schema.json') const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts') async function main() { @@ -20,7 +18,7 @@ async function main() { const inputPathArg = process.argv.find((arg) => arg.startsWith('--input=')) const inputPath = inputPathArg ? resolve(ROOT, inputPathArg.slice('--input='.length)) - : DEFAULT_CONTRACT_PATH + : DEFAULT_CONTRACT_PATH() const raw = await readFile(inputPath, 'utf8') const schema = JSON.parse(raw) From fb850c89cee7b3ca9bf6da6650248a9cdf3ae16d Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 02:57:18 -0700 Subject: [PATCH 03/14] test(secrets): pin that the non-secret exemption is inert and surgical Two regression guards for the redaction path. The first covers every workspace that has never marked a key non-secret: with an empty exempt set all three keys still activate, redact, and export exactly as before, so the feature is provably inert where it is unused. The second covers the workspaces that do use it: exempting one key removes exactly that key and leaves the secrets beside it redacting. --- .../resolved-secret-trace-registry.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts b/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts index c1d15025317..4f53e2379e4 100644 --- a/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts +++ b/apps/sim/executor/utils/resolved-secret-trace-registry.test.ts @@ -1046,6 +1046,54 @@ describe('ResolvedSecretTraceRegistry non-secret exemption', () => { expect(registry.getActiveMatches()).toEqual([]) }) + /** + * The no-regression guard. Every workspace that has never marked a key + * non-secret runs with an empty exempt set, so this pins that the feature is + * completely inert there: all three keys still activate, redact, and export + * exactly as before. + */ + it('is inert with an empty exempt set — every key still behaves as a secret', async () => { + const registry = await createResolvedSecretTraceRegistry({ + personalEncrypted: { PERSONAL_KEY: 'enc-p' }, + workspaceEncrypted: { STRIPE_KEY: 'enc-s', SUPPORT_EMAIL: 'enc-e' }, + personalDecrypted: { PERSONAL_KEY: 'p-secret' }, + workspaceDecrypted: { STRIPE_KEY: 'sk-live', SUPPORT_EMAIL: 'help@acme.com' }, + nonSecretNames: EMPTY_NON_SECRET_NAMES, + }) + registry.recordResolved('PERSONAL_KEY', 'p-secret') + registry.recordResolved('STRIPE_KEY', 'sk-live') + registry.recordResolved('SUPPORT_EMAIL', 'help@acme.com') + + expect(registry.isComplete()).toBe(true) + expect( + registry + .getActiveMatches() + .map((match) => match.replacement) + .sort() + ).toEqual(['{{PERSONAL_KEY}}', '{{STRIPE_KEY}}', '{{SUPPORT_EMAIL}}']) + expect(registry.exportProvenance().entries).toHaveLength(3) + }) + + /** Exempting one key must not perturb the secrets sitting beside it. */ + it('removes exactly the exempt key and leaves sibling secrets redacting', async () => { + const registry = await createResolvedSecretTraceRegistry({ + personalEncrypted: { PERSONAL_KEY: 'enc-p' }, + workspaceEncrypted: { STRIPE_KEY: 'enc-s', SUPPORT_EMAIL: 'enc-e' }, + personalDecrypted: { PERSONAL_KEY: 'p-secret' }, + workspaceDecrypted: { STRIPE_KEY: 'sk-live', SUPPORT_EMAIL: 'help@acme.com' }, + nonSecretNames: new Set(['SUPPORT_EMAIL']), + }) + registry.recordResolved('PERSONAL_KEY', 'p-secret') + registry.recordResolved('STRIPE_KEY', 'sk-live') + registry.recordResolved('SUPPORT_EMAIL', 'help@acme.com') + + expect(registry.isComplete()).toBe(true) + const matches = registry.getActiveMatches() + expect(matches.some((match) => match.plaintext === 'sk-live')).toBe(true) + expect(matches.some((match) => match.plaintext === 'p-secret')).toBe(true) + expect(matches.some((match) => match.plaintext === 'help@acme.com')).toBe(false) + }) + it('never activates or exports a non-secret name', async () => { const registry = await createResolvedSecretTraceRegistry({ personalEncrypted: {}, From bfd782535ad1a4d161edbbec7ecafe10903c20e2 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 12:32:11 -0700 Subject: [PATCH 04/14] fix(secrets): gate the non-secret bypass on workspace access Live-Postgres verification caught a real defect in the clause added for non-secret env vars. The other three clauses in getAccessibleEnvCredentials are all scoped to the user - an active credential membership, your own personal secret, workspace admin. The new one was not, so it returned a workspace's non-secret keys to ANY caller, including a user with no membership in that workspace at all. Callers do check access upstream today, so this was not reachable in practice. But the function's contract is "credentials this user may access", and a future caller trusting that name would have leaked. It now resolves workspace access itself and gates the bypass on it, rather than depending on every caller to remember. A caller without access still reaches their own personal credentials through the envOwnerUserId clause; only the bypass is withheld. getPersonalAndWorkspaceEnv passes both facts through from the access check it already performs, so the common path costs no extra query. Adds scripts/verify-env-acl.ts, which found this. The unit suites all run against a mocked @sim/db, so nothing exercised the actual SQL; this seeds a real workspace, two users, and a non-member, and asserts what the query must REFUSE. It also pins that credential_env_visibility_scope_check rejects both an INSERT and an UPDATE marking a personal secret non-secret. --- apps/sim/lib/credentials/environment.ts | 45 +++-- apps/sim/lib/environment/utils.ts | 5 +- apps/sim/scripts/verify-env-acl.ts | 249 ++++++++++++++++++++++++ 3 files changed, 287 insertions(+), 12 deletions(-) create mode 100644 apps/sim/scripts/verify-env-acl.ts diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 596a59b3173..225a25a9a7c 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -13,6 +13,7 @@ import { and, eq, inArray, isNotNull, isNull, notInArray, or, sql } from 'drizzl import { acquireUserBillingIdentityLock } from '@/lib/billing/organizations/billing-identity-lock' import type { DbOrTx } from '@/lib/db/types' import { + checkWorkspaceAccess, getEffectiveWorkspacePermission, hasWorkspaceAdminAccess, } from '@/lib/workspaces/permissions/utils' @@ -658,10 +659,28 @@ export async function syncPersonalEnvCredentialsForUser(params: { export async function getAccessibleEnvCredentials( workspaceId: string, userId: string, - options?: { isWorkspaceAdmin?: boolean } + options?: { isWorkspaceAdmin?: boolean; hasWorkspaceAccess?: boolean } ): Promise { - const isWorkspaceAdmin = - options?.isWorkspaceAdmin ?? (await hasWorkspaceAdminAccess(userId, workspaceId)) + // `hasWorkspaceAccess` gates the non-secret bypass below and must never be + // assumed. Without it the bypass hands a workspace's non-secret keys to ANY + // caller, including a user with no membership in that workspace — the other + // three clauses are all scoped to the user, and this one is not. Callers do + // check access upstream today, but this function's contract is "credentials + // this user may access", so it enforces that itself rather than trusting + // every future caller to remember. Verified against live Postgres by + // `apps/sim/scripts/verify-env-acl.ts`. + // + // Resolved from one `checkWorkspaceAccess` unless the caller supplied both + // facts; an admin trivially has access. A caller without access still sees + // their own personal credentials via the `envOwnerUserId` clause — only the + // bypass is withheld. + const resolvedAccess = + options?.isWorkspaceAdmin !== undefined && options?.hasWorkspaceAccess !== undefined + ? undefined + : await checkWorkspaceAccess(workspaceId, userId) + const isWorkspaceAdmin = options?.isWorkspaceAdmin ?? resolvedAccess?.canAdmin ?? false + const hasWorkspaceAccess = + options?.hasWorkspaceAccess ?? (isWorkspaceAdmin || (resolvedAccess?.hasAccess ?? false)) const rows = await db .select({ @@ -687,14 +706,18 @@ export async function getAccessibleEnvCredentials( or( isNotNull(credentialMember.id), eq(credential.envOwnerUserId, userId), - // Non-secret workspace values are readable by every member, so they - // bypass the per-key credential ACL. Workspace membership itself is - // still enforced upstream by `checkWorkspaceAccess`; this clause only - // relaxes the per-key gate. The `env_workspace` predicate is load - // bearing — the schema check constraint keeps `env_personal` rows from - // ever being marked `variable`, and this keeps the query honest even - // if that constraint were ever relaxed. - and(eq(credential.type, 'env_workspace'), eq(credential.envVisibility, 'variable')), + // Non-secret workspace values are readable by every MEMBER, so they + // bypass the per-key credential ACL — but only for a caller who + // actually has workspace access. Unlike the clauses above it, this + // one is not scoped to the user, so dropping the membership gate + // would return these keys to anyone who names the workspace. + // + // The `env_workspace` predicate is redundant with the schema's + // `credential_env_visibility_scope_check` and kept anyway, so the + // query stays correct on its own if that constraint is ever relaxed. + hasWorkspaceAccess + ? and(eq(credential.type, 'env_workspace'), eq(credential.envVisibility, 'variable')) + : undefined, isWorkspaceAdmin ? eq(credential.type, 'env_workspace') : undefined ) ) diff --git a/apps/sim/lib/environment/utils.ts b/apps/sim/lib/environment/utils.ts index 67adabe9a85..4b31bbb618f 100644 --- a/apps/sim/lib/environment/utils.ts +++ b/apps/sim/lib/environment/utils.ts @@ -172,7 +172,10 @@ export async function getPersonalAndWorkspaceEnv( .limit(1) : Promise.resolve([] as any[]), workspaceId - ? getAccessibleEnvCredentials(workspaceId, userId, { isWorkspaceAdmin: workspaceCanAdmin }) + ? getAccessibleEnvCredentials(workspaceId, userId, { + isWorkspaceAdmin: workspaceCanAdmin, + hasWorkspaceAccess: true, + }) : Promise.resolve([]), ]) diff --git a/apps/sim/scripts/verify-env-acl.ts b/apps/sim/scripts/verify-env-acl.ts new file mode 100644 index 00000000000..cda8cf69c28 --- /dev/null +++ b/apps/sim/scripts/verify-env-acl.ts @@ -0,0 +1,249 @@ +/** + * Live-Postgres verification for the non-secret env-var ACL bypass. + * + * `getAccessibleEnvCredentials` gains a clause letting `env_workspace` rows + * marked `variable` skip the per-key credential ACL. That clause is the single + * line separating "workspace member" from "can read this value", and the unit + * suites all run against a mocked `@sim/db`, so nothing else exercises the + * actual SQL. This seeds a real workspace and asserts what the query must + * REFUSE, not just what it allows. + * + * Run against a throwaway database: + * DATABASE_URL=postgresql://postgres:postgres@localhost:5432/simstudio_acl_test \ + * bun run apps/sim/scripts/verify-env-acl.ts + */ +import { db } from '@sim/db' +import { credential, credentialMember, permissions, user, workspace } from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { eq, sql } from 'drizzle-orm' +import { getAccessibleEnvCredentials } from '@/lib/credentials/environment' + +const ids = { + owner: generateId(), + member: generateId(), + outsider: generateId(), + wsA: generateId(), + wsB: generateId(), +} + +let failures = 0 + +function check(label: string, actual: unknown, expected: unknown) { + const a = JSON.stringify(actual) + const e = JSON.stringify(expected) + if (a === e) { + console.log(` PASS ${label}`) + return + } + failures++ + console.log(` FAIL ${label}\n expected ${e}\n actual ${a}`) +} + +async function seedUser(id: string, email: string) { + await db.insert(user).values({ + id, + name: email, + email, + emailVerified: true, + createdAt: new Date(), + updatedAt: new Date(), + }) +} + +async function seedWorkspace(id: string, name: string, ownerId: string) { + await db.insert(workspace).values({ + id, + name, + ownerId, + billedAccountUserId: ownerId, + createdAt: new Date(), + updatedAt: new Date(), + }) +} + +async function grant( + userId: string, + workspaceId: string, + permissionType: 'read' | 'write' | 'admin' +) { + await db.insert(permissions).values({ + id: generateId(), + userId, + entityType: 'workspace', + entityId: workspaceId, + permissionType, + createdAt: new Date(), + updatedAt: new Date(), + }) +} + +async function seedEnvCredential(opts: { + workspaceId: string + envKey: string + type: 'env_workspace' | 'env_personal' + visibility: 'secret' | 'variable' + createdBy: string + envOwnerUserId?: string +}) { + const id = generateId() + await db.insert(credential).values({ + id, + workspaceId: opts.workspaceId, + type: opts.type, + displayName: opts.envKey, + envKey: opts.envKey, + envVisibility: opts.visibility, + ...(opts.envOwnerUserId ? { envOwnerUserId: opts.envOwnerUserId } : {}), + createdBy: opts.createdBy, + createdAt: new Date(), + updatedAt: new Date(), + }) + return id +} + +async function keysFor(workspaceId: string, userId: string, isWorkspaceAdmin: boolean) { + const rows = await getAccessibleEnvCredentials(workspaceId, userId, { isWorkspaceAdmin }) + return rows.map((r) => r.envKey).sort() +} + +async function main() { + console.log('Seeding...') + await seedUser(ids.owner, 'owner@test.local') + await seedUser(ids.member, 'member@test.local') + await seedUser(ids.outsider, 'outsider@test.local') + await seedWorkspace(ids.wsA, 'Workspace A', ids.owner) + await seedWorkspace(ids.wsB, 'Workspace B', ids.owner) + await grant(ids.owner, ids.wsA, 'admin') + await grant(ids.member, ids.wsA, 'read') + await grant(ids.outsider, ids.wsB, 'admin') + + // Workspace A: a secret nobody is a member of, and a non-secret variable. + await seedEnvCredential({ + workspaceId: ids.wsA, + envKey: 'STRIPE_KEY', + type: 'env_workspace', + visibility: 'secret', + createdBy: ids.owner, + }) + await seedEnvCredential({ + workspaceId: ids.wsA, + envKey: 'SUPPORT_EMAIL', + type: 'env_workspace', + visibility: 'variable', + createdBy: ids.owner, + }) + // A personal secret owned by the OWNER, shared into workspace A. + await seedEnvCredential({ + workspaceId: ids.wsA, + envKey: 'OWNER_PERSONAL', + type: 'env_personal', + visibility: 'secret', + createdBy: ids.owner, + envOwnerUserId: ids.owner, + }) + // Workspace B has a same-named variable, to prove workspace isolation holds. + await seedEnvCredential({ + workspaceId: ids.wsB, + envKey: 'SUPPORT_EMAIL', + type: 'env_workspace', + visibility: 'variable', + createdBy: ids.outsider, + }) + + console.log('\nWhat the bypass must ALLOW:') + check( + 'read-only member with no credential membership sees the variable', + await keysFor(ids.wsA, ids.member, false), + ['SUPPORT_EMAIL'] + ) + + console.log('\nWhat the bypass must REFUSE:') + check( + 'that same member does NOT see the workspace secret (bypass did not widen secrets)', + (await keysFor(ids.wsA, ids.member, false)).includes('STRIPE_KEY'), + false + ) + check( + "member does NOT see another user's personal secret", + (await keysFor(ids.wsA, ids.member, false)).includes('OWNER_PERSONAL'), + false + ) + check( + "workspace B's variable does not leak into workspace A", + await keysFor(ids.wsA, ids.outsider, false), + [] + ) + check( + 'a non-member of workspace A sees nothing there, variable included', + await keysFor(ids.wsA, ids.outsider, false), + [] + ) + + console.log('\nAdmin path unchanged:') + check( + 'workspace admin still sees every workspace key plus their own personal', + await keysFor(ids.wsA, ids.owner, true), + ['OWNER_PERSONAL', 'STRIPE_KEY', 'SUPPORT_EMAIL'] + ) + + console.log('\nCredential membership still grants a secret:') + const [secretRow] = await db + .select({ id: credential.id }) + .from(credential) + .where(eq(credential.envKey, 'STRIPE_KEY')) + .limit(1) + await db.insert(credentialMember).values({ + id: generateId(), + credentialId: secretRow.id, + userId: ids.member, + role: 'member', + status: 'active', + joinedAt: new Date(), + createdAt: new Date(), + updatedAt: new Date(), + }) + check( + 'member with an active membership now sees the secret too', + await keysFor(ids.wsA, ids.member, false), + ['STRIPE_KEY', 'SUPPORT_EMAIL'] + ) + + console.log('\nDB constraint — a personal secret can never be non-secret:') + let constraintHeld = false + let constraintName = '' + try { + await db.execute(sql` + INSERT INTO credential (id, workspace_id, type, display_name, env_key, env_owner_user_id, env_visibility, created_by, created_at, updated_at) + VALUES (${generateId()}, ${ids.wsA}, 'env_personal', 'SNEAKY', 'SNEAKY', ${ids.member}, 'variable', ${ids.owner}, now(), now()) + `) + } catch (error) { + constraintHeld = true + // Drizzle wraps the driver error, so the constraint name is on `cause`. + const e = error as { cause?: { constraint_name?: string } } + constraintName = String(e.cause?.constraint_name ?? '') + } + check('insert rejected', constraintHeld, true) + check( + 'rejected by the intended constraint', + constraintName, + 'credential_env_visibility_scope_check' + ) + + let updateHeld = false + try { + await db.execute(sql` + UPDATE credential SET env_visibility = 'variable' WHERE env_key = 'OWNER_PERSONAL' + `) + } catch { + updateHeld = true + } + check('UPDATE to variable on an existing personal secret also rejected', updateHeld, true) + + console.log(failures === 0 ? '\nALL CHECKS PASSED' : `\n${failures} CHECK(S) FAILED`) + process.exit(failures === 0 ? 0 : 1) +} + +main().catch((error) => { + console.error(error) + process.exit(1) +}) From b20f8aace1c6b8bc56fb8d129109717e6d6f02aa Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 13:20:57 -0700 Subject: [PATCH 05/14] feat(secrets): split the secrets page into secrets and variables sections Browser testing showed the first cut had the wrong shape. Creating a variable meant creating a secret and then converting it, which is backwards for the common case, and the per-row "Non-secret" tag sat in the trailing auto-sized grid track, widening the column and knocking every other row's ... trigger out of alignment. Now there are two workspace sections, each with its own draft row, so either kind can be created directly. The heading carries the meaning, so the tag is gone along with the alignment problem it caused. Personal becomes "Personal secrets" - personal values cannot be non-secret, the schema check constraint sees to that, so there is no personal counterpart. Almost none of this reached the write path: the upsert mutation already accepted a visibility map and the server already applied it to new keys. Save just builds that map from whichever rows were drafted in the variables section. Two things the sectioning made necessary. A name drafted in one section that already exists in the other now blocks the save: visibility only applies to NEW keys, so it would otherwise have saved and silently kept its old policy, leaving a row whose section lied about its kind. And both draft arrays reset after a save, not just the original one - a saved key that stays drafted renders twice, which is exactly what happened. Converting moves a row between sections on its own, since the sections read the refetched visibility map. --- .../secrets-manager/secrets-manager.tsx | 271 +++++++++++++----- 1 file changed, 195 insertions(+), 76 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx index 2155f735a42..ca336f79793 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx @@ -1,7 +1,7 @@ 'use client' import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' -import { ChipConfirmModal, ChipInput, ChipTag, cn, toast } from '@sim/emcn' +import { ChipConfirmModal, ChipInput, cn, toast } from '@sim/emcn' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' import { useParams, useRouter } from 'next/navigation' @@ -278,20 +278,22 @@ function WorkspaceVariableRow({ visibility={visibility} name={`workspace_env_value_${envKey}_${autofillSalt}`} /> -
- {visibility === 'variable' && Non-secret} - copyName(envKey)} - onViewDetails={hasCredential && onViewDetails ? () => onViewDetails(envKey) : undefined} - onDelete={canEdit ? () => onDelete(envKey) : undefined} - visibility={visibility} - onToggleVisibility={ - canEdit && onToggleVisibility - ? () => onToggleVisibility(envKey, visibility === 'variable' ? 'secret' : 'variable') - : undefined - } - /> -
+ {/* + No per-row kind tag: the section heading already says which kind this is, + and a tag in the trailing `auto` track widened the column and knocked + every other row's `...` out of alignment. + */} + copyName(envKey)} + onViewDetails={hasCredential && onViewDetails ? () => onViewDetails(envKey) : undefined} + onDelete={canEdit ? () => onDelete(envKey) : undefined} + visibility={visibility} + onToggleVisibility={ + canEdit && onToggleVisibility + ? () => onToggleVisibility(envKey, visibility === 'variable' ? 'secret' : 'variable') + : undefined + } + /> ) } @@ -411,9 +413,15 @@ export function SecretsManager() { const isLoading = isPersonalLoading || isWorkspaceLoading const [envVars, setEnvVars] = useState([]) + // One draft array per section. A single array carrying a `kind` looked + // tempting, but `updateEnvVarArray`'s auto-add/auto-remove keys off "is this + // the LAST row", which stops meaning anything once two kinds are interleaved. const [newWorkspaceRows, setNewWorkspaceRows] = useState([ createEmptyEnvVar(), ]) + const [newWorkspaceVariableRows, setNewWorkspaceVariableRows] = useState( + [createEmptyEnvVar()] + ) const [searchTerm, setSearchTerm] = useSettingsSearch() const [showUnsavedChanges, setShowUnsavedChanges] = useState(false) const [workspaceVars, setWorkspaceVars] = useState>({}) @@ -477,6 +485,13 @@ export function SecretsManager() { return entries.filter(([key]) => key.toLowerCase().includes(term)) }, [workspaceVars, searchTerm]) + const filteredNewWorkspaceVariableRows = useMemo(() => { + const mapped = newWorkspaceVariableRows.map((row, index) => ({ row, originalIndex: index })) + if (!searchTerm.trim()) return mapped + const term = searchTerm.toLowerCase() + return mapped.filter(({ row }) => row.key.toLowerCase().includes(term)) + }, [newWorkspaceVariableRows, searchTerm]) + const filteredNewWorkspaceRows = useMemo(() => { const mapped = newWorkspaceRows.map((row, index) => ({ row, originalIndex: index })) if (!searchTerm.trim()) return mapped @@ -519,19 +534,44 @@ export function SecretsManager() { } if (newWorkspaceRows.some((row) => row.key && row.value)) return true + if (newWorkspaceVariableRows.some((row) => row.key && row.value)) return true return false - }, [envVars, workspaceVars, newWorkspaceRows]) + }, [envVars, workspaceVars, newWorkspaceRows, newWorkspaceVariableRows]) const hasConflicts = useMemo(() => { return envVars.some((envVar) => !!envVar.key && allWorkspaceKeys.has(envVar.key)) }, [envVars, allWorkspaceKeys]) + /** + * Names drafted in one workspace section that already exist in the other, or + * in both drafts at once. A new key only takes its section's visibility on + * create; saving a name that already exists would quietly keep the old + * policy, so this blocks the save instead of producing a row whose section + * lies about its kind. + */ + const duplicateDraftKeys = useMemo(() => { + const savedVisibility = workspaceEnvData?.visibility ?? {} + const clashes = new Set() + const secretDraftKeys = new Set(newWorkspaceRows.filter((r) => r.key).map((r) => r.key)) + for (const row of newWorkspaceVariableRows) { + if (!row.key) continue + if (secretDraftKeys.has(row.key)) clashes.add(row.key) + if (savedVisibility[row.key] === 'secret') clashes.add(row.key) + } + for (const row of newWorkspaceRows) { + if (row.key && savedVisibility[row.key] === 'variable') clashes.add(row.key) + } + return clashes + }, [newWorkspaceRows, newWorkspaceVariableRows, workspaceEnvData?.visibility]) + const hasInvalidKeys = useMemo(() => { const personalInvalid = envVars.some((envVar) => !!envVar.key && validateEnvVarKey(envVar.key)) - const workspaceInvalid = newWorkspaceRows.some((row) => !!row.key && validateEnvVarKey(row.key)) + const workspaceInvalid = [...newWorkspaceRows, ...newWorkspaceVariableRows].some( + (row) => !!row.key && validateEnvVarKey(row.key) + ) return personalInvalid || workspaceInvalid - }, [envVars, newWorkspaceRows]) + }, [envVars, newWorkspaceRows, newWorkspaceVariableRows]) const isListSaving = savePersonalMutation.isPending || @@ -722,6 +762,10 @@ export function SecretsManager() { setNewWorkspaceRows((prev) => updateEnvVarArray(prev, index, field, value)) } + const updateNewWorkspaceVariableRow = (index: number, field: 'key' | 'value', value: string) => { + setNewWorkspaceVariableRows((prev) => updateEnvVarArray(prev, index, field, value)) + } + const updateEnvVar = (index: number, field: 'key' | 'value', value: string) => { setEnvVars((prev) => updateEnvVarArray(prev, index, field, value)) } @@ -818,11 +862,94 @@ export function SecretsManager() { setEnvVars(structuredClone(initialVarsRef.current)) setWorkspaceVars({ ...initialWorkspaceVarsRef.current }) setNewWorkspaceRows([createEmptyEnvVar()]) + setNewWorkspaceVariableRows([createEmptyEnvVar()]) setShowUnsavedChanges(false) } const handleCancel = resetToSaved + /** + * Saved workspace rows in render order, paired with their disclosure policy so + * each section can take its own slice. Derived once rather than filtered twice + * so the two sections cannot disagree about which kind a key is. + */ + const workspaceEntriesForRender = useMemo(() => { + const entries = searchTerm.trim() ? filteredWorkspaceEntries : Object.entries(workspaceVars) + const visibilityMap = workspaceEnvData?.visibility ?? {} + return entries.map(([key, value]) => ({ + key, + value, + visibility: visibilityMap[key] ?? ('secret' as EnvVisibility), + })) + }, [searchTerm, filteredWorkspaceEntries, workspaceVars, workspaceEnvData?.visibility]) + + /** + * Renders one workspace section. Both sections share the row component and + * grid; only the slice of saved rows, the draft array, and the label differ. + * A section always renders when the search box is empty, so its draft row is + * there to create that kind directly. + */ + const renderWorkspaceSection = ({ + label, + visibility, + entries, + draftRows, + onUpdateDraft, + }: { + label: string + visibility: EnvVisibility + entries: Array<{ key: string; value: string; visibility: EnvVisibility }> + draftRows: Array<{ row: UIEnvironmentVariable; originalIndex: number }> + onUpdateDraft: (index: number, field: 'key' | 'value', value: string) => void + }) => { + const rows = entries.filter((entry) => entry.visibility === visibility) + const searching = Boolean(searchTerm.trim()) + if (searching && rows.length === 0 && draftRows.length === 0) return null + + return ( + +
+ {rows.map(({ key, value }) => { + const cred = workspaceEnvKeyToCredential.get(key) + const canEditRow = canCreateWorkspaceSecret && cred?.role === 'admin' + return ( + setPendingVisibilityChange({ envKey, next }) : undefined + } + /> + ) + })} + {canCreateWorkspaceSecret && + draftRows.map(({ row, originalIndex }) => ( + + ))} +
+
+ ) + } + const handleSave = async () => { if (isListSaving) return @@ -836,6 +963,18 @@ export function SecretsManager() { mergedWorkspaceVars[row.key] = row.value } } + // Names drafted in the Variables section. Only NEW keys can take a + // visibility here — the server never flips an existing key through this + // path (that needs the confirmed disclosure flow), which is why + // `duplicateDraftKeys` blocks saving a name that already exists as a secret + // rather than letting it save and silently stay secret. + const draftVariableVisibility: Record = {} + for (const row of newWorkspaceVariableRows) { + if (row.key && row.value) { + mergedWorkspaceVars[row.key] = row.value + draftVariableVisibility[row.key] = 'variable' + } + } const validVariables = envVars .filter((v) => v.key && v.value) @@ -879,7 +1018,16 @@ export function SecretsManager() { mutations.push( (async () => { if (Object.keys(toUpsert).length) { - await upsertWorkspaceMutation.mutateAsync({ workspaceId, variables: toUpsert }) + const visibility = Object.fromEntries( + Object.keys(toUpsert) + .filter((key) => draftVariableVisibility[key]) + .map((key) => [key, draftVariableVisibility[key]]) + ) + await upsertWorkspaceMutation.mutateAsync({ + workspaceId, + variables: toUpsert, + ...(Object.keys(visibility).length ? { visibility } : {}), + }) } if (toDelete.length) { await removeWorkspaceMutation.mutateAsync({ workspaceId, keys: toDelete }) @@ -900,7 +1048,11 @@ export function SecretsManager() { initialVarsRef.current = structuredClone(envVars.filter((v) => v.key && v.value)) setWorkspaceVars(mergedWorkspaceVars) + // Both draft arrays clear: the saved rows now render from workspaceVars, + // so leaving either behind shows the same key twice — once saved, once + // still drafted. setNewWorkspaceRows([createEmptyEnvVar()]) + setNewWorkspaceVariableRows([createEmptyEnvVar()]) if (mutations.length > 0) { toast.success('Secrets saved') } @@ -1038,73 +1190,40 @@ export function SecretsManager() { saving: isListSaving, onSave: handleSave, onDiscard: handleCancel, - saveDisabled: hasConflicts || hasInvalidKeys || isLoading, + saveDisabled: hasConflicts || hasInvalidKeys || duplicateDraftKeys.size > 0 || isLoading, saveTooltip: hasConflicts ? 'Resolve all conflicts before saving' : hasInvalidKeys ? 'Fix invalid variable names before saving' - : undefined, + : duplicateDraftKeys.size > 0 + ? `${[...duplicateDraftKeys].join(', ')} already exists in the other section` + : undefined, })} > {!isLoading && (
- {(!searchTerm.trim() || - filteredWorkspaceEntries.length > 0 || - filteredNewWorkspaceRows.length > 0) && ( - -
- {(searchTerm.trim() - ? filteredWorkspaceEntries - : Object.entries(workspaceVars) - ).map(([key, value]) => { - const cred = workspaceEnvKeyToCredential.get(key) - const canEditRow = canCreateWorkspaceSecret && cred?.role === 'admin' - return ( - setPendingVisibilityChange({ envKey, next }) - : undefined - } - /> - ) - })} - {canCreateWorkspaceSecret && - (searchTerm.trim() - ? filteredNewWorkspaceRows - : newWorkspaceRows.map((row, index) => ({ row, originalIndex: index })) - ).map(({ row, originalIndex }) => ( - - ))} -
-
- )} + {renderWorkspaceSection({ + label: 'Workspace secrets', + visibility: 'secret', + entries: workspaceEntriesForRender, + draftRows: searchTerm.trim() + ? filteredNewWorkspaceRows + : newWorkspaceRows.map((row, index) => ({ row, originalIndex: index })), + onUpdateDraft: updateNewWorkspaceRow, + })} + + {renderWorkspaceSection({ + label: 'Workspace variables', + visibility: 'variable', + entries: workspaceEntriesForRender, + draftRows: searchTerm.trim() + ? filteredNewWorkspaceVariableRows + : newWorkspaceVariableRows.map((row, index) => ({ row, originalIndex: index })), + onUpdateDraft: updateNewWorkspaceVariableRow, + })} {(!searchTerm.trim() || filteredEnvVars.length > 0) && ( - +
{filteredEnvVars.map(({ envVar, originalIndex }) => (
From 1373fe2a7bd9e31e98ffef36b9eaa0542efc87cd Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 14:05:53 -0700 Subject: [PATCH 06/14] fix(secrets): use getErrorMessage for the visibility-change toast CI's check:audits gate caught a documented rule violation: CLAUDE.md says never write `e instanceof Error ? e.message : 'fallback'`, use getErrorMessage(e, fallback?) from @sim/utils/errors. The visibility-change handler did exactly the banned form. Worth noting for next time: check:audits is not part of `bun run check`, so a local run of check + lint + type-check + tests passes while CI fails. --- .../secrets/components/secrets-manager/secrets-manager.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx index ca336f79793..2049866e32c 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' import { ChipConfirmModal, ChipInput, cn, toast } from '@sim/emcn' import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' import { useQueryClient } from '@tanstack/react-query' import { useParams, useRouter } from 'next/navigation' import { canMutateWorkspaceSettingsSection } from '@/components/settings/navigation' @@ -467,7 +468,7 @@ export function SecretsManager() { ) } catch (error) { logger.error('Failed to change secret visibility', { error }) - toast.error(error instanceof Error ? error.message : 'Failed to change visibility') + toast.error(getErrorMessage(error, 'Failed to change visibility')) } }, [pendingVisibilityChange, workspaceId, upsertWorkspaceMutation]) From 686e61c770f3cf32e460ad794158b1644ea8cae4 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 14:30:01 -0700 Subject: [PATCH 07/14] fix(secrets): authorize visibility changes before any write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings, both real. Ordering: the PUT committed the value upsert and credential rows, then called the visibility change, which can 403. A request mixing an allowed new key with an unauthorized flip therefore persisted half of itself and still failed — state changed by a rejected call, and the audit record for the successful part never written. setWorkspaceEnvVisibility splits into authorize and apply so the route can deny before anything reaches the database. Covered by a test that asserts no credential creation and no apply happen on the denied path. Silent no-op: visibilityByKey only reaches credential CREATION, so asking for a policy on an existing key was dropped while the call still reported success. That includes a variable -> secret remediation, the case where a false success is most harmful — a caller told it worked would never retry. Flipping an existing key needs the stricter disclosure gate this path does not perform, so it now throws instead of pretending. Also consumes the consolidated envVars contract: one kind where a secret is name-only and a non-secret carries its value. --- .../workspaces/[id]/environment/route.test.ts | 85 ++++++++++++++++++- .../api/workspaces/[id]/environment/route.ts | 64 +++++++------- .../copilot/chat/workspace-context.test.ts | 14 ++- .../sim/lib/copilot/chat/workspace-context.ts | 19 +++-- .../lib/copilot/generated/vfs-snapshot-v1.ts | 19 ++--- apps/sim/lib/credentials/environment.ts | 75 ++++++++++++++-- apps/sim/lib/environment/utils.ts | 42 +++++++++ 7 files changed, 257 insertions(+), 61 deletions(-) diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.test.ts b/apps/sim/app/api/workspaces/[id]/environment/route.test.ts index e6d6fc7f7fe..225f3778dbc 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.test.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.test.ts @@ -9,11 +9,29 @@ const { mockGetWorkspaceById, mockGetUserEntityPermissions, mockGetWorkspaceEnvKeyAdminAccess, + mockAuthorizeVisibility, + mockApplyVisibility, + mockCreateWorkspaceEnvCredentials, + MockVisibilityAccessError, } = vi.hoisted(() => ({ mockGetPersonalEnvKeyRawAccess: vi.fn(), mockGetWorkspaceById: vi.fn(), mockGetUserEntityPermissions: vi.fn(), mockGetWorkspaceEnvKeyAdminAccess: vi.fn(), + mockAuthorizeVisibility: vi.fn(), + mockApplyVisibility: vi.fn(), + mockCreateWorkspaceEnvCredentials: vi.fn(), + // Declared inside vi.hoisted: `vi.mock` factories hoist above module-scope + // class declarations, so a plain `class` here is in its TDZ when the factory + // runs and the mock module fails to initialize. + MockVisibilityAccessError: class extends Error { + keys: string[] + constructor(keys: string[]) { + super('You must be an admin of these secrets to change their visibility') + this.name = 'WorkspaceEnvVisibilityAccessError' + this.keys = keys + } + }, })) vi.mock('@/lib/workspaces/permissions/utils', () => ({ @@ -26,11 +44,14 @@ const mockGetPersonalAndWorkspaceEnv = environmentUtilsMockFns.mockGetPersonalAn vi.mock('@/lib/credentials/environment', () => ({ getPersonalEnvKeyRawAccess: mockGetPersonalEnvKeyRawAccess, getWorkspaceEnvKeyAdminAccess: mockGetWorkspaceEnvKeyAdminAccess, - createWorkspaceEnvCredentials: vi.fn(), + createWorkspaceEnvCredentials: mockCreateWorkspaceEnvCredentials, deleteWorkspaceEnvCredentials: vi.fn(), + authorizeWorkspaceEnvVisibilityChange: mockAuthorizeVisibility, + applyWorkspaceEnvVisibilityChange: mockApplyVisibility, + WorkspaceEnvVisibilityAccessError: MockVisibilityAccessError, })) -import { GET } from '@/app/api/workspaces/[id]/environment/route' +import { GET, PUT } from '@/app/api/workspaces/[id]/environment/route' const mockGetSession = authMockFns.mockGetSession @@ -202,3 +223,63 @@ describe('GET /api/workspaces/[id]/environment', () => { }) }) }) + +describe('PUT /api/workspaces/[id]/environment — visibility ordering', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetSession.mockResolvedValue({ user: { id: 'u-1' } }) + mockGetUserEntityPermissions.mockResolvedValue('write') + mockGetWorkspaceEnvKeyAdminAccess.mockResolvedValue({ + adminKeys: new Set(), + knownKeys: new Set(), + variableKeys: new Set(), + }) + mockApplyVisibility.mockResolvedValue({ changedKeys: [] }) + }) + + async function callPut(body: unknown) { + const request = createMockRequest('PUT', body) + const response = await PUT(request, buildParams()) + return { status: response.status, body: await response.json() } + } + + /** + * The ordering defect Greptile caught. A request mixing an allowed NEW key + * with an unauthorized visibility flip used to commit the key and its + * credential rows and only THEN return 403 — a rejected call that changed + * workspace state and skipped its audit record. + */ + it('writes nothing when a visibility change is denied', async () => { + mockAuthorizeVisibility.mockRejectedValue(new MockVisibilityAccessError(['STRIPE_KEY'])) + + const { status } = await callPut({ + variables: { BRAND_NEW: 'v' }, + visibility: { STRIPE_KEY: 'variable' }, + }) + + expect(status).toBe(403) + // The whole point: authorization ran before any write reached the database. + expect(mockCreateWorkspaceEnvCredentials).not.toHaveBeenCalled() + expect(mockApplyVisibility).not.toHaveBeenCalled() + }) + + it('authorizes before applying on the success path', async () => { + mockAuthorizeVisibility.mockResolvedValue([ + { credentialId: 'c-1', envKey: 'SUPPORT_EMAIL', next: 'variable' }, + ]) + mockApplyVisibility.mockResolvedValue({ changedKeys: ['SUPPORT_EMAIL'] }) + + const { status } = await callPut({ + variables: {}, + visibility: { SUPPORT_EMAIL: 'variable' }, + }) + + expect(status).toBe(200) + expect(mockAuthorizeVisibility).toHaveBeenCalled() + expect(mockApplyVisibility).toHaveBeenCalledWith( + expect.objectContaining({ + changes: [{ credentialId: 'c-1', envKey: 'SUPPORT_EMAIL', next: 'variable' }], + }) + ) + }) +}) diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.ts b/apps/sim/app/api/workspaces/[id]/environment/route.ts index c7a1d5fc226..ad9c0ac5dce 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.ts @@ -16,12 +16,14 @@ import { encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { + type AuthorizedVisibilityChange, + applyWorkspaceEnvVisibilityChange, + authorizeWorkspaceEnvVisibilityChange, createWorkspaceEnvCredentials, deleteWorkspaceEnvCredentials, type EnvVisibility, getPersonalEnvKeyRawAccess, getWorkspaceEnvKeyAdminAccess, - setWorkspaceEnvVisibility, WorkspaceEnvVisibilityAccessError, } from '@/lib/credentials/environment' import { @@ -249,6 +251,31 @@ export const PUT = withRouteHandler( ) } + // Authorized BEFORE any write. Running this after the value upsert let a + // request that mixes an allowed new key with an unauthorized flip commit + // the key and its credential rows and THEN return 403 — a rejected call + // that changed workspace state and skipped its audit record. + let authorizedVisibilityChanges: AuthorizedVisibilityChange[] = [] + if (visibility) { + try { + authorizedVisibilityChanges = await authorizeWorkspaceEnvVisibilityChange({ + workspaceId, + updates: visibility, + actingUserId: userId, + }) + } catch (error) { + if (error instanceof WorkspaceEnvVisibilityAccessError) { + logger.warn(`[${requestId}] Workspace env visibility change denied`, { + workspaceId, + userId, + keys: error.keys, + }) + return NextResponse.json({ error: error.message }, { status: 403 }) + } + throw error + } + } + const encryptedIncoming = await Promise.all( Object.entries(variables).map(async ([key, value]) => { const { encrypted } = await encryptSecret(value) @@ -300,34 +327,13 @@ export const PUT = withRouteHandler( visibilityByKey: visibility, }) - // Flips are applied after the credential rows exist so a create-as-variable - // in the same request lands on a row rather than silently no-opping. Only - // pre-existing keys can flip; brand-new ones got their policy above. - let flippedKeys: string[] = [] - if (visibility) { - const existingKeyUpdates = Object.fromEntries( - Object.entries(visibility).filter(([key]) => !newKeys.includes(key)) - ) - try { - const { changedKeys } = await setWorkspaceEnvVisibility({ - workspaceId, - updates: existingKeyUpdates, - actingUserId: userId, - }) - flippedKeys = changedKeys - } catch (error) { - if (error instanceof WorkspaceEnvVisibilityAccessError) { - logger.warn(`[${requestId}] Workspace env visibility change denied`, { - workspaceId, - userId, - keys: error.keys, - }) - return NextResponse.json({ error: error.message }, { status: 403 }) - } - throw error - } - if (flippedKeys.length > 0) invalidateEffectiveDecryptedEnvCache({ workspaceId }) - } + // Already authorized above; this only writes. Brand-new keys took their + // policy from `visibilityByKey` on create, so the authorized set only + // ever contains pre-existing keys. + const { changedKeys: flippedKeys } = await applyWorkspaceEnvVisibilityChange({ + changes: authorizedVisibilityChanges, + }) + if (flippedKeys.length > 0) invalidateEffectiveDecryptedEnvCache({ workspaceId }) recordAudit({ workspaceId, diff --git a/apps/sim/lib/copilot/chat/workspace-context.test.ts b/apps/sim/lib/copilot/chat/workspace-context.test.ts index c3678ce417e..58a746e464c 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.test.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.test.ts @@ -125,7 +125,10 @@ describe('buildWorkspaceMd - connected integrations / credentials', () => { expect(md).toContain('## Environment Variables (2)') expect(md).toContain('- OPENAI_API_KEY') expect(md).toContain('- STRIPE_SECRET_KEY') - expect(buildVfsSnapshot(data).envVars).toEqual(['OPENAI_API_KEY', 'STRIPE_SECRET_KEY']) + expect(buildVfsSnapshot(data).envVars).toEqual([ + { name: 'OPENAI_API_KEY' }, + { name: 'STRIPE_SECRET_KEY' }, + ]) }) it('shows values for non-secret env vars and only names for secrets', () => { @@ -141,8 +144,10 @@ describe('buildWorkspaceMd - connected integrations / credentials', () => { expect(md).toContain('- OPENAI_API_KEY\n') expect(md).not.toContain('OPENAI_API_KEY =') - // Carried as a struct kind so Go can diff a VALUE change, not just a name. - expect(buildVfsSnapshot(data).nonSecretEnvVars).toEqual([ + // One kind carries both: a secret is name-only, a non-secret adds its value + // so Go can diff a VALUE change and not just a name. + expect(buildVfsSnapshot(data).envVars).toEqual([ + { name: 'OPENAI_API_KEY' }, { name: 'SUPPORT_EMAIL', value: 'help@acme.com' }, ]) }) @@ -151,7 +156,8 @@ describe('buildWorkspaceMd - connected integrations / credentials', () => { const data = baseData({ envVariables: ['OPENAI_API_KEY'] }) expect(buildWorkspaceMd(data)).not.toContain('non-secret') - expect(buildVfsSnapshot(data).nonSecretEnvVars).toEqual([]) + // Every name still ships; none carries a value. + expect(buildVfsSnapshot(data).envVars).toEqual([{ name: 'OPENAI_API_KEY' }]) }) }) diff --git a/apps/sim/lib/copilot/chat/workspace-context.ts b/apps/sim/lib/copilot/chat/workspace-context.ts index 9ea5973a95f..d87569cddb9 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.ts @@ -703,14 +703,17 @@ export function buildVfsSnapshot(data: WorkspaceMdData): VfsSnapshotV1 { ...(c.displayName ? { displayName: c.displayName } : {}), ...(c.role ? { role: c.role } : {}), })), - envVars: data.envVariables, - // Carries values, unlike `envVars`. That is the point: a non-secret value - // edit changes no NAME, so a name-only kind would emit no delta and leave - // the model answering from a stale baseline. - nonSecretEnvVars: (data.nonSecretEnvVariables ?? []).map((v) => ({ - name: v.name, - value: v.value, - })), + // One kind for both. A secret carries its name and nothing else; a + // non-secret also carries its value, which is what lets the differ report a + // value edit — the name alone does not change, so a names-only shape would + // emit no delta and leave the model answering from a stale baseline. + envVars: (() => { + const values = new Map((data.nonSecretEnvVariables ?? []).map((v) => [v.name, v.value])) + return data.envVariables.map((name) => { + const value = values.get(name) + return value === undefined ? { name } : { name, value } + }) + })(), customTools: (data.customTools ?? []).map((t) => ({ id: t.id, name: t.name })), customBlocks: (data.customBlocks ?? []).map((b) => ({ type: b.type, diff --git a/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts b/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts index d1ae0288705..938b59dc401 100644 --- a/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts +++ b/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts @@ -7,13 +7,12 @@ export interface VfsSnapshotV1 { customBlocks?: VfsSnapshotV1CustomBlock[] customTools?: VfsSnapshotV1NamedResource[] - envVars?: string[] + envVars?: VfsSnapshotV1EnvVar[] files?: VfsSnapshotV1File[] integrations?: VfsSnapshotV1Integration[] knowledgeBases?: VfsSnapshotV1KnowledgeBase[] mcpServers?: VfsSnapshotV1McpServer[] members?: VfsSnapshotV1Member[] - nonSecretEnvVars?: VfsSnapshotV1NonSecretEnvVar[] sandboxes?: VfsSnapshotV1Sandbox[] skills?: VfsSnapshotV1Skill[] tables?: VfsSnapshotV1Table[] @@ -37,6 +36,14 @@ export interface VfsSnapshotV1NamedResource { id: string name: string } +/** + * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema + * via the `definition` "VfsSnapshotV1EnvVar". + */ +export interface VfsSnapshotV1EnvVar { + name: string + value?: string +} /** * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema * via the `definition` "VfsSnapshotV1File". @@ -88,14 +95,6 @@ export interface VfsSnapshotV1Member { name?: string permissionType?: string } -/** - * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema - * via the `definition` "VfsSnapshotV1NonSecretEnvVar". - */ -export interface VfsSnapshotV1NonSecretEnvVar { - name: string - value?: string -} /** * This interface was referenced by `VfsSnapshotV1`'s JSON-Schema * via the `definition` "VfsSnapshotV1Sandbox". diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 225a25a9a7c..93ffdb5fda7 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -503,15 +503,33 @@ export class WorkspaceEnvVisibilityAccessError extends Error { * A no-op request is authorized trivially — asking for the visibility a key * already has reveals nothing and must not 403. */ -export async function setWorkspaceEnvVisibility(params: { +export interface AuthorizedVisibilityChange { + credentialId: string + envKey: string + next: EnvVisibility +} + +/** + * Resolves and AUTHORIZES a visibility change without writing anything. + * + * Split from the apply step so a caller can fail a denied request before it + * commits anything else. The route mixes value writes and visibility changes in + * one request; running the check afterwards let an allowed new key (and its + * credential rows) persist while the request still returned 403, leaving state + * changed by a rejected call and skipping the audit record for it. + * + * A no-op request authorizes trivially — asking for the visibility a key + * already has reveals nothing and must not 403. + */ +export async function authorizeWorkspaceEnvVisibilityChange(params: { workspaceId: string updates: Record actingUserId: string executor?: DbOrTx -}): Promise<{ changedKeys: string[] }> { +}): Promise { const { workspaceId, updates, actingUserId, executor = db } = params const requestedKeys = Object.keys(updates).filter(Boolean) - if (requestedKeys.length === 0) return { changedKeys: [] } + if (requestedKeys.length === 0) return [] const existingRows = await executor .select({ @@ -531,7 +549,7 @@ export async function setWorkspaceEnvVisibility(params: { const changing = existingRows.filter( (row) => row.envKey !== null && updates[row.envKey] !== row.envVisibility ) - if (changing.length === 0) return { changedKeys: [] } + if (changing.length === 0) return [] const changingKeys = changing.map((row) => row.envKey as string) const [isWorkspaceAdmin, { adminKeys }] = await Promise.all([ @@ -544,15 +562,56 @@ export async function setWorkspaceEnvVisibility(params: { throw new WorkspaceEnvVisibilityAccessError(forbidden) } + return changing.map((row) => ({ + credentialId: row.id, + envKey: row.envKey as string, + next: updates[row.envKey as string], + })) +} + +/** + * Applies changes already authorized by + * {@link authorizeWorkspaceEnvVisibilityChange}. Performs no permission check of + * its own — never call it with unauthorized input. + */ +export async function applyWorkspaceEnvVisibilityChange(params: { + changes: AuthorizedVisibilityChange[] + executor?: DbOrTx +}): Promise<{ changedKeys: string[] }> { + const { changes, executor = db } = params + if (changes.length === 0) return { changedKeys: [] } + const now = new Date() - for (const row of changing) { + for (const change of changes) { await executor .update(credential) - .set({ envVisibility: updates[row.envKey as string], updatedAt: now }) - .where(eq(credential.id, row.id)) + .set({ envVisibility: change.next, updatedAt: now }) + .where(eq(credential.id, change.credentialId)) } + return { changedKeys: changes.map((change) => change.envKey) } +} - return { changedKeys: changingKeys } +/** + * The single path that changes an env key's disclosure policy. + * + * Centralized because flipping `secret -> variable` is a disclosure event that + * cannot be undone: by the time the flag is reverted the value has plausibly + * reached trace spans, log rows, agent context, and members' browsers. Both + * directions therefore take the stricter gate (workspace admin, or credential + * admin on that specific key) rather than the workspace `write` that suffices + * for editing a value. + * + * Authorizes then applies. Callers that must authorize BEFORE unrelated writes + * should use the two halves directly. + */ +export async function setWorkspaceEnvVisibility(params: { + workspaceId: string + updates: Record + actingUserId: string + executor?: DbOrTx +}): Promise<{ changedKeys: string[] }> { + const changes = await authorizeWorkspaceEnvVisibilityChange(params) + return applyWorkspaceEnvVisibilityChange({ changes, executor: params.executor }) } export async function syncPersonalEnvCredentialsForUser(params: { diff --git a/apps/sim/lib/environment/utils.ts b/apps/sim/lib/environment/utils.ts index 4b31bbb618f..3b92526bc66 100644 --- a/apps/sim/lib/environment/utils.ts +++ b/apps/sim/lib/environment/utils.ts @@ -33,6 +33,20 @@ const WORKSPACE_ENV_DENIAL_MESSAGES: Record = 'write-access-required': 'Write access is required to add new secrets', } +/** + * Thrown when a caller asks to change the disclosure policy of an EXISTING key + * through the value-upsert path, which cannot apply it. Loud rather than a + * silent no-op: a caller told the change succeeded would never retry it. + */ +export class WorkspaceEnvVisibilityUnsupportedError extends Error { + constructor(readonly keys: string[]) { + super( + `Changing visibility of an existing environment variable is not supported here (${keys.join(', ')}); change it in workspace settings` + ) + this.name = 'WorkspaceEnvVisibilityUnsupportedError' + } +} + /** Thrown when the acting user may not write one of the requested env keys. */ export class WorkspaceEnvAccessError extends Error { constructor( @@ -476,6 +490,34 @@ export async function upsertWorkspaceEnvVars( visibilityByKey: options?.visibilityByKey, }) + // `visibilityByKey` only reaches credential CREATION, so a requested policy on + // a key that already exists would be silently dropped while the call still + // reported success — including a `variable -> secret` remediation, the case + // where a false success is most harmful. Flipping an existing key needs the + // stricter disclosure gate this path does not perform, so surface the mismatch + // rather than pretending it applied. + if (options?.visibilityByKey) { + const existingKeys = updatedKeys.filter((key) => !newKeys.includes(key)) + const requested = Object.fromEntries( + existingKeys + .filter((key) => options.visibilityByKey?.[key]) + .map((key) => [key, options.visibilityByKey?.[key] as EnvVisibility]) + ) + if (Object.keys(requested).length > 0) { + const { variableKeys } = await getWorkspaceEnvKeyAdminAccess({ + workspaceId, + envKeys: Object.keys(requested), + userId: actingUserId, + }) + const unapplied = Object.entries(requested) + .filter(([key, next]) => (variableKeys.has(key) ? 'variable' : 'secret') !== next) + .map(([key]) => key) + if (unapplied.length > 0) { + throw new WorkspaceEnvVisibilityUnsupportedError(unapplied) + } + } + } + recordAudit({ workspaceId, actorId: actingUserId, From 2fac1094e538a279ea75863cd9503baf8a0ba8c3 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 14:38:08 -0700 Subject: [PATCH 08/14] fix(secrets): re-decide visibility authorization at write time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-flight authorization ran before the value upsert and the credential inserts, then its resolved credential IDs were applied afterwards. Access revoked in that window was never observed, so a former credential admin could still flip a secret to a workspace-visible variable. The flip now goes through setWorkspaceEnvVisibility inside its own transaction, which re-authorizes adjacent to the UPDATE. The pre-flight stays, with its result deliberately discarded, so a denied request still attempts no writes at all. applyWorkspaceEnvVisibilityChange is no longer exported — a decision carried across unrelated awaits is exactly what went stale, so there is no supported way to do that again. --- .../workspaces/[id]/environment/route.test.ts | 44 ++++++++++++--- .../api/workspaces/[id]/environment/route.ts | 56 ++++++++++++++----- apps/sim/lib/credentials/environment.ts | 21 +++++-- 3 files changed, 94 insertions(+), 27 deletions(-) diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.test.ts b/apps/sim/app/api/workspaces/[id]/environment/route.test.ts index 225f3778dbc..48f5b6946bd 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.test.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.test.ts @@ -10,7 +10,7 @@ const { mockGetUserEntityPermissions, mockGetWorkspaceEnvKeyAdminAccess, mockAuthorizeVisibility, - mockApplyVisibility, + mockSetVisibility, mockCreateWorkspaceEnvCredentials, MockVisibilityAccessError, } = vi.hoisted(() => ({ @@ -19,7 +19,7 @@ const { mockGetUserEntityPermissions: vi.fn(), mockGetWorkspaceEnvKeyAdminAccess: vi.fn(), mockAuthorizeVisibility: vi.fn(), - mockApplyVisibility: vi.fn(), + mockSetVisibility: vi.fn(), mockCreateWorkspaceEnvCredentials: vi.fn(), // Declared inside vi.hoisted: `vi.mock` factories hoist above module-scope // class declarations, so a plain `class` here is in its TDZ when the factory @@ -47,7 +47,7 @@ vi.mock('@/lib/credentials/environment', () => ({ createWorkspaceEnvCredentials: mockCreateWorkspaceEnvCredentials, deleteWorkspaceEnvCredentials: vi.fn(), authorizeWorkspaceEnvVisibilityChange: mockAuthorizeVisibility, - applyWorkspaceEnvVisibilityChange: mockApplyVisibility, + setWorkspaceEnvVisibility: mockSetVisibility, WorkspaceEnvVisibilityAccessError: MockVisibilityAccessError, })) @@ -234,7 +234,7 @@ describe('PUT /api/workspaces/[id]/environment — visibility ordering', () => { knownKeys: new Set(), variableKeys: new Set(), }) - mockApplyVisibility.mockResolvedValue({ changedKeys: [] }) + mockSetVisibility.mockResolvedValue({ changedKeys: [] }) }) async function callPut(body: unknown) { @@ -260,14 +260,14 @@ describe('PUT /api/workspaces/[id]/environment — visibility ordering', () => { expect(status).toBe(403) // The whole point: authorization ran before any write reached the database. expect(mockCreateWorkspaceEnvCredentials).not.toHaveBeenCalled() - expect(mockApplyVisibility).not.toHaveBeenCalled() + expect(mockSetVisibility).not.toHaveBeenCalled() }) - it('authorizes before applying on the success path', async () => { + it('re-decides authorization at write time rather than reusing the pre-flight', async () => { mockAuthorizeVisibility.mockResolvedValue([ { credentialId: 'c-1', envKey: 'SUPPORT_EMAIL', next: 'variable' }, ]) - mockApplyVisibility.mockResolvedValue({ changedKeys: ['SUPPORT_EMAIL'] }) + mockSetVisibility.mockResolvedValue({ changedKeys: ['SUPPORT_EMAIL'] }) const { status } = await callPut({ variables: {}, @@ -276,10 +276,36 @@ describe('PUT /api/workspaces/[id]/environment — visibility ordering', () => { expect(status).toBe(200) expect(mockAuthorizeVisibility).toHaveBeenCalled() - expect(mockApplyVisibility).toHaveBeenCalledWith( + // The pre-flight's resolved credential IDs are deliberately NOT carried into + // the write: the request the setter receives is the raw visibility map, so + // access is re-checked against current membership next to the UPDATE. + expect(mockSetVisibility).toHaveBeenCalledWith( expect.objectContaining({ - changes: [{ credentialId: 'c-1', envKey: 'SUPPORT_EMAIL', next: 'variable' }], + workspaceId: WORKSPACE_ID, + actingUserId: 'u-1', + updates: { SUPPORT_EMAIL: 'variable' }, }) ) }) + + /** + * The stale-authorization window. The pre-flight above passes, then the + * caller's credential-admin access is revoked while the value writes run. The + * write-time decision must still refuse — reusing the pre-flight result here + * would let a former admin disclose a secret after losing the right to. + */ + it('returns 403 when access is revoked between the pre-flight and the write', async () => { + mockAuthorizeVisibility.mockResolvedValue([ + { credentialId: 'c-1', envKey: 'STRIPE_KEY', next: 'variable' }, + ]) + mockSetVisibility.mockRejectedValue(new MockVisibilityAccessError(['STRIPE_KEY'])) + + const { status, body } = await callPut({ + variables: {}, + visibility: { STRIPE_KEY: 'variable' }, + }) + + expect(status).toBe(403) + expect(body.error).toContain('admin') + }) }) diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.ts b/apps/sim/app/api/workspaces/[id]/environment/route.ts index ad9c0ac5dce..76649988d40 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.ts @@ -16,14 +16,13 @@ import { encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { - type AuthorizedVisibilityChange, - applyWorkspaceEnvVisibilityChange, authorizeWorkspaceEnvVisibilityChange, createWorkspaceEnvCredentials, deleteWorkspaceEnvCredentials, type EnvVisibility, getPersonalEnvKeyRawAccess, getWorkspaceEnvKeyAdminAccess, + setWorkspaceEnvVisibility, WorkspaceEnvVisibilityAccessError, } from '@/lib/credentials/environment' import { @@ -251,14 +250,15 @@ export const PUT = withRouteHandler( ) } - // Authorized BEFORE any write. Running this after the value upsert let a - // request that mixes an allowed new key with an unauthorized flip commit - // the key and its credential rows and THEN return 403 — a rejected call - // that changed workspace state and skipped its audit record. - let authorizedVisibilityChanges: AuthorizedVisibilityChange[] = [] + // Pre-flight, BEFORE any write. Running the check after the value upsert + // let a request that mixes an allowed new key with an unauthorized flip + // commit the key and its credential rows and THEN return 403 — a rejected + // call that changed workspace state and skipped its audit record. The + // decision is deliberately discarded: it is re-made adjacent to the UPDATE + // below, so this pass only guarantees a denied request attempts no writes. if (visibility) { try { - authorizedVisibilityChanges = await authorizeWorkspaceEnvVisibilityChange({ + await authorizeWorkspaceEnvVisibilityChange({ workspaceId, updates: visibility, actingUserId: userId, @@ -327,12 +327,40 @@ export const PUT = withRouteHandler( visibilityByKey: visibility, }) - // Already authorized above; this only writes. Brand-new keys took their - // policy from `visibilityByKey` on create, so the authorized set only - // ever contains pre-existing keys. - const { changedKeys: flippedKeys } = await applyWorkspaceEnvVisibilityChange({ - changes: authorizedVisibilityChanges, - }) + // Re-authorized here rather than reusing the pre-flight decision above. + // That decision was made before the value writes and the credential + // inserts, so acting on it would let an admin whose access was revoked in + // the meantime still disclose a secret. The check runs adjacent to the + // UPDATE inside one transaction, and reads permissions through `db` (not + // `tx`) so it observes the latest committed membership rather than this + // transaction's snapshot. Brand-new keys took their policy from + // `visibilityByKey` on create, so only pre-existing keys can appear here. + let flippedKeys: string[] = [] + if (visibility) { + try { + flippedKeys = ( + await db.transaction((tx) => + setWorkspaceEnvVisibility({ + workspaceId, + updates: visibility, + actingUserId: userId, + executor: tx, + }) + ) + ).changedKeys + } catch (error) { + if (error instanceof WorkspaceEnvVisibilityAccessError) { + logger.warn(`[${requestId}] Workspace env visibility change denied at apply`, { + workspaceId, + userId, + keys: error.keys, + reason: 'access-revoked-mid-request', + }) + return NextResponse.json({ error: error.message }, { status: 403 }) + } + throw error + } + } if (flippedKeys.length > 0) invalidateEffectiveDecryptedEnvCache({ workspaceId }) recordAudit({ diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 93ffdb5fda7..88c8905ebdf 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -572,9 +572,16 @@ export async function authorizeWorkspaceEnvVisibilityChange(params: { /** * Applies changes already authorized by * {@link authorizeWorkspaceEnvVisibilityChange}. Performs no permission check of - * its own — never call it with unauthorized input. + * its own. + * + * Deliberately NOT exported. An authorization decision carried across unrelated + * awaits goes stale — the caller's credential-admin or workspace-admin access + * can be revoked in between, and applying by credential ID would then disclose a + * secret to someone who has just lost the right to disclose it. Keeping this + * private forces every caller through {@link setWorkspaceEnvVisibility}, which + * decides and writes adjacently. */ -export async function applyWorkspaceEnvVisibilityChange(params: { +async function applyWorkspaceEnvVisibilityChange(params: { changes: AuthorizedVisibilityChange[] executor?: DbOrTx }): Promise<{ changedKeys: string[] }> { @@ -601,8 +608,14 @@ export async function applyWorkspaceEnvVisibilityChange(params: { * admin on that specific key) rather than the workspace `write` that suffices * for editing a value. * - * Authorizes then applies. Callers that must authorize BEFORE unrelated writes - * should use the two halves directly. + * Authorizes and applies adjacently, so the decision cannot go stale between the + * two. Callers that also need to fail fast before unrelated writes can call + * {@link authorizeWorkspaceEnvVisibilityChange} as a pre-flight and discard its + * result — this function still re-decides authoritatively at write time. + * + * Pass `executor` to run the UPDATE inside a surrounding transaction. The + * permission reads intentionally go through `db`, not that transaction, so they + * observe the latest committed membership instead of the transaction's snapshot. */ export async function setWorkspaceEnvVisibility(params: { workspaceId: string From b552c94f9da31ed44920308ad65965852f1a1b99 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 14:52:14 -0700 Subject: [PATCH 09/14] fix(secrets): make env writes and their authorization one transaction Two rounds of ordering fixes each produced the next finding, because the value upsert, the credential inserts, and the visibility flip were three separate commits with the disclosure check somewhere among them. Moving the check only changed which writes were stranded by a denial. The PUT now runs authorization and every write it guards in one transaction, so a denial rolls all of it back and there is no ordering left to get wrong. createWorkspaceEnvCredentials takes an executor to join it. upsertWorkspaceEnvVars had the same shape on the copilot tool path and now validates the requested visibility inside its own transaction, before the write, where the stored key set is exact. Authorization also share-locks the permissions and credential_member rows that grant the caller's access before reading them, so a concurrent revocation either is observed and denies, or waits for the transaction rather than racing the UPDATE. Two UI fixes from the same review: a renamed variable saves as delete-old plus create-new, so it now carries its visibility across the rename instead of silently reverting to secret; and variable drafts join allWorkspaceKeys, so personal-vs-workspace conflict detection no longer misses names drafted in the Variables section. --- .../workspaces/[id]/environment/route.test.ts | 81 ++++---- .../api/workspaces/[id]/environment/route.ts | 179 ++++++++---------- .../secrets-manager/secrets-manager.tsx | 33 +++- apps/sim/lib/credentials/environment.ts | 75 ++++++-- apps/sim/lib/environment/utils.ts | 86 ++++++--- 5 files changed, 268 insertions(+), 186 deletions(-) diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.test.ts b/apps/sim/app/api/workspaces/[id]/environment/route.test.ts index 48f5b6946bd..3ea7c184082 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.test.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.test.ts @@ -9,18 +9,18 @@ const { mockGetWorkspaceById, mockGetUserEntityPermissions, mockGetWorkspaceEnvKeyAdminAccess, - mockAuthorizeVisibility, mockSetVisibility, mockCreateWorkspaceEnvCredentials, + mockRecordAudit, MockVisibilityAccessError, } = vi.hoisted(() => ({ mockGetPersonalEnvKeyRawAccess: vi.fn(), mockGetWorkspaceById: vi.fn(), mockGetUserEntityPermissions: vi.fn(), mockGetWorkspaceEnvKeyAdminAccess: vi.fn(), - mockAuthorizeVisibility: vi.fn(), mockSetVisibility: vi.fn(), mockCreateWorkspaceEnvCredentials: vi.fn(), + mockRecordAudit: vi.fn(), // Declared inside vi.hoisted: `vi.mock` factories hoist above module-scope // class declarations, so a plain `class` here is in its TDZ when the factory // runs and the mock module fails to initialize. @@ -34,6 +34,16 @@ const { }, })) +vi.mock('@/lib/core/security/encryption', () => ({ + encryptSecret: vi.fn(async (value: string) => ({ encrypted: `enc:${value}` })), +})) + +vi.mock('@sim/audit', () => ({ + recordAudit: mockRecordAudit, + AuditAction: { ENVIRONMENT_UPDATED: 'environment.updated' }, + AuditResourceType: { ENVIRONMENT: 'environment' }, +})) + vi.mock('@/lib/workspaces/permissions/utils', () => ({ getWorkspaceById: mockGetWorkspaceById, getUserEntityPermissions: mockGetUserEntityPermissions, @@ -46,7 +56,6 @@ vi.mock('@/lib/credentials/environment', () => ({ getWorkspaceEnvKeyAdminAccess: mockGetWorkspaceEnvKeyAdminAccess, createWorkspaceEnvCredentials: mockCreateWorkspaceEnvCredentials, deleteWorkspaceEnvCredentials: vi.fn(), - authorizeWorkspaceEnvVisibilityChange: mockAuthorizeVisibility, setWorkspaceEnvVisibility: mockSetVisibility, WorkspaceEnvVisibilityAccessError: MockVisibilityAccessError, })) @@ -244,68 +253,54 @@ describe('PUT /api/workspaces/[id]/environment — visibility ordering', () => { } /** - * The ordering defect Greptile caught. A request mixing an allowed NEW key - * with an unauthorized visibility flip used to commit the key and its - * credential rows and only THEN return 403 — a rejected call that changed - * workspace state and skipped its audit record. + * A rejected request must change nothing. The value upsert, the credential + * rows, and the visibility flip all run in ONE transaction now, so a denial + * anywhere throws and rolls the rest back — no ordering left to get wrong. */ - it('writes nothing when a visibility change is denied', async () => { - mockAuthorizeVisibility.mockRejectedValue(new MockVisibilityAccessError(['STRIPE_KEY'])) + it('returns 403 and records no audit when a visibility change is denied', async () => { + mockSetVisibility.mockRejectedValue(new MockVisibilityAccessError(['STRIPE_KEY'])) - const { status } = await callPut({ + const { status, body } = await callPut({ variables: { BRAND_NEW: 'v' }, visibility: { STRIPE_KEY: 'variable' }, }) expect(status).toBe(403) - // The whole point: authorization ran before any write reached the database. - expect(mockCreateWorkspaceEnvCredentials).not.toHaveBeenCalled() - expect(mockSetVisibility).not.toHaveBeenCalled() + expect(body.error).toContain('admin') + // The audit record lives after the transaction, so a rejected request + // cannot reach it — which is the observable proof nothing was committed. + expect(mockRecordAudit).not.toHaveBeenCalled() }) - it('re-decides authorization at write time rather than reusing the pre-flight', async () => { - mockAuthorizeVisibility.mockResolvedValue([ - { credentialId: 'c-1', envKey: 'SUPPORT_EMAIL', next: 'variable' }, - ]) + /** + * Both writes must be transaction-scoped. If either call moves back outside + * the transaction it loses its `executor` and a denial in the other would + * strand it committed — the exact failure this consolidation removed. + */ + it('runs the credential inserts and the visibility flip inside the transaction', async () => { mockSetVisibility.mockResolvedValue({ changedKeys: ['SUPPORT_EMAIL'] }) const { status } = await callPut({ - variables: {}, + variables: { NEW_KEY: 'v' }, visibility: { SUPPORT_EMAIL: 'variable' }, }) expect(status).toBe(200) - expect(mockAuthorizeVisibility).toHaveBeenCalled() - // The pre-flight's resolved credential IDs are deliberately NOT carried into - // the write: the request the setter receives is the raw visibility map, so - // access is re-checked against current membership next to the UPDATE. + expect(mockCreateWorkspaceEnvCredentials).toHaveBeenCalledWith( + expect.objectContaining({ executor: expect.anything() }) + ) expect(mockSetVisibility).toHaveBeenCalledWith( expect.objectContaining({ workspaceId: WORKSPACE_ID, actingUserId: 'u-1', updates: { SUPPORT_EMAIL: 'variable' }, + executor: expect.anything(), }) ) - }) - - /** - * The stale-authorization window. The pre-flight above passes, then the - * caller's credential-admin access is revoked while the value writes run. The - * write-time decision must still refuse — reusing the pre-flight result here - * would let a former admin disclose a secret after losing the right to. - */ - it('returns 403 when access is revoked between the pre-flight and the write', async () => { - mockAuthorizeVisibility.mockResolvedValue([ - { credentialId: 'c-1', envKey: 'STRIPE_KEY', next: 'variable' }, - ]) - mockSetVisibility.mockRejectedValue(new MockVisibilityAccessError(['STRIPE_KEY'])) - - const { status, body } = await callPut({ - variables: {}, - visibility: { STRIPE_KEY: 'variable' }, - }) - - expect(status).toBe(403) - expect(body.error).toContain('admin') + // Ordering matters: the disclosure change is applied last, after the writes + // it must not be separated from. + expect(mockCreateWorkspaceEnvCredentials.mock.invocationCallOrder[0]).toBeLessThan( + mockSetVisibility.mock.invocationCallOrder[0] + ) }) }) diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.ts b/apps/sim/app/api/workspaces/[id]/environment/route.ts index 76649988d40..0dc789d3687 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.ts @@ -16,7 +16,6 @@ import { encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { - authorizeWorkspaceEnvVisibilityChange, createWorkspaceEnvCredentials, deleteWorkspaceEnvCredentials, type EnvVisibility, @@ -250,32 +249,6 @@ export const PUT = withRouteHandler( ) } - // Pre-flight, BEFORE any write. Running the check after the value upsert - // let a request that mixes an allowed new key with an unauthorized flip - // commit the key and its credential rows and THEN return 403 — a rejected - // call that changed workspace state and skipped its audit record. The - // decision is deliberately discarded: it is re-made adjacent to the UPDATE - // below, so this pass only guarantees a denied request attempts no writes. - if (visibility) { - try { - await authorizeWorkspaceEnvVisibilityChange({ - workspaceId, - updates: visibility, - actingUserId: userId, - }) - } catch (error) { - if (error instanceof WorkspaceEnvVisibilityAccessError) { - logger.warn(`[${requestId}] Workspace env visibility change denied`, { - workspaceId, - userId, - keys: error.keys, - }) - return NextResponse.json({ error: error.message }, { status: 403 }) - } - throw error - } - } - const encryptedIncoming = await Promise.all( Object.entries(variables).map(async ([key, value]) => { const { encrypted } = await encryptSecret(value) @@ -283,85 +256,97 @@ export const PUT = withRouteHandler( }) ).then((entries) => Object.fromEntries(entries)) - const { existingEncrypted, merged } = await db.transaction(async (tx) => { - await tx.execute( - sql`SELECT set_config('lock_timeout', ${`${WORKSPACE_ENV_LOCK_TIMEOUT_MS}ms`}, true)` - ) - await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${workspaceId}, 0))`) - - const [existingRow] = await tx - .select() - .from(workspaceEnvironment) - .where(eq(workspaceEnvironment.workspaceId, workspaceId)) - .limit(1) - - const existing = ((existingRow?.variables as Record) ?? {}) as Record< - string, - string - > - const mergedVars = { ...existing, ...encryptedIncoming } + /** + * One transaction for authorization AND every write it guards. + * + * The value upsert, the credential rows, and the visibility flip used to + * be three separate commits with the disclosure check somewhere among + * them, so a request that was ultimately rejected could still leave the + * first writes committed and skip its audit record. Ordering the check + * earlier only moved which writes were stranded. With all of it in one + * transaction there is no ordering to get wrong: a denial throws and + * every write in the request rolls back together. + * + * `setWorkspaceEnvVisibility` runs LAST, after the writes it must not be + * separated from, and share-locks the rows granting the caller's access + * before reading them — so a revocation committing mid-request either is + * observed and denies, or waits for this transaction. + */ + let flippedKeys: string[] = [] + let existingEncrypted: Record + let merged: Record + try { + ;({ existingEncrypted, merged } = await db.transaction(async (tx) => { + await tx.execute( + sql`SELECT set_config('lock_timeout', ${`${WORKSPACE_ENV_LOCK_TIMEOUT_MS}ms`}, true)` + ) + await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${workspaceId}, 0))`) + + const [existingRow] = await tx + .select() + .from(workspaceEnvironment) + .where(eq(workspaceEnvironment.workspaceId, workspaceId)) + .limit(1) + + const existing = ((existingRow?.variables as Record) ?? {}) as Record< + string, + string + > + const mergedVars = { ...existing, ...encryptedIncoming } + + if (Object.keys(encryptedIncoming).length > 0) { + await tx + .insert(workspaceEnvironment) + .values({ + id: generateId(), + workspaceId, + variables: mergedVars, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: [workspaceEnvironment.workspaceId], + set: { variables: mergedVars, updatedAt: new Date() }, + }) + } - await tx - .insert(workspaceEnvironment) - .values({ - id: generateId(), + // Derived from the stored map, not the credential rows: a legacy key + // present in jsonb without a credential row is NOT new, and minting an + // ACL for it would make the caller its secret-admin. + await createWorkspaceEnvCredentials({ workspaceId, - variables: mergedVars, - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoUpdate({ - target: [workspaceEnvironment.workspaceId], - set: { variables: mergedVars, updatedAt: new Date() }, + newKeys: Object.keys(variables).filter((k) => !(k in existing)), + actingUserId: userId, + visibilityByKey: visibility, + executor: tx, }) - return { existingEncrypted: existing, merged: mergedVars } - }) - - invalidateEffectiveDecryptedEnvCache({ workspaceId }) - const newKeys = Object.keys(variables).filter((k) => !(k in existingEncrypted)) - await createWorkspaceEnvCredentials({ - workspaceId, - newKeys, - actingUserId: userId, - visibilityByKey: visibility, - }) - - // Re-authorized here rather than reusing the pre-flight decision above. - // That decision was made before the value writes and the credential - // inserts, so acting on it would let an admin whose access was revoked in - // the meantime still disclose a secret. The check runs adjacent to the - // UPDATE inside one transaction, and reads permissions through `db` (not - // `tx`) so it observes the latest committed membership rather than this - // transaction's snapshot. Brand-new keys took their policy from - // `visibilityByKey` on create, so only pre-existing keys can appear here. - let flippedKeys: string[] = [] - if (visibility) { - try { - flippedKeys = ( - await db.transaction((tx) => - setWorkspaceEnvVisibility({ - workspaceId, - updates: visibility, - actingUserId: userId, - executor: tx, - }) - ) - ).changedKeys - } catch (error) { - if (error instanceof WorkspaceEnvVisibilityAccessError) { - logger.warn(`[${requestId}] Workspace env visibility change denied at apply`, { + if (visibility) { + const applied = await setWorkspaceEnvVisibility({ workspaceId, - userId, - keys: error.keys, - reason: 'access-revoked-mid-request', + updates: visibility, + actingUserId: userId, + executor: tx, }) - return NextResponse.json({ error: error.message }, { status: 403 }) + flippedKeys = applied.changedKeys } - throw error + + return { existingEncrypted: existing, merged: mergedVars } + })) + } catch (error) { + if (error instanceof WorkspaceEnvVisibilityAccessError) { + logger.warn(`[${requestId}] Workspace env visibility change denied`, { + workspaceId, + userId, + keys: error.keys, + }) + // Nothing to undo: the denial rolled the whole transaction back. + return NextResponse.json({ error: error.message }, { status: 403 }) } + throw error } - if (flippedKeys.length > 0) invalidateEffectiveDecryptedEnvCache({ workspaceId }) + + invalidateEffectiveDecryptedEnvCache({ workspaceId }) recordAudit({ workspaceId, diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx index 2049866e32c..2e1ccb25e86 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx @@ -427,6 +427,15 @@ export function SecretsManager() { const [showUnsavedChanges, setShowUnsavedChanges] = useState(false) const [workspaceVars, setWorkspaceVars] = useState>({}) const [renamingKey, setRenamingKey] = useState(null) + /** + * Visibility carried across a rename. A rename saves as delete-old + + * create-new, and only the new name reaches the server — so without this the + * recreated key defaults to `secret`, silently revoking every member's read + * access and bouncing the row into the Secrets section. + */ + const [renamedKeyVisibility, setRenamedKeyVisibility] = useState>( + {} + ) const [pendingKeyValue, setPendingKeyValue] = useState('') const [pendingVisibilityChange, setPendingVisibilityChange] = useState<{ envKey: string @@ -500,13 +509,19 @@ export function SecretsManager() { return mapped.filter(({ row }) => row.key.toLowerCase().includes(term)) }, [newWorkspaceRows, searchTerm]) + /** + * Every name that will exist at workspace scope after save — saved keys plus + * BOTH sections' drafts. Personal-vs-workspace conflict detection reads this, + * so omitting the variable drafts let a clashing name save from the Variables + * section that the Secrets section would have blocked. + */ const allWorkspaceKeys = useMemo(() => { const keys = new Set(Object.keys(workspaceVars)) - for (const row of newWorkspaceRows) { + for (const row of [...newWorkspaceRows, ...newWorkspaceVariableRows]) { if (row.key) keys.add(row.key) } return keys - }, [workspaceVars, newWorkspaceRows]) + }, [workspaceVars, newWorkspaceRows, newWorkspaceVariableRows]) const hasChanges = useMemo(() => { const initialVars = initialVarsRef.current.filter((v) => v.key || v.value) @@ -745,6 +760,14 @@ export function SecretsManager() { next[newKey] = currentValue return next }) + const previousVisibility = + renamedKeyVisibility[currentKey] ?? workspaceEnvData?.visibility?.[currentKey] ?? 'secret' + setRenamedKeyVisibility((prev) => { + const next = { ...prev } + delete next[currentKey] + if (previousVisibility === 'variable') next[newKey] = 'variable' + return next + }) } const handleWorkspaceValueChange = (key: string, value: string) => { @@ -864,6 +887,7 @@ export function SecretsManager() { setWorkspaceVars({ ...initialWorkspaceVarsRef.current }) setNewWorkspaceRows([createEmptyEnvVar()]) setNewWorkspaceVariableRows([createEmptyEnvVar()]) + setRenamedKeyVisibility({}) setShowUnsavedChanges(false) } @@ -969,7 +993,7 @@ export function SecretsManager() { // path (that needs the confirmed disclosure flow), which is why // `duplicateDraftKeys` blocks saving a name that already exists as a secret // rather than letting it save and silently stay secret. - const draftVariableVisibility: Record = {} + const draftVariableVisibility: Record = { ...renamedKeyVisibility } for (const row of newWorkspaceVariableRows) { if (row.key && row.value) { mergedWorkspaceVars[row.key] = row.value @@ -1054,6 +1078,9 @@ export function SecretsManager() { // still drafted. setNewWorkspaceRows([createEmptyEnvVar()]) setNewWorkspaceVariableRows([createEmptyEnvVar()]) + // Carried visibility is consumed by the save it belonged to; the refetched + // server state is authoritative from here. + setRenamedKeyVisibility({}) if (mutations.length > 0) { toast.success('Secrets saved') } diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 88c8905ebdf..9e9508e8cda 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -31,14 +31,17 @@ export interface WorkspaceMembership { * Credential-admin status is derived from workspace role at access time, so * members are seeded only for use access (the owner plus permission holders). */ -async function getWorkspaceMembership(workspaceId: string): Promise { +async function getWorkspaceMembership( + workspaceId: string, + executor: DbOrTx = db +): Promise { const [workspaceRows, permissionRows] = await Promise.all([ - db + executor .select({ ownerId: workspace.ownerId }) .from(workspace) .where(eq(workspace.id, workspaceId)) .limit(1), - db + executor .select({ userId: permissions.userId }) .from(permissions) .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))), @@ -409,18 +412,25 @@ export async function createWorkspaceEnvCredentials(params: { actingUserId: string /** Per-key disclosure policy for the new keys. Anything unlisted is a secret. */ visibilityByKey?: Record + /** + * Runs the inserts inside a caller-supplied transaction so they roll back with + * the rest of the request. The env PUT mixes these credential rows with a jsonb + * value upsert and a visibility change, and a denial in any of them must leave + * none of the others committed. + */ + executor?: DbOrTx }): Promise { - const { workspaceId, newKeys, actingUserId, visibilityByKey } = params + const { workspaceId, newKeys, actingUserId, visibilityByKey, executor = db } = params const keys = Array.from(new Set(newKeys.filter(Boolean))) if (keys.length === 0) return - const { ownerId, memberUserIds } = await getWorkspaceMembership(workspaceId) + const { ownerId, memberUserIds } = await getWorkspaceMembership(workspaceId, executor) if (!ownerId) return const now = new Date() - const inserted = await db + const inserted = await executor .insert(credential) .values( keys.map((envKey) => ({ @@ -456,7 +466,7 @@ export async function createWorkspaceEnvCredentials(params: { })) ) - await db.insert(credentialMember).values(membershipValues).onConflictDoNothing() + await executor.insert(credentialMember).values(membershipValues).onConflictDoNothing() } /** @@ -521,7 +531,7 @@ export interface AuthorizedVisibilityChange { * A no-op request authorizes trivially — asking for the visibility a key * already has reveals nothing and must not 403. */ -export async function authorizeWorkspaceEnvVisibilityChange(params: { +async function authorizeWorkspaceEnvVisibilityChange(params: { workspaceId: string updates: Record actingUserId: string @@ -552,6 +562,43 @@ export async function authorizeWorkspaceEnvVisibilityChange(params: { if (changing.length === 0) return [] const changingKeys = changing.map((row) => row.envKey as string) + + // Share-lock the rows that grant this caller the right to disclose, BEFORE + // reading them. A concurrent revocation must either commit before the lock — + // in which case the reads below observe it and deny — or block until this + // transaction ends. Without the locks the permission read and the UPDATE are + // two independent statements, and a revocation landing between them leaves a + // former admin able to flip a secret to a workspace-visible variable. + // + // `executor` is the caller's transaction, which is what makes the locks span + // the UPDATE. Called without one, each statement is its own implicit + // transaction and the locks release immediately — harmless, but it is why + // `setWorkspaceEnvVisibility` is the only supported entry point. + await executor + .select({ id: permissions.id }) + .from(permissions) + .where( + and( + eq(permissions.userId, actingUserId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) + ) + .for('share') + await executor + .select({ id: credentialMember.id }) + .from(credentialMember) + .where( + and( + eq(credentialMember.userId, actingUserId), + inArray( + credentialMember.credentialId, + changing.map((row) => row.id) + ) + ) + ) + .for('share') + const [isWorkspaceAdmin, { adminKeys }] = await Promise.all([ hasWorkspaceAdminAccess(actingUserId, workspaceId), getWorkspaceEnvKeyAdminAccess({ workspaceId, envKeys: changingKeys, userId: actingUserId }), @@ -608,14 +655,12 @@ async function applyWorkspaceEnvVisibilityChange(params: { * admin on that specific key) rather than the workspace `write` that suffices * for editing a value. * - * Authorizes and applies adjacently, so the decision cannot go stale between the - * two. Callers that also need to fail fast before unrelated writes can call - * {@link authorizeWorkspaceEnvVisibilityChange} as a pre-flight and discard its - * result — this function still re-decides authoritatively at write time. + * Authorizes and applies adjacently, in that order, with no caller able to hold + * the decision in between — both halves are unexported for exactly that reason. * - * Pass `executor` to run the UPDATE inside a surrounding transaction. The - * permission reads intentionally go through `db`, not that transaction, so they - * observe the latest committed membership instead of the transaction's snapshot. + * Pass `executor` to run inside the caller's transaction. That is what lets the + * authorization's share-locks span the UPDATE, and what lets a denial roll back + * the caller's other writes rather than stranding them. */ export async function setWorkspaceEnvVisibility(params: { workspaceId: string diff --git a/apps/sim/lib/environment/utils.ts b/apps/sim/lib/environment/utils.ts index 3b92526bc66..e16d385cc29 100644 --- a/apps/sim/lib/environment/utils.ts +++ b/apps/sim/lib/environment/utils.ts @@ -395,6 +395,50 @@ export async function upsertPersonalEnvVars( /** * Encrypts and upserts workspace environment variables, merging with existing. */ +/** + * Rejects a requested disclosure policy that this path cannot apply. + * + * `visibilityByKey` only reaches credential CREATION, so a policy requested for + * a key that already exists would be silently dropped while the call still + * reported success — including a `variable -> secret` remediation, the case + * where a false success is most harmful. Flipping an existing key needs the + * stricter disclosure gate this path does not perform, so surface the mismatch + * rather than pretend it applied. + * + * `existingKeys` is the stored ciphertext map read under the caller's advisory + * lock: a key present there is not created by this call, so its policy is + * whatever it already had. Callers must invoke this BEFORE writing anything. + */ +async function assertRequestedVisibilityApplies(params: { + workspaceId: string + actingUserId: string + updatedKeys: string[] + existingKeys: Record + visibilityByKey?: Record +}): Promise { + const { workspaceId, actingUserId, updatedKeys, existingKeys, visibilityByKey } = params + if (!visibilityByKey) return + + const requested = Object.fromEntries( + updatedKeys + .filter((key) => key in existingKeys && visibilityByKey[key]) + .map((key) => [key, visibilityByKey[key] as EnvVisibility]) + ) + if (Object.keys(requested).length === 0) return + + const { variableKeys } = await getWorkspaceEnvKeyAdminAccess({ + workspaceId, + envKeys: Object.keys(requested), + userId: actingUserId, + }) + const unapplied = Object.entries(requested) + .filter(([key, next]) => (variableKeys.has(key) ? 'variable' : 'secret') !== next) + .map(([key]) => key) + if (unapplied.length > 0) { + throw new WorkspaceEnvVisibilityUnsupportedError(unapplied) + } +} + export async function upsertWorkspaceEnvVars( workspaceId: string, newVars: Record, @@ -461,6 +505,20 @@ export async function upsertWorkspaceEnvVars( const existing = (existingRow?.variables as Record) || {} const merged = { ...existing, ...newlyEncrypted } + // Rejected BEFORE the write, inside the same transaction that reads + // `existing` — which is what makes "already exists" exact rather than a + // guess taken outside the lock. Validating after the upsert (and after + // createWorkspaceEnvCredentials) meant a rejected call still changed + // workspace values and could mint credential rows, then threw and skipped + // its audit record. + await assertRequestedVisibilityApplies({ + workspaceId, + actingUserId, + updatedKeys, + existingKeys: existing, + visibilityByKey: options?.visibilityByKey, + }) + await tx .insert(workspaceEnvironment) .values({ @@ -490,34 +548,6 @@ export async function upsertWorkspaceEnvVars( visibilityByKey: options?.visibilityByKey, }) - // `visibilityByKey` only reaches credential CREATION, so a requested policy on - // a key that already exists would be silently dropped while the call still - // reported success — including a `variable -> secret` remediation, the case - // where a false success is most harmful. Flipping an existing key needs the - // stricter disclosure gate this path does not perform, so surface the mismatch - // rather than pretending it applied. - if (options?.visibilityByKey) { - const existingKeys = updatedKeys.filter((key) => !newKeys.includes(key)) - const requested = Object.fromEntries( - existingKeys - .filter((key) => options.visibilityByKey?.[key]) - .map((key) => [key, options.visibilityByKey?.[key] as EnvVisibility]) - ) - if (Object.keys(requested).length > 0) { - const { variableKeys } = await getWorkspaceEnvKeyAdminAccess({ - workspaceId, - envKeys: Object.keys(requested), - userId: actingUserId, - }) - const unapplied = Object.entries(requested) - .filter(([key, next]) => (variableKeys.has(key) ? 'variable' : 'secret') !== next) - .map(([key]) => key) - if (unapplied.length > 0) { - throw new WorkspaceEnvVisibilityUnsupportedError(unapplied) - } - } - } - recordAudit({ workspaceId, actorId: actingUserId, From 40c5188e836c786242f92fb62e04db8ae7b9b12f Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 15:01:08 -0700 Subject: [PATCH 10/14] fix(secrets): lock the org-admin grant too, and read auth on the tx The revocation locks covered the explicit workspace permission and the per-key credential membership, but workspace admin can also be INHERITED from an organization admin role. That grant lives in a `member` row no lock covered, so revoking it could still race the disclosure update. Locks now cover all three grants. Both authorization reads also go through the caller's transaction rather than the global client, so the decision runs on the same connection holding those locks instead of alongside them on a pooled one. --- apps/sim/lib/credentials/environment.ts | 75 +++++++++++++++++++------ 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 9e9508e8cda..5ea97b3034a 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -3,6 +3,7 @@ import { credential, type credentialEnvVisibilityEnum, credentialMember, + member, permissions, workspace, } from '@sim/db/schema' @@ -197,14 +198,20 @@ export async function getWorkspaceEnvKeyAdminAccess(params: { workspaceId: string envKeys: string[] userId: string + /** + * Runs the read on the caller's transaction. Disclosure authorization passes + * one so this read happens on the same connection that holds its revocation + * locks, rather than on a pooled connection alongside them. + */ + executor?: DbOrTx }): Promise { - const { workspaceId, envKeys, userId } = params + const { workspaceId, envKeys, userId, executor = db } = params const keys = Array.from(new Set(envKeys.filter(Boolean))) if (keys.length === 0) { return { adminKeys: new Set(), knownKeys: new Set(), variableKeys: new Set() } } - const rows = await db + const rows = await executor .select({ envKey: credential.envKey, envVisibility: credential.envVisibility, @@ -563,17 +570,33 @@ async function authorizeWorkspaceEnvVisibilityChange(params: { const changingKeys = changing.map((row) => row.envKey as string) - // Share-lock the rows that grant this caller the right to disclose, BEFORE - // reading them. A concurrent revocation must either commit before the lock — - // in which case the reads below observe it and deny — or block until this - // transaction ends. Without the locks the permission read and the UPDATE are - // two independent statements, and a revocation landing between them leaves a - // former admin able to flip a secret to a workspace-visible variable. - // - // `executor` is the caller's transaction, which is what makes the locks span - // the UPDATE. Called without one, each statement is its own implicit - // transaction and the locks release immediately — harmless, but it is why - // `setWorkspaceEnvVisibility` is the only supported entry point. + const [workspaceRow] = await executor + .select({ organizationId: workspace.organizationId }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .limit(1) + const organizationId = workspaceRow?.organizationId ?? null + + /** + * Share-lock every row that can grant this caller the right to disclose, + * BEFORE reading any of them, so a concurrent revocation either commits + * first — and the reads below observe it and deny — or blocks until this + * transaction ends. Since the UPDATE is in the same transaction, that leaves + * no window between deciding and disclosing. + * + * All three grants must be covered or the weakest one decides: + * - the explicit workspace `permissions` row, + * - the `member` row, because org admin INHERITS workspace admin + * (`resolveEffectiveWorkspacePermission`) — locking only `permissions` + * leaves an org-admin revocation free to race, + * - the `credential_member` rows for the credentials being changed, which + * is the per-key grant (and whose `status` flip is also a revocation). + * + * `executor` must be the caller's transaction for any of this to hold. + * Called without one, each statement is its own implicit transaction and the + * locks release immediately — which is why both this and the apply half are + * unexported and `setWorkspaceEnvVisibility` is the only entry point. + */ await executor .select({ id: permissions.id }) .from(permissions) @@ -585,6 +608,13 @@ async function authorizeWorkspaceEnvVisibilityChange(params: { ) ) .for('share') + if (organizationId) { + await executor + .select({ id: member.id }) + .from(member) + .where(and(eq(member.userId, actingUserId), eq(member.organizationId, organizationId))) + .for('share') + } await executor .select({ id: credentialMember.id }) .from(credentialMember) @@ -599,10 +629,21 @@ async function authorizeWorkspaceEnvVisibilityChange(params: { ) .for('share') - const [isWorkspaceAdmin, { adminKeys }] = await Promise.all([ - hasWorkspaceAdminAccess(actingUserId, workspaceId), - getWorkspaceEnvKeyAdminAccess({ workspaceId, envKeys: changingKeys, userId: actingUserId }), - ]) + // Both reads go through `executor` — the same connection holding the locks + // above. Sequential rather than Promise.all: a transaction handle is one + // connection, so concurrent statements on it serialize anyway. + const permission = await getEffectiveWorkspacePermission( + actingUserId, + { id: workspaceId, organizationId }, + executor + ) + const isWorkspaceAdmin = permissionSatisfies(permission, 'admin') + const { adminKeys } = await getWorkspaceEnvKeyAdminAccess({ + workspaceId, + envKeys: changingKeys, + userId: actingUserId, + executor, + }) const forbidden = isWorkspaceAdmin ? [] : changingKeys.filter((key) => !adminKeys.has(key)) if (forbidden.length > 0) { From 2e771db200e121596fb6cab6f904d60d8ec02ded Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 15:08:49 -0700 Subject: [PATCH 11/14] fix(secrets): show a renamed variable as a variable before save The rename carried visibility into the save payload but not into the render, which still resolved disclosure from server state only. The new name has no server visibility yet, so the row defaulted to secret: it jumped into Workspace secrets and bullet-masked a value the member can plainly read, until save landed. --- .../components/secrets-manager/secrets-manager.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx index 2e1ccb25e86..21b637a3a2e 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx @@ -904,9 +904,19 @@ export function SecretsManager() { return entries.map(([key, value]) => ({ key, value, - visibility: visibilityMap[key] ?? ('secret' as EnvVisibility), + // A pending rename is checked FIRST: the new name has no server-side + // visibility yet, so falling straight through to `visibilityMap` would + // default it to `secret` — bouncing the row into Workspace secrets and + // bullet-masking a value the member can plainly read, until save lands. + visibility: renamedKeyVisibility[key] ?? visibilityMap[key] ?? ('secret' as EnvVisibility), })) - }, [searchTerm, filteredWorkspaceEntries, workspaceVars, workspaceEnvData?.visibility]) + }, [ + searchTerm, + filteredWorkspaceEntries, + workspaceVars, + workspaceEnvData?.visibility, + renamedKeyVisibility, + ]) /** * Renders one workspace section. Both sections share the row component and From 626d845fdf2e1848ce31eb18d1f0e6480db13a87 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 15:16:13 -0700 Subject: [PATCH 12/14] fix(secrets): let server visibility outrank the carried rename The carried value won over the server map, so a rename-back (or a rename onto an existing key) left an entry shadowing a key that still has a credential: a later confirmed convert-to-secret would apply server-side while the row stayed under Workspace variables and kept rendering unmasked until refresh. Server state now wins wherever it exists and the carried value only fills the gap where it doesn't, which is all it was ever for. A stale entry is inert under that ordering rather than needing to be cleared on every path that could invalidate it. --- .../secrets-manager/secrets-manager.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx index 21b637a3a2e..28c162f9f77 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx @@ -904,11 +904,17 @@ export function SecretsManager() { return entries.map(([key, value]) => ({ key, value, - // A pending rename is checked FIRST: the new name has no server-side - // visibility yet, so falling straight through to `visibilityMap` would - // default it to `secret` — bouncing the row into Workspace secrets and - // bullet-masking a value the member can plainly read, until save lands. - visibility: renamedKeyVisibility[key] ?? visibilityMap[key] ?? ('secret' as EnvVisibility), + // Server state wins wherever it exists; the carried rename only fills the + // gap where it doesn't. That ordering is the whole contract: a renamed key + // has no server visibility yet, so without the fallback it would default + // to `secret` and bounce into Workspace secrets, bullet-masking a value + // the member can plainly read. But letting the carried value WIN would + // shadow the server after a rename-back or a rename onto an existing key, + // so a later confirmed convert-to-secret would apply server-side and the + // row would still render as non-secret. Under this ordering a stale entry + // is inert, rather than needing to be cleared on every path that could + // invalidate it. + visibility: visibilityMap[key] ?? renamedKeyVisibility[key] ?? ('secret' as EnvVisibility), })) }, [ searchTerm, From 44f331aae8c194db563bb5ada190c7ce4cb873fc Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 15:23:12 -0700 Subject: [PATCH 13/14] fix(secrets): never send carried rename visibility for a known key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The render treated a stale carried entry as inert, but handleSave still spread the raw map into the upsert payload — and since the PUT now applies visibility to existing keys, that entry could flip a real key. A rename-back let the next unrelated value save revert a confirmed convert-to-secret, and a rename onto an existing secret could turn it into a variable with no confirm dialog at all. Render and save now read one narrowed derivation, restricted to keys the server has no visibility for, so a carried value can only ever describe the new name it was created for. Sharing the derivation is the point: the last two defects were the two halves disagreeing. --- .../secrets-manager/secrets-manager.tsx | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx index 28c162f9f77..8205c358de7 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx @@ -898,30 +898,47 @@ export function SecretsManager() { * each section can take its own slice. Derived once rather than filtered twice * so the two sections cannot disagree about which kind a key is. */ + /** + * The carried rename visibility, narrowed to keys the server has no opinion + * about — the single source both the render and the save payload read. + * + * The narrowing is the safety property, not an optimization. A rename-back, + * or a rename onto a key that already exists, leaves an entry for a key that + * still has a credential; sending that in the save payload would flip a real + * key's disclosure. Since the PUT now applies visibility to existing keys, + * that could revert a confirmed convert-to-secret on the next unrelated value + * save, or turn an existing secret into a variable with no confirm dialog at + * all. Restricted to keys the server does not know, the carried value can + * only ever describe the new name it was created for. + */ + const carriedRenameVisibility = useMemo(() => { + const serverVisibility = workspaceEnvData?.visibility ?? {} + const carried: Record = {} + for (const [key, visibility] of Object.entries(renamedKeyVisibility)) { + if (!(key in serverVisibility)) carried[key] = visibility + } + return carried + }, [renamedKeyVisibility, workspaceEnvData?.visibility]) + const workspaceEntriesForRender = useMemo(() => { const entries = searchTerm.trim() ? filteredWorkspaceEntries : Object.entries(workspaceVars) const visibilityMap = workspaceEnvData?.visibility ?? {} return entries.map(([key, value]) => ({ key, value, - // Server state wins wherever it exists; the carried rename only fills the - // gap where it doesn't. That ordering is the whole contract: a renamed key - // has no server visibility yet, so without the fallback it would default - // to `secret` and bounce into Workspace secrets, bullet-masking a value - // the member can plainly read. But letting the carried value WIN would - // shadow the server after a rename-back or a rename onto an existing key, - // so a later confirmed convert-to-secret would apply server-side and the - // row would still render as non-secret. Under this ordering a stale entry - // is inert, rather than needing to be cleared on every path that could - // invalidate it. - visibility: visibilityMap[key] ?? renamedKeyVisibility[key] ?? ('secret' as EnvVisibility), + // A renamed key has no server visibility yet, so without the carried + // fallback it would default to `secret` and bounce into Workspace + // secrets, bullet-masking a value the member can plainly read. The + // carried map is already narrowed to keys the server does not know, so + // the two can never disagree about a key that exists. + visibility: visibilityMap[key] ?? carriedRenameVisibility[key] ?? ('secret' as EnvVisibility), })) }, [ searchTerm, filteredWorkspaceEntries, workspaceVars, workspaceEnvData?.visibility, - renamedKeyVisibility, + carriedRenameVisibility, ]) /** @@ -1004,12 +1021,13 @@ export function SecretsManager() { mergedWorkspaceVars[row.key] = row.value } } - // Names drafted in the Variables section. Only NEW keys can take a - // visibility here — the server never flips an existing key through this - // path (that needs the confirmed disclosure flow), which is why - // `duplicateDraftKeys` blocks saving a name that already exists as a secret - // rather than letting it save and silently stay secret. - const draftVariableVisibility: Record = { ...renamedKeyVisibility } + // Names drafted in the Variables section, plus any carried across a rename. + // Both only ever describe keys the server does not already know: the PUT + // does apply visibility to existing keys, so an entry for an existing key + // would flip its disclosure with no confirm dialog. `duplicateDraftKeys` + // blocks a draft name that already exists, and `carriedRenameVisibility` is + // narrowed to unknown keys for the same reason. + const draftVariableVisibility: Record = { ...carriedRenameVisibility } for (const row of newWorkspaceVariableRows) { if (row.key && row.value) { mergedWorkspaceVars[row.key] = row.value From d991a08e8fffa4031c257c16021a1973a3a09ca6 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Fri, 7 Aug 2026 15:23:57 -0700 Subject: [PATCH 14/14] fix(db): add the visibility constraint NOT VALID, then validate check:migrations blocks a plain ADD CONSTRAINT CHECK on an existing table: it takes an ACCESS EXCLUSIVE lock for a full scan of credential and rejects concurrent writes for the duration. Split into the expand/contract form so the lock is brief and validation runs under SHARE UPDATE EXCLUSIVE, which readers and writers do not block on. --- packages/db/migrations/0285_brainy_mauler.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/db/migrations/0285_brainy_mauler.sql b/packages/db/migrations/0285_brainy_mauler.sql index cfbd8f5496d..beb314b2854 100644 --- a/packages/db/migrations/0285_brainy_mauler.sql +++ b/packages/db/migrations/0285_brainy_mauler.sql @@ -1,3 +1,4 @@ CREATE TYPE "public"."credential_env_visibility" AS ENUM('secret', 'variable');--> statement-breakpoint ALTER TABLE "credential" ADD COLUMN "env_visibility" "credential_env_visibility" DEFAULT 'secret' NOT NULL;--> statement-breakpoint -ALTER TABLE "credential" ADD CONSTRAINT "credential_env_visibility_scope_check" CHECK ((env_visibility = 'secret') OR (type = 'env_workspace')); \ No newline at end of file +ALTER TABLE "credential" ADD CONSTRAINT "credential_env_visibility_scope_check" CHECK ((env_visibility = 'secret') OR (type = 'env_workspace')) NOT VALID;--> statement-breakpoint +ALTER TABLE "credential" VALIDATE CONSTRAINT "credential_env_visibility_scope_check"; \ No newline at end of file