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..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,11 +9,39 @@ const { mockGetWorkspaceById, mockGetUserEntityPermissions, mockGetWorkspaceEnvKeyAdminAccess, + mockSetVisibility, + mockCreateWorkspaceEnvCredentials, + mockRecordAudit, + MockVisibilityAccessError, } = vi.hoisted(() => ({ mockGetPersonalEnvKeyRawAccess: vi.fn(), mockGetWorkspaceById: vi.fn(), mockGetUserEntityPermissions: vi.fn(), mockGetWorkspaceEnvKeyAdminAccess: 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. + 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/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', () => ({ @@ -26,11 +54,13 @@ const mockGetPersonalAndWorkspaceEnv = environmentUtilsMockFns.mockGetPersonalAn vi.mock('@/lib/credentials/environment', () => ({ getPersonalEnvKeyRawAccess: mockGetPersonalEnvKeyRawAccess, getWorkspaceEnvKeyAdminAccess: mockGetWorkspaceEnvKeyAdminAccess, - createWorkspaceEnvCredentials: vi.fn(), + createWorkspaceEnvCredentials: mockCreateWorkspaceEnvCredentials, deleteWorkspaceEnvCredentials: vi.fn(), + setWorkspaceEnvVisibility: mockSetVisibility, + 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 @@ -56,6 +86,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 +109,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 +120,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 +176,7 @@ describe('GET /api/workspaces/[id]/environment', () => { mockGetWorkspaceEnvKeyAdminAccess.mockResolvedValue({ adminKeys: new Set(), knownKeys: new Set(), + variableKeys: new Set(), }) const { body } = await callGet() @@ -119,6 +190,7 @@ describe('GET /api/workspaces/[id]/environment', () => { mockGetWorkspaceEnvKeyAdminAccess.mockResolvedValue({ adminKeys: new Set(), knownKeys: new Set(), + variableKeys: new Set(), }) const { body } = await callGet() @@ -132,6 +204,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 +217,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']), @@ -158,3 +232,75 @@ 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(), + }) + mockSetVisibility.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() } + } + + /** + * 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('returns 403 and records no audit when a visibility change is denied', async () => { + mockSetVisibility.mockRejectedValue(new MockVisibilityAccessError(['STRIPE_KEY'])) + + const { status, body } = await callPut({ + variables: { BRAND_NEW: 'v' }, + visibility: { STRIPE_KEY: 'variable' }, + }) + + expect(status).toBe(403) + 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() + }) + + /** + * 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: { NEW_KEY: 'v' }, + visibility: { SUPPORT_EMAIL: 'variable' }, + }) + + expect(status).toBe(200) + 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(), + }) + ) + // 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 98194c979b0..0dc789d3687 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({ @@ -237,44 +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))`) + /** + * 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() }, + }) + } - const [existingRow] = await tx - .select() - .from(workspaceEnvironment) - .where(eq(workspaceEnvironment.workspaceId, workspaceId)) - .limit(1) + // 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, + newKeys: Object.keys(variables).filter((k) => !(k in existing)), + actingUserId: userId, + visibilityByKey: visibility, + executor: tx, + }) - const existing = ((existingRow?.variables as Record) ?? {}) as Record< - string, - string - > - const mergedVars = { ...existing, ...encryptedIncoming } + if (visibility) { + const applied = await setWorkspaceEnvVisibility({ + workspaceId, + updates: visibility, + actingUserId: userId, + executor: tx, + }) + flippedKeys = applied.changedKeys + } - await tx - .insert(workspaceEnvironment) - .values({ - id: generateId(), + return { existingEncrypted: existing, merged: mergedVars } + })) + } catch (error) { + if (error instanceof WorkspaceEnvVisibilityAccessError) { + logger.warn(`[${requestId}] Workspace env visibility change denied`, { workspaceId, - variables: mergedVars, - createdAt: new Date(), - updatedAt: new Date(), + userId, + keys: error.keys, }) - .onConflictDoUpdate({ - target: [workspaceEnvironment.workspaceId], - set: { variables: mergedVars, updatedAt: new Date() }, - }) - - return { existingEncrypted: existing, merged: mergedVars } - }) + // Nothing to undo: the denial rolled the whole transaction back. + return NextResponse.json({ error: error.message }, { status: 403 }) + } + throw error + } invalidateEffectiveDecryptedEnvCache({ workspaceId }) - const newKeys = Object.keys(variables).filter((k) => !(k in existingEncrypted)) - await createWorkspaceEnvCredentials({ workspaceId, newKeys, actingUserId: userId }) recordAudit({ workspaceId, @@ -284,11 +356,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..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 @@ -1,19 +1,20 @@ 'use client' import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' -import { ChipInput, cn, toast } from '@sim/emcn' +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' 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 +53,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 +220,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 +240,8 @@ function WorkspaceVariableRow({ onValueChange, onDelete, onViewDetails, + visibility, + onToggleVisibility, }: WorkspaceVariableRowProps) { /** * Salts the generated `name` attributes so password managers can't match them @@ -250,12 +276,24 @@ function WorkspaceVariableRow({ value={value} onChange={(next) => onValueChange(envKey, next)} canEdit={canEdit} + visibility={visibility} name={`workspace_env_value_${envKey}_${autofillSalt}`} /> + {/* + 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 + } /> ) @@ -352,19 +390,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() @@ -387,14 +414,33 @@ 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>({}) 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 + next: EnvVisibility + } | null>(null) const initialWorkspaceVarsRef = useRef>({}) const scrollContainerRef = useRef(null) const initialVarsRef = useRef([]) @@ -412,6 +458,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(getErrorMessage(error, 'Failed to change visibility')) + } + }, [pendingVisibilityChange, workspaceId, upsertWorkspaceMutation]) + const filteredEnvVars = useMemo(() => { const mapped = envVars.map((envVar, index) => ({ envVar, originalIndex: index })) if (!searchTerm.trim()) return mapped @@ -426,6 +495,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 @@ -433,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) @@ -468,19 +550,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 || @@ -653,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) => { @@ -671,6 +786,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)) } @@ -767,11 +886,128 @@ export function SecretsManager() { setEnvVars(structuredClone(initialVarsRef.current)) setWorkspaceVars({ ...initialWorkspaceVarsRef.current }) setNewWorkspaceRows([createEmptyEnvVar()]) + setNewWorkspaceVariableRows([createEmptyEnvVar()]) + setRenamedKeyVisibility({}) 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. + */ + /** + * 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, + // 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, + carriedRenameVisibility, + ]) + + /** + * 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 @@ -785,6 +1021,19 @@ export function SecretsManager() { mergedWorkspaceVars[row.key] = row.value } } + // 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 + draftVariableVisibility[row.key] = 'variable' + } + } const validVariables = envVars .filter((v) => v.key && v.value) @@ -828,7 +1077,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 }) @@ -849,7 +1107,14 @@ 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()]) + // 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') } @@ -987,67 +1252,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 ( - - ) - })} - {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 }) => (
@@ -1072,6 +1310,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..4f53e2379e4 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,180 @@ 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([]) + }) + + /** + * 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: {}, + 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.test.ts b/apps/sim/lib/copilot/chat/workspace-context.test.ts index 3e090f01943..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,39 @@ 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', () => { + 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 =') + + // 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' }, + ]) + }) + + 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') + // 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 d2144ab844a..d87569cddb9 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' @@ -78,6 +79,20 @@ export interface WorkspaceMdData { role?: string | null }> envVariables: string[] + /** + * 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?: 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 }> @@ -103,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) @@ -261,8 +294,18 @@ 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 nonSecretValues = new Map( + (data.nonSecretEnvVariables ?? []).map((v) => [v.name, v.value]) + ) + const lines = [...data.envVariables] + .sort(stableCompare) + .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')}` + ) } if (data.customTools && data.customTools.length > 0) { @@ -356,6 +399,7 @@ async function buildWorkspaceMdData( mcpServerRows, skillRows, customBlockSummaries, + envSnapshot, sandboxResult, ] = await Promise.all([ getUsersWithPermissions(workspaceId), @@ -431,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) : [], @@ -513,6 +562,22 @@ async function buildWorkspaceMdData( envVariables: [...new Set(envCredentials.map((credential) => credential.envKey))].sort( stableCompare ), + // 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) + .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, @@ -557,6 +622,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 shown with a + // value in the inventory while being absent from the list above. + nonSecretEnvVariables: filterNonSecretEnvVarsByMountPolicy( + data.nonSecretEnvVariables ?? [], + options?.secretMountPolicy + ), }) } @@ -631,7 +703,17 @@ export function buildVfsSnapshot(data: WorkspaceMdData): VfsSnapshotV1 { ...(c.displayName ? { displayName: c.displayName } : {}), ...(c.role ? { role: c.role } : {}), })), - envVars: data.envVariables, + // 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/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/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 9c996be921a..068c0285619 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 e7745482158..ffdbd73870b 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..938b59dc401 100644 --- a/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts +++ b/apps/sim/lib/copilot/generated/vfs-snapshot-v1.ts @@ -7,10 +7,9 @@ export interface VfsSnapshotV1 { customBlocks?: VfsSnapshotV1CustomBlock[] customTools?: VfsSnapshotV1NamedResource[] - envVars?: string[] + envVars?: VfsSnapshotV1EnvVar[] files?: VfsSnapshotV1File[] integrations?: VfsSnapshotV1Integration[] - jobs?: VfsSnapshotV1Job[] knowledgeBases?: VfsSnapshotV1KnowledgeBase[] mcpServers?: VfsSnapshotV1McpServer[] members?: VfsSnapshotV1Member[] @@ -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". @@ -59,19 +66,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". 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/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/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..cfa5ccffe9f 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,20 @@ export class WorkspaceVFS { role: c.role, })), envVariables: envKeys, + // 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', { 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..5ea97b3034a 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -1,5 +1,12 @@ import { db } from '@sim/db' -import { credential, credentialMember, permissions, workspace } from '@sim/db/schema' +import { + credential, + type credentialEnvVisibilityEnum, + credentialMember, + member, + 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' @@ -7,6 +14,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' @@ -24,14 +32,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))), @@ -100,11 +111,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 { @@ -178,14 +198,23 @@ 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() } + 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, role: credentialMember.role, status: credentialMember.status, }) @@ -204,18 +233,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,18 +417,27 @@ 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 + /** + * 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 } = 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) => ({ @@ -405,6 +446,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, @@ -431,7 +473,7 @@ export async function createWorkspaceEnvCredentials(params: { })) ) - await db.insert(credentialMember).values(membershipValues).onConflictDoNothing() + await executor.insert(credentialMember).values(membershipValues).onConflictDoNothing() } /** @@ -457,6 +499,220 @@ 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 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. + */ +async function authorizeWorkspaceEnvVisibilityChange(params: { + workspaceId: string + updates: Record + actingUserId: string + executor?: DbOrTx +}): Promise { + const { workspaceId, updates, actingUserId, executor = db } = params + const requestedKeys = Object.keys(updates).filter(Boolean) + if (requestedKeys.length === 0) return [] + + 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 [] + + const changingKeys = changing.map((row) => row.envKey as string) + + 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) + .where( + and( + eq(permissions.userId, actingUserId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) + ) + .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) + .where( + and( + eq(credentialMember.userId, actingUserId), + inArray( + credentialMember.credentialId, + changing.map((row) => row.id) + ) + ) + ) + .for('share') + + // 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) { + 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. + * + * 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. + */ +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 change of changes) { + await executor + .update(credential) + .set({ envVisibility: change.next, updatedAt: now }) + .where(eq(credential.id, change.credentialId)) + } + return { changedKeys: changes.map((change) => change.envKey) } +} + +/** + * 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 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 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 + 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: { userId: string envKeys: string[] @@ -561,16 +817,35 @@ 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({ type: credential.type, envKey: credential.envKey, envOwnerUserId: credential.envOwnerUserId, + envVisibility: credential.envVisibility, updatedAt: credential.updatedAt, }) .from(credential) @@ -589,6 +864,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 — 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 ) ) @@ -603,6 +890,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..e16d385cc29 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, @@ -32,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( @@ -51,6 +66,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 +104,7 @@ function cloneEnvironmentResolutionSnapshot( personalOwners: { ...snapshot.personalOwners }, conflicts: [...snapshot.conflicts], decryptionFailures: [...snapshot.decryptionFailures], + workspaceVariableKeys: [...snapshot.workspaceVariableKeys], } } @@ -160,7 +186,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([]), ]) @@ -262,6 +291,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 +315,7 @@ export async function getPersonalAndWorkspaceEnv( personalOwners, conflicts, decryptionFailures, + workspaceVariableKeys, } } @@ -358,10 +395,55 @@ 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, - actingUserId: string + actingUserId: string, + options?: { visibilityByKey?: Record } ): Promise { const updatedKeys = Object.keys(newVars) if (updatedKeys.length === 0) return [] @@ -423,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({ @@ -445,7 +541,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/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) +}) 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..beb314b2854 --- /dev/null +++ b/packages/db/migrations/0285_brainy_mauler.sql @@ -0,0 +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')) NOT VALID;--> statement-breakpoint +ALTER TABLE "credential" VALIDATE CONSTRAINT "credential_env_visibility_scope_check"; \ 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: [], } } 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)