Files
simstudioai--sim/apps/sim/lib/copilot/tools/server/user/set-environment-variables.ts
T
wehub-resource-sync d25d482dc2
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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

152 lines
4.9 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { z } from 'zod'
import { SetEnvironmentVariables } from '@/lib/copilot/generated/tool-catalog-v1'
import {
ensureWorkflowAccess,
ensureWorkspaceAccess,
getDefaultWorkspaceId,
} from '@/lib/copilot/tools/handlers/access'
import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
import { upsertPersonalEnvVars, upsertWorkspaceEnvVars } from '@/lib/environment/utils'
type EnvironmentVariableInputValue = string | number | boolean | null | undefined
interface EnvironmentVariableInput {
name: string
value: EnvironmentVariableInputValue
}
interface SetEnvironmentVariablesParams {
variables: Record<string, EnvironmentVariableInputValue> | EnvironmentVariableInput[]
scope?: 'personal' | 'workspace'
workflowId?: string
workspaceId?: string
}
interface SetEnvironmentVariablesResult {
message: string
scope: 'personal' | 'workspace'
workspaceId?: string
variableCount: number
variableNames: string[]
addedVariables: string[]
updatedVariables: string[]
workspaceUpdatedVariables: string[]
}
const EnvVarSchema = z.object({ variables: z.record(z.string(), z.string()) })
function normalizeVariables(
input: Record<string, EnvironmentVariableInputValue> | EnvironmentVariableInput[]
): Record<string, string> {
if (Array.isArray(input)) {
return input.reduce(
(acc, item) => {
if (item && typeof item.name === 'string') {
acc[item.name] = String(item.value ?? '')
}
return acc
},
{} as Record<string, string>
)
}
return Object.fromEntries(
Object.entries(input || {}).map(([k, v]) => [k, String(v ?? '')])
) as Record<string, string>
}
async function resolveWorkspaceId(
params: SetEnvironmentVariablesParams,
context: ServerToolContext | undefined,
userId: string
): Promise<string> {
if (params.workflowId) {
const { workflow } = await ensureWorkflowAccess(params.workflowId, userId, 'write')
if (!workflow.workspaceId) {
throw new Error(`Workflow ${params.workflowId} is not associated with a workspace`)
}
return workflow.workspaceId
}
const workspaceId = params.workspaceId ?? context?.workspaceId
if (workspaceId) {
await ensureWorkspaceAccess(workspaceId, userId, 'write')
return workspaceId
}
return getDefaultWorkspaceId(userId)
}
export const setEnvironmentVariablesServerTool: BaseServerTool<
SetEnvironmentVariablesParams,
SetEnvironmentVariablesResult
> = {
name: SetEnvironmentVariables.id,
async execute(
params: SetEnvironmentVariablesParams,
context?: ServerToolContext
): Promise<SetEnvironmentVariablesResult> {
const logger = createLogger('SetEnvironmentVariablesServerTool')
if (!context?.userId) {
logger.error(
'Unauthorized attempt to set environment variables - no authenticated user context'
)
throw new Error('Authentication required')
}
const authenticatedUserId = context.userId
const { variables } = params || ({} as SetEnvironmentVariablesParams)
const scope = params.scope === 'personal' ? 'personal' : 'workspace'
const normalized = normalizeVariables(variables || {})
const { variables: validatedVariables } = EnvVarSchema.parse({ variables: normalized })
const variableNames = Object.keys(validatedVariables)
const added: string[] = []
const updated: string[] = []
let workspaceUpdated: string[] = []
let resolvedWorkspaceId: string | undefined
if (scope === 'workspace') {
resolvedWorkspaceId = await resolveWorkspaceId(params, context, authenticatedUserId)
workspaceUpdated = await upsertWorkspaceEnvVars(
resolvedWorkspaceId,
validatedVariables,
authenticatedUserId
)
} else {
const result = await upsertPersonalEnvVars(authenticatedUserId, validatedVariables)
added.push(...result.added)
updated.push(...result.updated)
}
const totalProcessed = added.length + updated.length + workspaceUpdated.length
logger.info('Saved environment variables', {
userId: authenticatedUserId,
scope,
addedCount: added.length,
updatedCount: updated.length,
workspaceUpdatedCount: workspaceUpdated.length,
workspaceId: resolvedWorkspaceId,
})
const parts: string[] = []
if (added.length > 0) parts.push(`${added.length} personal secret(s) added`)
if (updated.length > 0) parts.push(`${updated.length} personal secret(s) updated`)
if (workspaceUpdated.length > 0)
parts.push(`${workspaceUpdated.length} workspace secret(s) updated`)
return {
message: `Successfully processed ${totalProcessed} secret(s): ${parts.join(', ')}`,
scope,
workspaceId: resolvedWorkspaceId,
variableCount: variableNames.length,
variableNames,
addedVariables: added,
updatedVariables: updated,
workspaceUpdatedVariables: workspaceUpdated,
}
},
}