Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (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 / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

484 lines
16 KiB
TypeScript

/**
* Non-secret deployment-capability status derived from the same definitions the
* application uses at runtime.
*
* @packageDocumentation
*/
import {
ASYNC_JOBS_CAPABILITY,
CACHE_CAPABILITY,
EMAIL_CAPABILITY,
type EnvCapabilityConfigurationError,
type EnvCapabilityValues,
getCapabilityConfigurationError,
hasEnvCapabilityValue,
inspectCapability,
inspectOAuthClientCapability,
isTruthyEnvCapabilityValue,
LLM_KEY_POOLS,
OAUTH_CLIENT_CAPABILITIES,
type OAuthClientCapabilityId,
OCR_CAPABILITY,
type ProviderConfigurationState,
type ProviderInspection,
SANDBOX_CAPABILITY,
STORAGE_CAPABILITY,
} from '../../apps/sim/lib/core/config/env-capabilities.ts'
import { SETUP_FEATURES, type SetupFeatureId } from './capability-config.ts'
export type SetupStatusFeatureId = Exclude<SetupFeatureId, 'integration'>
export type CapabilityStatusState = 'default' | 'configured' | 'missing' | 'partial' | 'invalid'
export interface CapabilityStatusIssue {
state: Extract<CapabilityStatusState, 'missing' | 'partial' | 'invalid'>
message: string
}
interface FeatureStatusBase<TId extends SetupStatusFeatureId> {
id: TId
label: string
setupCommand: `bun run setup ${TId}`
state: CapabilityStatusState
issue?: CapabilityStatusIssue
}
type EmailProviderId = (typeof EMAIL_CAPABILITY.providers)[number]['id']
type StorageProviderId =
| (typeof STORAGE_CAPABILITY)['defaultProvider']['id']
| (typeof STORAGE_CAPABILITY.providers)[number]['id']
type LlmKeyPoolId = keyof typeof LLM_KEY_POOLS
export interface EmailCapabilityStatus extends FeatureStatusBase<'email'> {
strategy: 'fallback'
providerIds: readonly EmailProviderId[]
providers: readonly ProviderInspection<EmailProviderId>[]
}
export interface StorageCapabilityStatus extends FeatureStatusBase<'storage'> {
strategy: 'selected'
providerId: StorageProviderId | null
defaultProviderId: (typeof STORAGE_CAPABILITY)['defaultProvider']['id']
providers: readonly ProviderInspection<(typeof STORAGE_CAPABILITY.providers)[number]['id']>[]
}
export interface SandboxCapabilityStatus extends FeatureStatusBase<'sandbox'> {
providerId: 'disabled' | 'e2b' | 'daytona' | null
}
export interface JobsCapabilityStatus extends FeatureStatusBase<'jobs'> {
providerId: 'database' | 'trigger-dev'
}
export interface CacheCapabilityStatus extends FeatureStatusBase<'cache'> {
providerId: 'database' | 'redis'
}
export interface KnowledgeCapabilityStatus extends FeatureStatusBase<'knowledge'> {
providerId: 'local' | 'mistral' | 'azure-mistral' | null
}
export interface LlmKeyPoolStatus {
id: LlmKeyPoolId
state: 'configured' | 'missing'
configuredKeyCount: number
effectiveKeyCount: number
fallbackKeyConfigured: boolean
}
export interface LlmCapabilityStatus extends FeatureStatusBase<'llm'> {
pools: Readonly<Record<LlmKeyPoolId, LlmKeyPoolStatus>>
configuredPoolCount: number
configuredKeyCount: number
effectiveKeyCount: number
}
interface FeatureStatusById {
email: EmailCapabilityStatus
storage: StorageCapabilityStatus
sandbox: SandboxCapabilityStatus
jobs: JobsCapabilityStatus
cache: CacheCapabilityStatus
knowledge: KnowledgeCapabilityStatus
llm: LlmCapabilityStatus
}
export type EnvCapabilityFeatureStatuses = Pick<FeatureStatusById, SetupStatusFeatureId>
export interface OAuthClientStatus {
id: OAuthClientCapabilityId
state: ProviderConfigurationState
configuredFieldCount: number
requiredFieldCount: number
missingFields: readonly string[]
setupCommand: string
}
export interface OAuthClientStatuses {
clients: Readonly<Record<OAuthClientCapabilityId, OAuthClientStatus>>
readyCount: number
partialCount: number
absentCount: number
invalidCount: number
}
export interface EnvCapabilityStatusSnapshot {
features: EnvCapabilityFeatureStatuses
oauthClients: OAuthClientStatuses
}
function featureMetadata<TId extends SetupStatusFeatureId>(id: TId) {
const definition = SETUP_FEATURES.find((feature) => feature.id === id)
if (!definition) throw new Error(`Missing setup feature definition for ${id}`)
return {
id,
label: definition.label,
setupCommand: `bun run setup ${id}` as const,
}
}
function readConfiguredString(values: EnvCapabilityValues, key: string): string | null {
if (!hasEnvCapabilityValue(values, key)) return null
const value =
values instanceof Map ? values.get(key) : (values as Readonly<Record<string, unknown>>)[key]
return String(value).trim().toLowerCase()
}
function issue(
state: CapabilityStatusIssue['state'],
error: EnvCapabilityConfigurationError,
safeMessage = error.message
): CapabilityStatusIssue {
return { state, message: safeMessage }
}
function brokenProviderState(
providers: readonly ProviderInspection[]
): Extract<CapabilityStatusState, 'partial' | 'invalid'> | null {
if (providers.some((provider) => provider.state === 'invalid')) return 'invalid'
if (providers.some((provider) => provider.state === 'partial')) return 'partial'
return null
}
function inspectEmail(values: EnvCapabilityValues): EmailCapabilityStatus {
const inspection = inspectCapability(EMAIL_CAPABILITY, values)
const brokenState = brokenProviderState(inspection.providers)
const state = inspection.configured ? 'configured' : (brokenState ?? 'missing')
const configurationError =
inspection.error ?? getCapabilityConfigurationError(EMAIL_CAPABILITY, inspection.providers)
return {
...featureMetadata('email'),
strategy: 'fallback',
state,
providerIds: inspection.providerIds,
providers: inspection.providers,
...(configurationError ? { issue: issue(brokenState ?? 'invalid', configurationError) } : {}),
}
}
function inspectStorage(values: EnvCapabilityValues): StorageCapabilityStatus {
const inspection = inspectCapability(STORAGE_CAPABILITY, values)
if (!inspection.error && inspection.providerId) {
const providerId = inspection.providerId as StorageProviderId
return {
...featureMetadata('storage'),
strategy: 'selected',
state: providerId === STORAGE_CAPABILITY.defaultProvider.id ? 'default' : 'configured',
providerId,
defaultProviderId: STORAGE_CAPABILITY.defaultProvider.id,
providers: inspection.providers,
}
}
const selectedId = readConfiguredString(values, STORAGE_CAPABILITY.selectorKey)
const selected = inspection.providers.find((provider) => provider.id === selectedId)
const state =
selected && !selected.active
? 'missing'
: selected?.state === 'partial'
? 'partial'
: selected?.state === 'invalid'
? 'invalid'
: (brokenProviderState(inspection.providers) ?? 'invalid')
const error = inspection.error
if (!error) throw new Error('Storage resolution failed without a configuration error')
const providerId: StorageProviderId | null =
selectedId === STORAGE_CAPABILITY.defaultProvider.id
? STORAGE_CAPABILITY.defaultProvider.id
: (selected?.id ?? null)
const safeMessage =
selectedId && !providerId
? `Unknown ${STORAGE_CAPABILITY.selectorKey}. Expected one of: ${[
STORAGE_CAPABILITY.defaultProvider.id,
...STORAGE_CAPABILITY.providers.map((provider) => provider.id),
].join(', ')}`
: error.message
return {
...featureMetadata('storage'),
strategy: 'selected',
state,
providerId,
defaultProviderId: STORAGE_CAPABILITY.defaultProvider.id,
providers: inspection.providers,
issue: issue(state, error, safeMessage),
}
}
function inspectSandbox(values: EnvCapabilityValues): SandboxCapabilityStatus {
const inspection = inspectCapability(SANDBOX_CAPABILITY, values)
const requestedProvider =
readConfiguredString(values, SANDBOX_CAPABILITY.selectorKey) ??
SANDBOX_CAPABILITY.defaultProvider.id
const providerId =
inspection.providerId === 'e2b' && !isTruthyEnvCapabilityValue(values, 'E2B_ENABLED')
? 'disabled'
: inspection.providerId
if (!inspection.error) {
const coherenceProblems: string[] = []
if (
isTruthyEnvCapabilityValue(values, 'E2B_ENABLED') !==
isTruthyEnvCapabilityValue(values, 'NEXT_PUBLIC_E2B_ENABLED')
) {
coherenceProblems.push('E2B_ENABLED and NEXT_PUBLIC_E2B_ENABLED disagree')
}
const remoteAvailable = providerId !== null && providerId !== 'disabled'
if (remoteAvailable !== isTruthyEnvCapabilityValue(values, 'NEXT_PUBLIC_SANDBOXES_ENABLED')) {
coherenceProblems.push(
'remote sandbox availability and NEXT_PUBLIC_SANDBOXES_ENABLED disagree'
)
}
if (coherenceProblems.length > 0) {
return {
...featureMetadata('sandbox'),
state: 'partial',
providerId,
issue: {
state: 'partial',
message: `${coherenceProblems.join('; ')}. Server and browser configuration must match.`,
},
}
}
return {
...featureMetadata('sandbox'),
state: providerId === 'disabled' ? 'default' : 'configured',
providerId,
}
}
const knownProvider = requestedProvider === 'e2b' || requestedProvider === 'daytona'
const selectedProvider = inspection.providers.find(
(provider) => provider.id === requestedProvider
)
const state = !knownProvider || selectedProvider?.state === 'invalid' ? 'invalid' : 'missing'
return {
...featureMetadata('sandbox'),
state,
providerId: knownProvider ? requestedProvider : null,
issue: issue(
state,
inspection.error,
knownProvider
? inspection.error.message
: 'Unknown SANDBOX_PROVIDER. Expected one of: e2b, daytona'
),
}
}
function inspectJobs(values: EnvCapabilityValues): JobsCapabilityStatus {
const inspection = inspectCapability(ASYNC_JOBS_CAPABILITY, values)
if (!inspection.error && inspection.providerId) {
return {
...featureMetadata('jobs'),
state: inspection.providerId === 'database' ? 'default' : 'configured',
providerId: inspection.providerId,
}
}
if (!inspection.error) {
throw new Error('Async jobs inspection failed without a configuration error')
}
const configuredFieldCount = ['TRIGGER_SECRET_KEY', 'TRIGGER_PROJECT_ID'].filter((key) =>
hasEnvCapabilityValue(values, key)
).length
const state = configuredFieldCount === 0 ? 'missing' : 'partial'
return {
...featureMetadata('jobs'),
state,
providerId: 'trigger-dev',
issue: issue(state, inspection.error),
}
}
function inspectCache(values: EnvCapabilityValues): CacheCapabilityStatus {
const inspection = inspectCapability(CACHE_CAPABILITY, values)
if (!inspection.error && inspection.providerId) {
return {
...featureMetadata('cache'),
state: inspection.providerId === 'database' ? 'default' : 'configured',
providerId: inspection.providerId,
}
}
if (!inspection.error) {
throw new Error('Cache inspection failed without a configuration error')
}
const redis = inspection.providers.find((provider) => provider.id === 'redis')
const state: CapabilityStatusIssue['state'] = redis?.state === 'invalid' ? 'invalid' : 'partial'
return {
...featureMetadata('cache'),
state,
providerId: 'redis',
issue: issue(state, inspection.error),
}
}
function inspectKnowledge(values: EnvCapabilityValues): KnowledgeCapabilityStatus {
const inspection = inspectCapability(OCR_CAPABILITY, values)
if (!inspection.error && inspection.providerId) {
return {
...featureMetadata('knowledge'),
state: inspection.providerId === 'local' ? 'default' : 'configured',
providerId: inspection.providerId,
}
}
if (!inspection.error) {
throw new Error('OCR inspection failed without a configuration error')
}
const selected = readConfiguredString(values, OCR_CAPABILITY.selectorKey)
const azureFields = ['OCR_AZURE_API_KEY', 'OCR_AZURE_ENDPOINT', 'OCR_AZURE_MODEL_NAME'] as const
const azureFieldCount = azureFields.filter((key) => hasEnvCapabilityValue(values, key)).length
const knownProvider =
selected === null ||
selected === 'local' ||
selected === 'mistral' ||
selected === 'azure-mistral'
const resolvedProvider = inspection.providerId
const selectedInspection = inspection.providers.find(
(provider) => provider.id === resolvedProvider
)
const state =
!knownProvider || selectedInspection?.state === 'invalid'
? 'invalid'
: selected === 'mistral' || selected === 'azure-mistral'
? azureFieldCount === 0 && selected === 'azure-mistral'
? 'missing'
: selected === 'mistral' && !hasEnvCapabilityValue(values, 'MISTRAL_API_KEY')
? 'missing'
: 'partial'
: 'partial'
return {
...featureMetadata('knowledge'),
state,
providerId:
selected === 'local' || selected === 'mistral' || selected === 'azure-mistral'
? selected
: selected === null
? resolvedProvider
: null,
issue: issue(
state,
inspection.error,
knownProvider
? inspection.error.message
: 'Unknown OCR_PROVIDER. Expected one of: local, mistral, azure-mistral'
),
}
}
function inspectLlm(values: EnvCapabilityValues): LlmCapabilityStatus {
const pools = {} as Record<LlmKeyPoolId, LlmKeyPoolStatus>
let configuredPoolCount = 0
let configuredKeyCount = 0
let effectiveKeyCount = 0
for (const id of Object.keys(LLM_KEY_POOLS) as LlmKeyPoolId[]) {
const definition = LLM_KEY_POOLS[id]
const rotationKeyCount = definition.keys.filter((key) =>
hasEnvCapabilityValue(values, key)
).length
const fallbackKeyConfigured =
'fallbackKey' in definition && hasEnvCapabilityValue(values, definition.fallbackKey)
const poolConfiguredKeyCount = rotationKeyCount + (fallbackKeyConfigured ? 1 : 0)
const poolEffectiveKeyCount = rotationKeyCount || (fallbackKeyConfigured ? 1 : 0)
const state = poolEffectiveKeyCount > 0 ? 'configured' : 'missing'
if (state === 'configured') configuredPoolCount += 1
configuredKeyCount += poolConfiguredKeyCount
effectiveKeyCount += poolEffectiveKeyCount
pools[id] = {
id,
state,
configuredKeyCount: poolConfiguredKeyCount,
effectiveKeyCount: poolEffectiveKeyCount,
fallbackKeyConfigured,
}
}
return {
...featureMetadata('llm'),
state: configuredPoolCount > 0 ? 'configured' : 'missing',
pools,
configuredPoolCount,
configuredKeyCount,
effectiveKeyCount,
}
}
function inspectOAuthClients(values: EnvCapabilityValues): OAuthClientStatuses {
const clients = {} as Record<OAuthClientCapabilityId, OAuthClientStatus>
let readyCount = 0
let partialCount = 0
let absentCount = 0
let invalidCount = 0
for (const id of Object.keys(OAUTH_CLIENT_CAPABILITIES) as OAuthClientCapabilityId[]) {
const inspection = inspectOAuthClientCapability(id, values)
if (inspection.state === 'ready') readyCount += 1
else if (inspection.state === 'partial') partialCount += 1
else if (inspection.state === 'absent') absentCount += 1
else invalidCount += 1
clients[id] = {
id,
state: inspection.state,
configuredFieldCount: OAUTH_CLIENT_CAPABILITIES[id].length - inspection.missingFields.length,
requiredFieldCount: OAUTH_CLIENT_CAPABILITIES[id].length,
missingFields: inspection.missingFields,
setupCommand: inspection.setupCommand,
}
}
return { clients, readyCount, partialCount, absentCount, invalidCount }
}
const FEATURE_STATUS_BUILDERS = {
email: inspectEmail,
storage: inspectStorage,
sandbox: inspectSandbox,
jobs: inspectJobs,
cache: inspectCache,
knowledge: inspectKnowledge,
llm: inspectLlm,
} satisfies Record<SetupStatusFeatureId, (values: EnvCapabilityValues) => unknown>
/** Builds a status snapshot without returning any configured secret values. */
export function buildEnvCapabilityStatus(values: EnvCapabilityValues): EnvCapabilityStatusSnapshot {
return {
features: {
email: FEATURE_STATUS_BUILDERS.email(values),
storage: FEATURE_STATUS_BUILDERS.storage(values),
sandbox: FEATURE_STATUS_BUILDERS.sandbox(values),
jobs: FEATURE_STATUS_BUILDERS.jobs(values),
cache: FEATURE_STATUS_BUILDERS.cache(values),
knowledge: FEATURE_STATUS_BUILDERS.knowledge(values),
llm: FEATURE_STATUS_BUILDERS.llm(values),
},
oauthClients: inspectOAuthClients(values),
}
}