chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import { customTools, knowledgeBase, knowledgeConnector, mcpServers } from '@sim/db/schema'
|
||||
import { and, eq, inArray, isNull } from 'drizzle-orm'
|
||||
import type { DbOrTx } from '@/lib/db/types'
|
||||
import {
|
||||
ENV_REF_PATTERN,
|
||||
type ForkReference,
|
||||
type ForkReferenceResolver,
|
||||
} from '@/ee/workspace-forking/lib/remap/remap-references'
|
||||
|
||||
function extractEnvKeys(text: string): string[] {
|
||||
const keys = new Set<string>()
|
||||
for (const match of text.matchAll(ENV_REF_PATTERN)) {
|
||||
if (match[1]) keys.add(match[1])
|
||||
}
|
||||
return Array.from(keys)
|
||||
}
|
||||
|
||||
export interface ForkCascadeResult {
|
||||
/** Transitive env-var / credential references discovered inside referenced resources. */
|
||||
references: ForkReference[]
|
||||
unmapped: ForkReference[]
|
||||
/** Source MCP server ids that use OAuth and need re-authorization in the target. */
|
||||
mcpReauthServerIds: string[]
|
||||
/** Human-readable descriptions of inline secrets that cannot be mapped (review-only). */
|
||||
inlineSecretSources: string[]
|
||||
}
|
||||
|
||||
const EMPTY: ForkCascadeResult = {
|
||||
references: [],
|
||||
unmapped: [],
|
||||
mcpReauthServerIds: [],
|
||||
inlineSecretSources: [],
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the bodies of resources a workflow references (custom tools, MCP servers,
|
||||
* knowledge bases) and surface the secrets they carry transitively: `{{ENV}}`
|
||||
* keys inside custom tool code and MCP url/headers, and credential ids on KB
|
||||
* connectors. These become additional required env-var / credential references
|
||||
* (validated for existence in the target via `resolve`). OAuth MCP servers and
|
||||
* inline connector keys are surfaced separately for review since they cannot be
|
||||
* id-mapped. Reads only the source workspace's resources.
|
||||
*/
|
||||
export async function detectForkCascadeReferences(params: {
|
||||
executor: DbOrTx
|
||||
sourceWorkspaceId: string
|
||||
references: ForkReference[]
|
||||
resolve: ForkReferenceResolver
|
||||
}): Promise<ForkCascadeResult> {
|
||||
const { executor, sourceWorkspaceId, references, resolve } = params
|
||||
|
||||
const customToolIds = new Set<string>()
|
||||
const mcpServerIds = new Set<string>()
|
||||
const knowledgeBaseIds = new Set<string>()
|
||||
for (const reference of references) {
|
||||
if (reference.kind === 'custom-tool') customToolIds.add(reference.sourceId)
|
||||
else if (reference.kind === 'mcp-server') mcpServerIds.add(reference.sourceId)
|
||||
else if (reference.kind === 'knowledge-base') knowledgeBaseIds.add(reference.sourceId)
|
||||
}
|
||||
|
||||
if (customToolIds.size === 0 && mcpServerIds.size === 0 && knowledgeBaseIds.size === 0) {
|
||||
return EMPTY
|
||||
}
|
||||
|
||||
const refs = new Map<string, ForkReference>()
|
||||
const unmapped = new Map<string, ForkReference>()
|
||||
const mcpReauthServerIds = new Set<string>()
|
||||
const inlineSecretSources: string[] = []
|
||||
|
||||
const recordEnv = (key: string, sourceLabel: string) => {
|
||||
const dedupeKey = `env-var:${key}`
|
||||
if (refs.has(dedupeKey)) return
|
||||
const reference: ForkReference = {
|
||||
kind: 'env-var',
|
||||
sourceId: key,
|
||||
subBlockKey: '(cascade)',
|
||||
blockName: sourceLabel,
|
||||
required: true,
|
||||
}
|
||||
refs.set(dedupeKey, reference)
|
||||
if (resolve('env-var', key) == null) unmapped.set(dedupeKey, reference)
|
||||
}
|
||||
|
||||
const recordCredential = (credentialId: string, sourceLabel: string) => {
|
||||
const dedupeKey = `credential:${credentialId}`
|
||||
if (refs.has(dedupeKey)) return
|
||||
const reference: ForkReference = {
|
||||
kind: 'credential',
|
||||
sourceId: credentialId,
|
||||
subBlockKey: '(cascade)',
|
||||
blockName: sourceLabel,
|
||||
required: true,
|
||||
}
|
||||
refs.set(dedupeKey, reference)
|
||||
if (resolve('credential', credentialId) == null) unmapped.set(dedupeKey, reference)
|
||||
}
|
||||
|
||||
if (customToolIds.size > 0) {
|
||||
const tools = await executor
|
||||
.select({ id: customTools.id, title: customTools.title, code: customTools.code })
|
||||
.from(customTools)
|
||||
.where(
|
||||
and(
|
||||
inArray(customTools.id, Array.from(customToolIds)),
|
||||
eq(customTools.workspaceId, sourceWorkspaceId)
|
||||
)
|
||||
)
|
||||
for (const tool of tools) {
|
||||
for (const key of extractEnvKeys(tool.code ?? '')) {
|
||||
recordEnv(key, `Custom tool: ${tool.title}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mcpServerIds.size > 0) {
|
||||
const servers = await executor
|
||||
.select({
|
||||
id: mcpServers.id,
|
||||
name: mcpServers.name,
|
||||
url: mcpServers.url,
|
||||
headers: mcpServers.headers,
|
||||
authType: mcpServers.authType,
|
||||
})
|
||||
.from(mcpServers)
|
||||
.where(
|
||||
and(
|
||||
inArray(mcpServers.id, Array.from(mcpServerIds)),
|
||||
eq(mcpServers.workspaceId, sourceWorkspaceId)
|
||||
)
|
||||
)
|
||||
for (const server of servers) {
|
||||
const label = `MCP server: ${server.name}`
|
||||
if (server.url) {
|
||||
for (const key of extractEnvKeys(server.url)) recordEnv(key, label)
|
||||
}
|
||||
const headers = (server.headers ?? {}) as Record<string, unknown>
|
||||
for (const [headerName, headerValue] of Object.entries(headers)) {
|
||||
if (typeof headerValue !== 'string') continue
|
||||
const keys = extractEnvKeys(headerValue)
|
||||
if (keys.length > 0) {
|
||||
for (const key of keys) recordEnv(key, label)
|
||||
} else if (server.authType === 'headers' && headerValue) {
|
||||
inlineSecretSources.push(`${label} (header ${headerName})`)
|
||||
}
|
||||
}
|
||||
if (server.authType === 'oauth') mcpReauthServerIds.add(server.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (knowledgeBaseIds.size > 0) {
|
||||
// Join to knowledgeBase + filter by the source workspace (defense-in-depth, matching the
|
||||
// customTools/mcpServers reads above): a connector is only cascaded when its KB actually
|
||||
// belongs to the source workspace, so a stale/crafted KB id can't pull in a foreign connector.
|
||||
const connectors = await executor
|
||||
.select({
|
||||
id: knowledgeConnector.id,
|
||||
knowledgeBaseId: knowledgeConnector.knowledgeBaseId,
|
||||
credentialId: knowledgeConnector.credentialId,
|
||||
encryptedApiKey: knowledgeConnector.encryptedApiKey,
|
||||
})
|
||||
.from(knowledgeConnector)
|
||||
.innerJoin(knowledgeBase, eq(knowledgeConnector.knowledgeBaseId, knowledgeBase.id))
|
||||
.where(
|
||||
and(
|
||||
inArray(knowledgeConnector.knowledgeBaseId, Array.from(knowledgeBaseIds)),
|
||||
eq(knowledgeBase.workspaceId, sourceWorkspaceId),
|
||||
isNull(knowledgeBase.deletedAt),
|
||||
isNull(knowledgeConnector.deletedAt)
|
||||
)
|
||||
)
|
||||
for (const connector of connectors) {
|
||||
if (connector.credentialId) {
|
||||
recordCredential(connector.credentialId, `Knowledge base connector`)
|
||||
} else if (connector.encryptedApiKey) {
|
||||
inlineSecretSources.push(`Knowledge base connector ${connector.id} (API key)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
references: Array.from(refs.values()),
|
||||
unmapped: Array.from(unmapped.values()),
|
||||
mcpReauthServerIds: Array.from(mcpReauthServerIds),
|
||||
inlineSecretSources,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user