import { useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AGENT_ADAPTER_TYPES, getAdapterEnvironmentSupport, type Environment, type EnvironmentProbeResult, type JsonSchema, } from "@paperclipai/shared"; import { Check, Settings } from "lucide-react"; import { environmentsApi } from "@/api/environments"; import { instanceSettingsApi } from "@/api/instanceSettings"; import { secretsApi } from "@/api/secrets"; import { Button } from "@/components/ui/button"; import { JsonSchemaForm, getDefaultValues, validateJsonSchemaForm } from "@/components/JsonSchemaForm"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; import { useCompany } from "@/context/CompanyContext"; import { useToast } from "@/context/ToastContext"; import { queryKeys } from "@/lib/queryKeys"; import { Field, ToggleField, adapterLabels, } from "../components/agent-config-primitives"; type EnvironmentFormState = { name: string; description: string; driver: "local" | "ssh" | "sandbox"; sshHost: string; sshPort: string; sshUsername: string; sshRemoteWorkspacePath: string; sshPrivateKey: string; sshPrivateKeySecretId: string; sshKnownHosts: string; sshStrictHostKeyChecking: boolean; sandboxProvider: string; sandboxConfig: Record; }; const ENVIRONMENT_SUPPORT_ROWS = AGENT_ADAPTER_TYPES.map((adapterType) => ({ adapterType, support: getAdapterEnvironmentSupport(adapterType), })); function buildEnvironmentPayload(form: EnvironmentFormState) { return { name: form.name.trim(), description: form.description.trim() || null, driver: form.driver, config: form.driver === "ssh" ? { host: form.sshHost.trim(), port: Number.parseInt(form.sshPort || "22", 10) || 22, username: form.sshUsername.trim(), remoteWorkspacePath: form.sshRemoteWorkspacePath.trim(), privateKey: form.sshPrivateKey.trim() || null, privateKeySecretRef: form.sshPrivateKey.trim().length > 0 || !form.sshPrivateKeySecretId ? null : { type: "secret_ref" as const, secretId: form.sshPrivateKeySecretId, version: "latest" as const }, knownHosts: form.sshKnownHosts.trim() || null, strictHostKeyChecking: form.sshStrictHostKeyChecking, } : form.driver === "sandbox" ? { provider: form.sandboxProvider.trim(), ...form.sandboxConfig, } : {}, } as const; } function createEmptyEnvironmentForm(): EnvironmentFormState { return { name: "", description: "", driver: "ssh", sshHost: "", sshPort: "22", sshUsername: "", sshRemoteWorkspacePath: "", sshPrivateKey: "", sshPrivateKeySecretId: "", sshKnownHosts: "", sshStrictHostKeyChecking: true, sandboxProvider: "", sandboxConfig: {}, }; } function readSshConfig(environment: Environment) { const config = environment.config ?? {}; return { host: typeof config.host === "string" ? config.host : "", port: typeof config.port === "number" ? String(config.port) : typeof config.port === "string" ? config.port : "22", username: typeof config.username === "string" ? config.username : "", remoteWorkspacePath: typeof config.remoteWorkspacePath === "string" ? config.remoteWorkspacePath : "", privateKey: "", privateKeySecretId: config.privateKeySecretRef && typeof config.privateKeySecretRef === "object" && !Array.isArray(config.privateKeySecretRef) && typeof (config.privateKeySecretRef as { secretId?: unknown }).secretId === "string" ? String((config.privateKeySecretRef as { secretId: string }).secretId) : "", knownHosts: typeof config.knownHosts === "string" ? config.knownHosts : "", strictHostKeyChecking: typeof config.strictHostKeyChecking === "boolean" ? config.strictHostKeyChecking : true, }; } function readSandboxConfig(environment: Environment) { const config = environment.config ?? {}; const { provider: rawProvider, ...providerConfig } = config; return { provider: typeof rawProvider === "string" && rawProvider.trim().length > 0 ? rawProvider : "fake", config: providerConfig, }; } function normalizeJsonSchema(schema: unknown): JsonSchema | null { return schema && typeof schema === "object" && !Array.isArray(schema) ? schema as JsonSchema : null; } function summarizeSandboxConfig(config: Record): string | null { for (const key of ["template", "image", "region", "workspacePath"]) { const value = config[key]; if (typeof value === "string" && value.trim().length > 0) { return value; } } return null; } function SupportMark({ supported }: { supported: boolean }) { return supported ? ( Yes ) : ( No ); } export function CompanyEnvironments() { const { selectedCompany, selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const { pushToast } = useToast(); const queryClient = useQueryClient(); const [editingEnvironmentId, setEditingEnvironmentId] = useState(null); const [environmentForm, setEnvironmentForm] = useState(createEmptyEnvironmentForm); const [probeResults, setProbeResults] = useState>({}); useEffect(() => { setBreadcrumbs([ { label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: "Settings", href: "/company/settings" }, { label: "Environments" }, ]); }, [selectedCompany?.name, setBreadcrumbs]); const { data: experimentalSettings } = useQuery({ queryKey: queryKeys.instance.experimentalSettings, queryFn: () => instanceSettingsApi.getExperimental(), retry: false, }); const environmentsEnabled = experimentalSettings?.enableEnvironments === true; const { data: environments } = useQuery({ queryKey: selectedCompanyId ? queryKeys.environments.list(selectedCompanyId) : ["environments", "none"], queryFn: () => environmentsApi.list(selectedCompanyId!), enabled: Boolean(selectedCompanyId) && environmentsEnabled, }); const { data: environmentCapabilities } = useQuery({ queryKey: selectedCompanyId ? ["environment-capabilities", selectedCompanyId] : ["environment-capabilities", "none"], queryFn: () => environmentsApi.capabilities(selectedCompanyId!), enabled: Boolean(selectedCompanyId) && environmentsEnabled, }); const { data: secrets } = useQuery({ queryKey: selectedCompanyId ? ["company-secrets", selectedCompanyId] : ["company-secrets", "none"], queryFn: () => secretsApi.list(selectedCompanyId!), enabled: Boolean(selectedCompanyId), }); const environmentMutation = useMutation({ mutationFn: async (form: EnvironmentFormState) => { const body = buildEnvironmentPayload(form); if (editingEnvironmentId) { return await environmentsApi.update(editingEnvironmentId, body); } return await environmentsApi.create(selectedCompanyId!, body); }, onSuccess: async (environment) => { await queryClient.invalidateQueries({ queryKey: queryKeys.environments.list(selectedCompanyId!), }); setEditingEnvironmentId(null); setEnvironmentForm(createEmptyEnvironmentForm()); pushToast({ title: editingEnvironmentId ? "Environment updated" : "Environment created", body: `${environment.name} is ready.`, tone: "success", }); }, onError: (error) => { pushToast({ title: "Failed to save environment", body: error instanceof Error ? error.message : "Environment save failed.", tone: "error", }); }, }); const environmentProbeMutation = useMutation({ mutationFn: async (environmentId: string) => await environmentsApi.probe(environmentId), onSuccess: (probe, environmentId) => { setProbeResults((current) => ({ ...current, [environmentId]: probe, })); pushToast({ title: probe.ok ? "Environment probe passed" : "Environment probe failed", body: probe.summary, tone: probe.ok ? "success" : "error", }); }, onError: (error, environmentId) => { const failedEnvironment = (environments ?? []).find((environment) => environment.id === environmentId); setProbeResults((current) => ({ ...current, [environmentId]: { ok: false, driver: failedEnvironment?.driver ?? "local", summary: error instanceof Error ? error.message : "Environment probe failed.", details: null, }, })); pushToast({ title: "Environment probe failed", body: error instanceof Error ? error.message : "Environment probe failed.", tone: "error", }); }, }); const draftEnvironmentProbeMutation = useMutation({ mutationFn: async (form: EnvironmentFormState) => { const body = buildEnvironmentPayload(form); return await environmentsApi.probeConfig(selectedCompanyId!, body); }, onSuccess: (probe) => { pushToast({ title: probe.ok ? "Draft probe passed" : "Draft probe failed", body: probe.summary, tone: probe.ok ? "success" : "error", }); }, onError: (error) => { pushToast({ title: "Draft probe failed", body: error instanceof Error ? error.message : "Environment probe failed.", tone: "error", }); }, }); useEffect(() => { setEditingEnvironmentId(null); setEnvironmentForm(createEmptyEnvironmentForm()); setProbeResults({}); }, [selectedCompanyId]); function handleEditEnvironment(environment: Environment) { setEditingEnvironmentId(environment.id); if (environment.driver === "ssh") { const ssh = readSshConfig(environment); setEnvironmentForm({ ...createEmptyEnvironmentForm(), name: environment.name, description: environment.description ?? "", driver: "ssh", sshHost: ssh.host, sshPort: ssh.port, sshUsername: ssh.username, sshRemoteWorkspacePath: ssh.remoteWorkspacePath, sshPrivateKey: ssh.privateKey, sshPrivateKeySecretId: ssh.privateKeySecretId, sshKnownHosts: ssh.knownHosts, sshStrictHostKeyChecking: ssh.strictHostKeyChecking, }); return; } if (environment.driver === "sandbox") { const sandbox = readSandboxConfig(environment); setEnvironmentForm({ ...createEmptyEnvironmentForm(), name: environment.name, description: environment.description ?? "", driver: "sandbox", sandboxProvider: sandbox.provider, sandboxConfig: sandbox.config, }); return; } setEnvironmentForm({ ...createEmptyEnvironmentForm(), name: environment.name, description: environment.description ?? "", driver: "local", }); } function handleCancelEnvironmentEdit() { setEditingEnvironmentId(null); setEnvironmentForm(createEmptyEnvironmentForm()); } const discoveredPluginSandboxProviders = Object.entries(environmentCapabilities?.sandboxProviders ?? {}) .filter(([provider, capability]) => provider !== "fake" && capability.supportsRunExecution) .map(([provider, capability]) => ({ provider, displayName: capability.displayName || provider, description: capability.description, configSchema: normalizeJsonSchema(capability.configSchema), })) .sort((left, right) => left.displayName.localeCompare(right.displayName)); const sandboxCreationEnabled = discoveredPluginSandboxProviders.length > 0; const sandboxSupportVisible = sandboxCreationEnabled; const pluginSandboxProviders = environmentForm.sandboxProvider.trim().length > 0 && environmentForm.sandboxProvider !== "fake" && !discoveredPluginSandboxProviders.some((provider) => provider.provider === environmentForm.sandboxProvider) ? [ ...discoveredPluginSandboxProviders, { provider: environmentForm.sandboxProvider, displayName: environmentForm.sandboxProvider, description: undefined, configSchema: null }, ] : discoveredPluginSandboxProviders; const selectedSandboxProvider = pluginSandboxProviders.find( (provider) => provider.provider === environmentForm.sandboxProvider, ) ?? null; const selectedSandboxSchema = selectedSandboxProvider?.configSchema ?? null; const sandboxConfigErrors = environmentForm.driver === "sandbox" && selectedSandboxSchema ? validateJsonSchemaForm(selectedSandboxSchema as any, environmentForm.sandboxConfig) : {}; useEffect(() => { if (environmentForm.driver !== "sandbox") return; if (environmentForm.sandboxProvider.trim().length > 0 && environmentForm.sandboxProvider !== "fake") return; const firstProvider = discoveredPluginSandboxProviders[0]?.provider; if (!firstProvider) return; const firstSchema = discoveredPluginSandboxProviders[0]?.configSchema; setEnvironmentForm((current) => ( current.driver !== "sandbox" || (current.sandboxProvider.trim().length > 0 && current.sandboxProvider !== "fake") ? current : { ...current, sandboxProvider: firstProvider, sandboxConfig: firstSchema ? getDefaultValues(firstSchema as any) : {}, } )); }, [discoveredPluginSandboxProviders, environmentForm.driver, environmentForm.sandboxProvider]); const environmentFormValid = environmentForm.name.trim().length > 0 && (environmentForm.driver !== "ssh" || ( environmentForm.sshHost.trim().length > 0 && environmentForm.sshUsername.trim().length > 0 && environmentForm.sshRemoteWorkspacePath.trim().length > 0 )) && (environmentForm.driver !== "sandbox" || environmentForm.sandboxProvider.trim().length > 0 && environmentForm.sandboxProvider !== "fake" && Object.keys(sandboxConfigErrors).length === 0); if (!selectedCompanyId) { return
Select a company to manage environments.
; } if (!environmentsEnabled) { return (

Company Environments

Enable Environments in instance experimental settings to manage company execution targets.
); } return (

Company Environments

Define reusable execution targets for projects, issue workspaces, and remote-capable adapters.

Environment choices use the same adapter support matrix as agent defaults. SSH is always available for remote-managed adapters, and sandbox environments appear only when a run-capable sandbox provider plugin is installed.
{sandboxSupportVisible ? ( ) : null} {(environmentCapabilities?.adapters.map((support) => ({ adapterType: support.adapterType, support, })) ?? ENVIRONMENT_SUPPORT_ROWS).map(({ adapterType, support }) => ( {sandboxSupportVisible ? ( ) : null} ))}
Environment support by adapter
Adapter Local SSHSandbox
{adapterLabels[adapterType] ?? adapterType} support.sandboxProviders[provider.provider] === "supported")} />
{(environments ?? []).length === 0 ? (
No environments saved for this company yet.
) : ( (environments ?? []).map((environment) => { const probe = probeResults[environment.id] ?? null; const isEditing = editingEnvironmentId === environment.id; return (
{environment.name} · {environment.driver}
{environment.description ? (
{environment.description}
) : null} {environment.driver === "ssh" ? (
{typeof environment.config.host === "string" ? environment.config.host : "SSH host"} ·{" "} {typeof environment.config.username === "string" ? environment.config.username : "user"}
) : environment.driver === "sandbox" ? (
{(() => { const provider = typeof environment.config.provider === "string" ? environment.config.provider : "sandbox"; const displayName = environmentCapabilities?.sandboxProviders?.[provider]?.displayName ?? provider; const summary = summarizeSandboxConfig(environment.config as Record); return `${displayName} sandbox provider${summary ? ` · ${summary}` : ""}`; })()}
) : (
Runs on this Paperclip host.
)}
{environment.driver !== "local" ? ( ) : null}
{probe ? (
{probe.summary}
{probe.details?.error && typeof probe.details.error === "string" ? (
{probe.details.error}
) : null}
) : null}
); }) )}
{editingEnvironmentId ? "Edit environment" : "Add environment"}
setEnvironmentForm((current) => ({ ...current, name: e.target.value }))} /> setEnvironmentForm((current) => ({ ...current, description: e.target.value }))} /> {environmentForm.driver === "ssh" ? (
setEnvironmentForm((current) => ({ ...current, sshHost: e.target.value }))} /> setEnvironmentForm((current) => ({ ...current, sshPort: e.target.value }))} /> setEnvironmentForm((current) => ({ ...current, sshUsername: e.target.value }))} /> setEnvironmentForm((current) => ({ ...current, sshRemoteWorkspacePath: e.target.value }))} />