From 2cbdbf1553ff24aeed80777715aae19d75e99f28 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 2 Sep 2026 22:30:10 -0700 Subject: [PATCH 01/14] feat(supply-chain): sign published images and generate the chart image inventory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enterprise security reviews ask for three artifacts Sim did not publish: a signature proving who built an image, SLSA provenance describing how, and an SBOM listing what is inside. Add all three, plus the image inventory an operator needs to mirror Sim into a disconnected registry. attest-images runs after create-ghcr-manifests rather than inside the build. buildx's own provenance/sbom attestations stay off because the extra manifests they add to an index break the imagetools retagging promote-images depends on; attaching attestations to the finished index leaves it untouched, since they are stored as separate referrer manifests. The subject is the sha index digest — imagetools create is deterministic, so the version and latest indexes built from the same per-arch manifests share that digest and one attestation covers every tag a customer can pull. The digest comes from `{{json .Manifest}}` piped through jq rather than `{{.Manifest.Digest}}`: the latter resolves the index to the runner's own platform, which would attest one architecture and leave the pulled index unsigned. helm/sim/images.yaml is derived from the rendered chart, not from values.yaml, because one image is written directly into a template — the NVIDIA device plugin — and a values-derived list misses it in the case that is hardest to notice, where the mirror succeeds and one pod still pulls from the internet. Tags stay unresolved: digests belong to a release, and the chart's sim.image helper already accepts a per-image digest for pinning at install time. The check runs in the helm workflow, which has Helm set up; check:audits does not. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015BwsJTEQRzWJaY4BRCkPZt --- .github/workflows/ci.yml | 102 +++++++++++++++ .github/workflows/helm.yml | 7 ++ helm/sim/images.yaml | 23 ++++ package.json | 2 + scripts/generate-image-manifest.test.ts | 93 ++++++++++++++ scripts/generate-image-manifest.ts | 158 ++++++++++++++++++++++++ 6 files changed, 385 insertions(+) create mode 100644 helm/sim/images.yaml create mode 100644 scripts/generate-image-manifest.test.ts create mode 100644 scripts/generate-image-manifest.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82f4b3299cc..a15b7982ba3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -611,6 +611,108 @@ jobs: "${IMAGE}:${SHA}-amd64" "${IMAGE}:${SHA}-arm64" fi + # Sign the published images and attach SLSA provenance and an SBOM to each. + # + # This runs after create-ghcr-manifests rather than inside the build because + # buildx's own provenance/sbom attestations stay off (see the note in + # .github/actions/docker-build): the extra manifests they add to an index + # break the `imagetools create` retagging that promote-images depends on. + # Attaching attestations here instead leaves the index itself untouched — they + # are stored as separate referrer manifests that point at it. + # + # The subject is the sha index digest. `imagetools create` builds the version + # and latest indexes from the same two per-arch manifests in the same order, + # so all three tags resolve to one digest and a single attestation covers + # every tag a customer can pull. + attest-images: + name: Attest Images + runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }} + timeout-minutes: 15 + needs: [create-ghcr-manifests] + if: >- + !cancelled() && + needs.create-ghcr-manifests.result == 'success' && + github.event_name == 'push' && github.ref == 'refs/heads/main' + permissions: + contents: read + packages: write + # Sigstore signs against the runner's OIDC identity; no key material is stored. + id-token: write + attestations: write + strategy: + fail-fast: false + matrix: + image: + - ghcr.io/simstudioai/simstudio + - ghcr.io/simstudioai/migrations + - ghcr.io/simstudioai/realtime + - ghcr.io/simstudioai/pii + - ghcr.io/simstudioai/cron + + steps: + - name: Login to GHCR + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Resolved once and reused by every step below: signing a tag would sign + # whatever that tag points at when the step runs, which is not necessarily + # what this run published. + # + # `{{json .Manifest}}` piped through jq, not `{{.Manifest.Digest}}` — the + # latter resolves the index to the runner's own platform and prints that + # manifest's digest instead, which would attest one architecture and leave + # the index a customer actually pulls unsigned. + - name: Resolve index digest + id: digest + run: | + DIGEST="$(docker buildx imagetools inspect "${{ matrix.image }}:${{ github.sha }}" \ + --format '{{json .Manifest}}' | jq -r '.digest')" + if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then + echo "::error::Could not resolve a digest for ${{ matrix.image }}:${{ github.sha }}" + exit 1 + fi + echo "value=${DIGEST}" >> "$GITHUB_OUTPUT" + + - name: Generate SBOM + uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 + with: + image: ${{ matrix.image }}@${{ steps.digest.outputs.value }} + format: spdx-json + output-file: sbom.spdx.json + # The action's own release upload is for workflows triggered by a + # release; these attach to the image instead. + upload-artifact: false + upload-release-assets: false + + - name: Attest SBOM + uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 + with: + subject-name: ${{ matrix.image }} + subject-digest: ${{ steps.digest.outputs.value }} + sbom-path: sbom.spdx.json + # Stored alongside the image so a mirrored registry carries the + # attestation with it, rather than only being retrievable from GitHub. + push-to-registry: true + + - name: Attest build provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ${{ matrix.image }} + subject-digest: ${{ steps.digest.outputs.value }} + push-to-registry: true + + - name: Install Cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + + # The attestations above prove how the image was built; this is the plain + # signature that admission controllers (Kyverno, the Sigstore policy + # controller) verify before admitting a pod. + - name: Sign image + run: cosign sign --yes "${{ matrix.image }}@${{ steps.digest.outputs.value }}" + # Check if docs changed # Smallest runner on purpose: a depth-2 checkout plus a path filter, no # install and no build. diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index 05e2ee8a12b..fc68777d92a 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -43,6 +43,13 @@ jobs: - name: Scheduler parity (docker/crontab vs helm cronjobs) run: bun run scripts/check-cron-parity.ts + # helm/sim/images.yaml is what an operator mirrors into a disconnected + # registry, so a chart change that adds an image has to update it. Lives + # here rather than in `check:audits` because it renders the chart, and the + # audits job has no Helm. The script imports only node builtins. + - name: Image inventory is current + run: bun run images:check + - name: Helm lint run: helm lint helm/sim --values helm/sim/ci/default-values.yaml diff --git a/helm/sim/images.yaml b/helm/sim/images.yaml new file mode 100644 index 00000000000..8e1d3ae41ca --- /dev/null +++ b/helm/sim/images.yaml @@ -0,0 +1,23 @@ +# Generated by `bun run images:generate`. Do not edit this file directly. +# +# Every container image a complete Sim install pulls, rendered from this chart +# with all optional components enabled. Mirror all of them into a disconnected +# registry before installing, then point the chart at your registry with +# `global.imageRegistry` and pin each image's `digest` to what your mirror +# resolved. +chartVersion: 1.8.0 +appVersion: v0.8.18 +images: + - busybox:1.36 + - curlimages/curl:8.5.0 + - ghcr.io/simstudioai/copilot:v0.8.18 + - ghcr.io/simstudioai/migrations:v0.8.18 + - ghcr.io/simstudioai/pii:v0.8.18 + - ghcr.io/simstudioai/realtime:v0.8.18 + - ghcr.io/simstudioai/simstudio:v0.8.18 + - nvcr.io/nvidia/k8s-device-plugin:v0.18.2 + - ollama/ollama:0.23.2 + - otel/opentelemetry-collector-contrib:0.91.0 + - pgvector/pgvector:pg17 + - postgres:17-alpine + - redis:7-alpine diff --git a/package.json b/package.json index d19acf8cc2e..a1ab89af429 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,8 @@ "tool-metadata:check": "bun run scripts/sync-tool-metadata.ts --check", "deployment-config:generate": "bun run scripts/generate-deployment-config.ts", "deployment-config:check": "bun run scripts/generate-deployment-config.ts --check", + "images:generate": "bun run scripts/generate-image-manifest.ts", + "images:check": "bun run scripts/generate-image-manifest.ts --check", "integration-catalog:check": "bun run scripts/check-integration-catalog.ts", "docs:check": "bun run scripts/generate-docs.ts --check", "mship-tools:generate": "bun run scripts/sync-tool-catalog.ts", diff --git a/scripts/generate-image-manifest.test.ts b/scripts/generate-image-manifest.test.ts new file mode 100644 index 00000000000..c3018bff576 --- /dev/null +++ b/scripts/generate-image-manifest.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from 'vitest' +import { collectImages, renderManifest } from './generate-image-manifest' + +describe('collectImages', () => { + it('finds images across every container key a pod spec can use', () => { + const images = collectImages([ + { + kind: 'Deployment', + spec: { + template: { + spec: { + initContainers: [{ image: 'ghcr.io/simstudioai/migrations:v1' }], + containers: [{ image: 'ghcr.io/simstudioai/simstudio:v1' }], + ephemeralContainers: [{ image: 'busybox:1.36' }], + }, + }, + }, + }, + ]) + + expect(images).toEqual([ + 'busybox:1.36', + 'ghcr.io/simstudioai/migrations:v1', + 'ghcr.io/simstudioai/simstudio:v1', + ]) + }) + + it('reaches containers nested below a workload wrapper', () => { + const images = collectImages([ + { + kind: 'CronJob', + spec: { + jobTemplate: { + spec: { template: { spec: { containers: [{ image: 'curlimages/curl:8.5.0' }] } } }, + }, + }, + }, + ]) + + expect(images).toEqual(['curlimages/curl:8.5.0']) + }) + + it('deduplicates the same image pulled by several workloads', () => { + const container = { containers: [{ image: 'redis:7-alpine' }] } + const images = collectImages([ + { spec: { template: { spec: container } } }, + { spec: { template: { spec: container } } }, + ]) + + expect(images).toEqual(['redis:7-alpine']) + }) + + it('ignores an image field that is not a container image', () => { + const images = collectImages([{ metadata: { annotations: { image: 'not-a-container' } } }]) + + expect(images).toEqual([]) + }) + + it('skips a container whose image is absent or empty', () => { + const images = collectImages([ + { spec: { containers: [{ name: 'no-image' }, { image: '' }, { image: 'busybox:1.36' }] } }, + ]) + + expect(images).toEqual(['busybox:1.36']) + }) + + it('tolerates null entries rather than throwing on a sparse render', () => { + const images = collectImages([null, { spec: { containers: [null, { image: 'redis:7' }] } }]) + + expect(images).toEqual(['redis:7']) + }) +}) + +describe('renderManifest', () => { + it('renders versions and a sorted image list', () => { + const manifest = renderManifest({ + chartVersion: '1.8.0', + appVersion: 'v0.8.18', + images: ['busybox:1.36', 'redis:7-alpine'], + }) + + expect(manifest).toContain('chartVersion: 1.8.0') + expect(manifest).toContain('appVersion: v0.8.18') + expect(manifest).toContain(' - busybox:1.36\n - redis:7-alpine\n') + }) + + it('ends with a trailing newline so the checked-in file is POSIX-clean', () => { + const manifest = renderManifest({ chartVersion: '1.0.0', appVersion: 'v1', images: ['a:1'] }) + + expect(manifest.endsWith('\n')).toBe(true) + expect(manifest.endsWith('\n\n')).toBe(false) + }) +}) diff --git a/scripts/generate-image-manifest.ts b/scripts/generate-image-manifest.ts new file mode 100644 index 00000000000..88f2182d554 --- /dev/null +++ b/scripts/generate-image-manifest.ts @@ -0,0 +1,158 @@ +#!/usr/bin/env bun +/** + * Generates the container image inventory for the Sim Helm chart. + * + * An operator mirroring Sim into a disconnected registry needs the complete set + * of images a install pulls, and today that set can only be recovered by reading + * `values.yaml` by hand. That misses images no values key names — the NVIDIA + * device plugin is written directly into `templates/gpu-device-plugin.yaml` — so + * a hand-built list is wrong in exactly the case that is hardest to notice: the + * mirror succeeds, and one pod pulls from the internet at install time. + * + * The inventory is therefore derived from the rendered chart rather than from + * values, and checked in so a chart change that adds an image has to update it. + * Tags stay unresolved here: digests belong to a release, not to the chart, and + * pinning them in a checked-in file would drift on every upstream rebuild. The + * chart's `sim.image` helper already accepts a per-image `digest`, so an operator + * pins at install time against the digests their own mirror resolved. + * + * Usage: + * bun run scripts/generate-image-manifest.ts + * bun run scripts/generate-image-manifest.ts --check + */ +import { readFile, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) +const ROOT = resolve(SCRIPT_DIR, '..') +const CHART_DIR = resolve(ROOT, 'helm/sim') +const OUTPUT_PATH = resolve(CHART_DIR, 'images.yaml') +const CHECK_MODE = process.argv.includes('--check') + +/** + * Render arguments. + * + * `ci/full-values.yaml` is the chart's own "every optional component enabled" + * profile, but it leaves `ollama.gpu.enabled` off, and the device plugin renders + * only under that key. Enabling it here is what makes this an inventory of every + * image the chart can pull rather than of one popular configuration. + */ +const RENDER_ARGS = [ + 'template', + 'sim', + CHART_DIR, + '--namespace', + 'sim', + '--values', + resolve(CHART_DIR, 'ci/full-values.yaml'), + '--set', + 'ollama.gpu.enabled=true', +] as const + +/** Pod-spec keys whose entries carry an image reference. */ +const CONTAINER_KEYS = new Set(['containers', 'initContainers', 'ephemeralContainers']) + +/** + * Collects every image reference in a set of rendered Kubernetes documents. + * + * Walks for container arrays by key rather than matching on workload kind, so a + * chart that grows a Job, DaemonSet, or bare Pod is covered without changing + * this: every workload nests its containers under the same three keys. + */ +export function collectImages(documents: readonly unknown[]): string[] { + const images = new Set() + + const visit = (node: unknown): void => { + if (Array.isArray(node)) { + for (const entry of node) visit(entry) + return + } + if (node === null || typeof node !== 'object') return + + for (const [key, value] of Object.entries(node)) { + if (CONTAINER_KEYS.has(key) && Array.isArray(value)) { + for (const container of value) { + if (container === null || typeof container !== 'object') continue + const image = (container as { image?: unknown }).image + if (typeof image === 'string' && image.length > 0) images.add(image) + } + } + visit(value) + } + } + + visit(documents) + return [...images].sort() +} + +/** Renders the checked-in manifest. */ +export function renderManifest(input: { + chartVersion: string + appVersion: string + images: readonly string[] +}): string { + const entries = input.images.map((image) => ` - ${image}`).join('\n') + + return `# Generated by \`bun run images:generate\`. Do not edit this file directly. +# +# Every container image a complete Sim install pulls, rendered from this chart +# with all optional components enabled. Mirror all of them into a disconnected +# registry before installing, then point the chart at your registry with +# \`global.imageRegistry\` and pin each image's \`digest\` to what your mirror +# resolved. +chartVersion: ${input.chartVersion} +appVersion: ${input.appVersion} +images: +${entries} +` +} + +/** Reads a scalar field from Chart.yaml without pulling in a YAML dependency for two keys. */ +function readChartField(chart: string, field: string): string { + const match = chart.match(new RegExp(`^${field}:\\s*"?([^"\\n]+)"?\\s*$`, 'm')) + if (!match) throw new Error(`Chart.yaml is missing a \`${field}\` field`) + return match[1].trim() +} + +function renderChart(): unknown[] { + const rendered = Bun.spawnSync(['helm', ...RENDER_ARGS], { cwd: ROOT }) + + if (!rendered.success) { + const stderr = rendered.stderr.toString().trim() + throw new Error( + rendered.exitCode === null + ? 'Could not run `helm`. Install the Helm CLI to regenerate the image inventory.' + : `helm template failed:\n${stderr}` + ) + } + + const documents = Bun.YAML.parse(rendered.stdout.toString()) + return Array.isArray(documents) ? documents : [documents] +} + +async function main(): Promise { + const chart = await readFile(resolve(CHART_DIR, 'Chart.yaml'), 'utf8') + const manifest = renderManifest({ + chartVersion: readChartField(chart, 'version'), + appVersion: readChartField(chart, 'appVersion'), + images: collectImages(renderChart()), + }) + + if (!CHECK_MODE) { + await writeFile(OUTPUT_PATH, manifest) + console.log(`Wrote ${OUTPUT_PATH}`) + return + } + + const current = await readFile(OUTPUT_PATH, 'utf8').catch(() => '') + if (current !== manifest) { + console.error( + `${OUTPUT_PATH} is out of date. Run \`bun run images:generate\` and commit the result.` + ) + process.exit(1) + } + console.log('Image inventory is up to date.') +} + +if (import.meta.main) await main() From b3824503cb9173b15345e8f0b0dbed29d2a21a03 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 2 Sep 2026 22:51:54 -0700 Subject: [PATCH 02/14] docs(self-hosting): correct claims that contradict the source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An audit of the 27 self-hosting and enterprise pages checked every concrete claim against the code it describes. This fixes what it found. Errors that made a documented path fail: - `openssl rand -hex 32` prints 64 hex characters, not the "32 hex chars" five entries claimed. Only ENCRYPTION_KEY and API_ENCRYPTION_KEY are length- validated; the rest are free-form minimums, so the fix differs per variable. - API_ENCRYPTION_KEY was absent from the whole Kubernetes path and listed as required elsewhere. It is optional, and unset means API keys are stored in plain text behind one warning — a silent failure documented nowhere. - The per-purpose S3 fallback was described backwards: knowledge-base, chat, copilot and profile-picture buckets resolve to an empty string, not a literal. The GCS table carried the mirror-image claim, and Azure had no warning at all. - Local uploads go to /app/uploads, not /uploads, and no compose file or chart mounts a volume there. - The remote-sandbox table omitted the immutable template and snapshot refs, so the documented configuration could not work. - The GKE BackendConfig snippet set a values key no template renders. - Retention deletion is scheduled by nothing in the chart, so a deployment that followed the docs deleted nothing. - Every "Settings → Enterprise/Security → X" path named nav groups that do not exist, across eleven enterprise pages. Claims that understated what ships: - The outbound-proxy section said no platform traffic can be proxied. The server runs on Bun, whose fetch honors the proxy variables, so provider SDK and Resend traffic already proxies; guarded egress, SMTP, object storage and OTLP do not. Replaced with a per-path table, including the constraint that the per-request proxyUrl refuses a private address. - Telemetry is off by default on Helm, not on, and enabling the collector collects nothing until NEXT_TELEMETRY_DISABLED is cleared. - The CronJob count was 18 in two places; there are 22 in both the chart and the crontab. - The permission-group table was stale throughout, named a deployment key that does not exist, and inverted the checkbox semantic. About fifteen governed toggles were undocumented. Also documents ~25 environment variables nothing described, the migration advisory lock and its operational knobs, the CloudWatch metrics that activate on any deployment with AWS credentials, and sim-setup doctor. Redundancy removed where a page restated itself: FAQ blocks that repeated their own body, four drifting copies of the same tables, and three cloud tabs identical but for a filename. The sandbox base-image runbook moves from the enterprise enablement page, where it was out of place, to its own self-hosting page rather than being lost. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015BwsJTEQRzWJaY4BRCkPZt --- .../platform/enterprise/access-control.mdx | 171 +++++++++--------- .../docs/platform/enterprise/audit-logs.mdx | 13 +- .../platform/enterprise/custom-blocks.mdx | 4 +- .../docs/platform/enterprise/data-drains.mdx | 31 +++- .../platform/enterprise/data-retention.mdx | 33 +++- .../docs/platform/enterprise/forks.mdx | 13 +- .../docs/platform/enterprise/index.mdx | 34 ++-- .../docs/platform/enterprise/self-hosted.mdx | 158 +++++----------- .../platform/enterprise/session-policies.mdx | 2 +- .../content/docs/platform/enterprise/sso.mdx | 4 +- .../platform/enterprise/usage-tracking.mdx | 3 + .../platform/enterprise/verified-domains.mdx | 17 +- .../platform/enterprise/whitelabeling.mdx | 87 ++++++++- .../platform/self-hosting/architecture.mdx | 4 +- .../platform/self-hosting/authentication.mdx | 11 +- .../platform/self-hosting/background-jobs.mdx | 17 +- .../docs/platform/self-hosting/desktop.mdx | 48 ++++- .../docs/platform/self-hosting/docker.mdx | 22 ++- .../docs/platform/self-hosting/email.mdx | 13 +- .../self-hosting/environment-variables.mdx | 87 ++++----- .../docs/platform/self-hosting/index.mdx | 18 +- .../self-hosting/integrations-oauth.mdx | 4 +- .../docs/platform/self-hosting/kubernetes.mdx | 57 ++---- .../docs/platform/self-hosting/meta.json | 1 + .../docs/platform/self-hosting/networking.mdx | 79 ++++++-- .../platform/self-hosting/object-storage.mdx | 68 +++---- .../platform/self-hosting/observability.mdx | 95 +++++----- .../docs/platform/self-hosting/platforms.mdx | 1 - .../docs/platform/self-hosting/redis.mdx | 10 +- .../docs/platform/self-hosting/sandboxes.mdx | 121 +++++++++++++ .../docs/platform/self-hosting/scaling.mdx | 13 +- .../docs/platform/self-hosting/security.mdx | 66 ++++++- .../platform/self-hosting/troubleshooting.mdx | 31 ++-- .../docs/platform/self-hosting/upgrades.mdx | 65 +++++-- .../docs/platform/self-hosting/verify.mdx | 35 +++- 35 files changed, 894 insertions(+), 542 deletions(-) create mode 100644 apps/docs/content/docs/platform/self-hosting/sandboxes.mdx diff --git a/apps/docs/content/docs/platform/enterprise/access-control.mdx b/apps/docs/content/docs/platform/enterprise/access-control.mdx index 3ff1b78f6f2..2c1aa779d46 100644 --- a/apps/docs/content/docs/platform/enterprise/access-control.mdx +++ b/apps/docs/content/docs/platform/enterprise/access-control.mdx @@ -33,9 +33,9 @@ When a user runs a workflow or uses Chat, Sim reads the resolved group's configu ## Setup -### 1. Open Access Control settings +### 1. Open Permission groups settings -Go to **Settings → Enterprise → Access Control** from any workspace in your organization. Permission groups are defined once at the organization level and apply to every workspace under it. Only organization owners and admins can manage them. +Go to **Settings → Organization → Permission groups** from any workspace in your organization. Permission groups are defined once at the organization level and apply to every workspace under it. Only organization owners and admins can manage them. Access Control settings showing a list of permission groups: Contractors, Sales, Engineering, and Marketing, each with Details and Delete actions @@ -45,11 +45,15 @@ Click **+ Create** and enter a name (required) and optional description. A group ### 3. Configure permissions -Click **Details** on a group, then open **Configure Permissions**. Non-default groups have a **Members** tab plus three restriction tabs; the default group has only the restriction tabs. +Click **Details** on a group to open its configuration. It has four tabs — **General**, **Model Providers**, **Blocks**, and **Platform**. Changes are buffered until you save. -#### Members +Throughout the editor, a **checked** box means allowed. Clearing a checkbox is what applies a restriction. -A workspace-scoped group with **no members** applies to everyone in its workspaces (including external members). Add members here — searching your organization by name or email — to restrict the group to only those people; removing every member returns it to governing everyone. The default group ignores members and has no Members tab. +#### General + +Holds the group's **Name** and **Description**, the **Default group** switch, the **Workspaces** the group governs, and its **Members**. + +A workspace-scoped group with **no members** applies to everyone in its workspaces (including external members). Add members here — searching your organization by name or email — to restrict the group to only those people; removing every member returns it to governing everyone. The default group ignores members and governs every workspace in the organization. #### Model Providers @@ -60,6 +64,8 @@ Controls which AI model providers members of this group can use. - **All checked (default):** All providers are allowed. - **Subset checked:** Only the selected providers are allowed. Any workflow block or agent using a provider not on the list will fail at execution time. +Expand a provider row to reach its **model denylist**. Clearing individual models blocks exactly those models while leaving the rest of the provider available — useful when a provider is sanctioned but a specific model is not. + #### Blocks Controls which workflow blocks members can place and execute. @@ -69,81 +75,114 @@ Controls which workflow blocks members can place and execute. - **All checked (default):** All blocks are allowed. - **Subset checked:** Only the selected blocks are allowed. Workflows that already contain a disallowed block will fail when run — they are not automatically modified. +Expand an integration block to reach its **tool denylist**. Clearing individual tools blocks those operations while leaving the rest of the integration usable — for example, allowing a member to read from a service but not to delete in it. + The `start_trigger` block (the entry point of every workflow) is always allowed and cannot be restricted. #### Platform -Controls visibility of platform features and modules. +Controls the modules, actions, and credentials available to group members. Every row refuses at the API, not only in the UI — clearing a box revokes the access, it does not merely hide a tab. -Platform tab showing feature toggles grouped by category: Sidebar (Knowledge Base, Tables), Workflow Panel (Copilot), Settings Tabs, Tools, Deploy Tabs, Features, Logs, and Collaboration Each checkbox maps to a specific feature; checking it hides or disables that feature for group members. +Platform tab showing feature toggles grouped by category: Modules, Knowledge Base, Tables, Files, Deployment, Tools, Logs, Collaboration, and Credentials & Access -**Sidebar** +**Modules** -| Feature | Effect when checked | -|---------|-------------------| -| Knowledge Base | Hides the Knowledge Base section from the sidebar | -| Tables | Hides the Tables section from the sidebar | +| Feature | What clearing it withholds | +|---------|---------------------------| +| Chat | Revokes Chat. Members cannot ask Sim to build or edit anything. | +| Sim Mailer | Revokes the Sim Mailer inbox. Members cannot read or send mail. | -**Workflow Panel** +**Knowledge Base** -| Feature | Effect when checked | -|---------|-------------------| -| Copilot | Hides the Copilot panel inside the workflow editor | +| Feature | What clearing it withholds | +|---------|---------------------------| +| Knowledge Base | Revokes the Knowledge Base module. Members cannot open, search, or query any knowledge base. | +| Knowledge Base Creation | Prevents creating knowledge bases, leaving existing ones queryable. | +| Knowledge Base Uploads | Prevents uploading local documents, leaving sanctioned connectors as the only source. | -**Settings Tabs** +The **Knowledge Base** row also carries a **connector allowlist** — *Connectors knowledge bases may sync from*. Leave it untouched to allow every connector, or select a subset to limit which external sources a knowledge base may sync. -| Feature | Effect when checked | -|---------|-------------------| -| Integrations | Hides the Integrations tab in Settings | -| Secrets | Hides the Secrets tab in Settings | -| API Keys | Hides the Sim Keys tab in Settings | -| Files | Hides the Files tab in Settings | +**Tables** -**Tools** +| Feature | What clearing it withholds | +|---------|---------------------------| +| Tables | Revokes the Tables module. Members cannot read or write any table. | +| Table Creation | Prevents creating tables, leaving existing ones usable. | +| Table Export | Prevents downloading a whole table as CSV or JSON. | + +**Files** + +| Feature | What clearing it withholds | +|---------|---------------------------| +| Files | Revokes the Files module. Members cannot list, upload, or download workspace files. | +| Public Sharing | Revokes public file sharing. Members cannot create a share link. | +| Bulk Download | Prevents downloading folders as an archive. | -| Feature | Effect when checked | -|---------|-------------------| -| MCP Tools | Disables the use of MCP tools in workflows and agents | -| Custom Tools | Disables the use of custom tools in workflows and agents | -| Skills | Disables the use of Sim Skills in workflows and agents | +The **Public Sharing** row also carries an **auth-mode allowlist** — *Auth modes public file-share links may use* (Anyone with link, Password, Email, SSO). Select a subset to force share links onto stronger authentication. -**Deploy Tabs** +**Deployment** -| Feature | Effect when checked | -|---------|-------------------| -| API | Hides the API deployment tab | -| MCP | Hides the MCP deployment tab | -| Chat | Hides the Chat deployment tab | -| Template | Hides the Template deployment tab | +| Feature | What clearing it withholds | +|---------|---------------------------| +| Public API | Revokes public API access. Calls to a deployed workflow are refused. | +| API Deployment | Prevents deploying a workflow as an API endpoint. | +| MCP Server | Prevents exposing a workflow as an MCP server. | +| Chat Deployment | Prevents publishing a workflow as a chat. | +| Webhook Triggers | Prevents making a workflow reachable from an inbound webhook. | -**Features** +The **Chat Deployment** row also carries an **auth-mode allowlist** — *Auth modes chat deployments may use* (Public, Password, Email, SSO). -| Feature | Effect when checked | -|---------|-------------------| -| Sim Mailer | Hides the Sim Mailer (Inbox) feature | -| Public API | Disables public API access for deployed workflows | +**Tools** + +| Feature | What clearing it withholds | +|---------|---------------------------| +| MCP Tools | Blocks agents from calling MCP tools. | +| Custom Tools | Blocks agents from calling user-defined custom tools. | +| Skills | Blocks agents from loading skills. | +| Tool Auto-Approval | Prevents silencing a tool confirmation, so every call is confirmed again. | **Logs** -| Feature | Effect when checked | -|---------|-------------------| -| Trace Spans | Hides trace span details in execution logs | +| Feature | What clearing it withholds | +|---------|---------------------------| +| Trace Spans | Withholds per-block trace spans from logs and from the API. | +| Log Export | Prevents downloading execution logs as a CSV. | +| Execution Cost | Withholds execution cost. Logs and member exports omit cost and token spend; organization-level data drains, configurable by org admins only, are not projected. | **Collaboration** -| Feature | Effect when checked | -|---------|-------------------| -| Invitations | Disables the ability to invite new members to the workspace | +| Feature | What clearing it withholds | +|---------|---------------------------| +| Invitations | Prevents inviting anyone to a workspace or to the organization. | +| Workspace Creation | Prevents creating new workspaces, which no existing group would govern. | +| Member Directory | Withholds the member directory. Members cannot see the names or email addresses of other members. | + +**Credentials & Access** + +| Feature | What clearing it withholds | +|---------|---------------------------| +| Integrations | Revokes integration connections. Members cannot view, add, or remove an OAuth connection. | +| Secrets | Revokes secrets. Members cannot read, add, or change a workspace environment variable. | +| API Keys | Revokes workspace API keys. Members cannot list, create, or revoke one. | +| Personal API Keys | Prevents members from using a personal API key against this workspace. | +| Personal Credentials | Prevents connecting personal credentials, leaving only workspace-shared ones. | +| CLI Access | Prevents approving a CLI login, which mints a key for the public API. | + +##### Rows read from the organization default group + +Two rows — **Workspace Creation** and **Member Directory** — are read only from the organization's **default group**, because the act they govern names no workspace. On any other group the editor renders them inert, tags them **Organization**, and skips them in **Select All**. Set them on the default group. + +Five more rows — **Integrations**, **API Keys**, **Invitations**, **Personal API Keys**, and **CLI Access** — apply on the group in front of you for anything scoped to one of its workspaces, but fall back to the default group for the account-level path of the same action (minting a personal key, an organization-wide invitation, an account-level CLI login). To close one of these completely, set it on the default group as well. ### 4. Choose who it applies to -A workspace-scoped group applies to **all members of its workspaces by default** — including external members. To restrict it to specific people instead, open **Configure Permissions → Members** and add members by searching your organization by name or email. Removing every member returns the group to governing everyone in its workspaces. +A workspace-scoped group applies to **all members of its workspaces by default** — including external members. To restrict it to specific people instead, open the group's **Details → General** tab and add members by searching your organization by name or email. Removing every member returns the group to governing everyone in its workspaces. A user is governed by one group per workspace, so adding a user is rejected when it would conflict with another of their groups on a shared workspace (skipped rather than added in bulk). The default group ignores members entirely — it always governs everyone not covered by a workspace group. -Manage which workspaces a group governs from the **Workspaces** list in the group's **Details** view (Add and Remove). A non-default group is created targeting at least one workspace, but you can later remove all of them — a group with no workspaces simply governs nothing until you add one back. +Manage which workspaces a group governs from the **Workspaces** list on the same **General** tab. A non-default group is created targeting at least one workspace, but you can later remove all of them — a group with no workspaces simply governs nothing until you add one back. External workspace members (people who have access to a workspace but belong to a different organization) can't be added as named members, but a workspace-scoped group with no members — and the organization default group — still governs them. @@ -170,18 +209,6 @@ When a user opens Chat, their permission group is read before any block or tool --- -## User membership rules - -- A user can belong to **multiple** permission groups, but **at most one** group governs them in any given workspace. -- For a given workspace, a non-default group the user is an **explicit member** of takes precedence over a non-default **all-members** group (one with no members) targeting that workspace, which takes precedence over the organization's **default group**. -- A workspace has **at most one all-members group**, and a user is an explicit member of **at most one** group per workspace. Adding a user, adding a workspace, or removing a group's last member is rejected when it would violate this — memberships and scopes are never silently moved. -- A workspace-scoped group with **no members** governs everyone in its workspaces (including external members); add members to narrow it to specific people. -- Users not covered by any workspace group fall under the organization's **default group** if one is set; otherwise no restrictions are applied to them. -- Only one group per organization can be the **default group**; it always applies to all workspaces, ignores members, and also governs external workspace members. -- Personal or grandfathered workspaces that do not belong to an organization have no permission groups. - ---- - @@ -241,4 +248,4 @@ You can also set a server-level block allowlist using the `ALLOWED_INTEGRATIONS` ALLOWED_INTEGRATIONS=slack,gmail,agent,function,condition ``` -Once enabled, permission groups are managed through **Settings → Enterprise → Access Control** the same way as Sim Cloud. +Once enabled, permission groups are managed through **Settings → Organization → Permission groups** the same way as Sim Cloud. diff --git a/apps/docs/content/docs/platform/enterprise/audit-logs.mdx b/apps/docs/content/docs/platform/enterprise/audit-logs.mdx index 9be2eb67ad1..8869ca4991b 100644 --- a/apps/docs/content/docs/platform/enterprise/audit-logs.mdx +++ b/apps/docs/content/docs/platform/enterprise/audit-logs.mdx @@ -14,7 +14,7 @@ Audit logs give your organization a tamper-evident record of every significant a ### In the UI -Go to **Settings → Enterprise → Audit Logs** in your workspace. Logs are displayed in a table with the following columns: +Go to **Settings → Organization → Audit logs** in your workspace. Logs are displayed in a table with the following columns: Audit Logs settings showing a table of events with columns for Timestamp, Event, Description, and Actor, along with search and filter controls @@ -149,4 +149,13 @@ AUDIT_LOGS_ENABLED=true NEXT_PUBLIC_AUDIT_LOGS_ENABLED=true ``` -Once enabled, audit logs are viewable in **Settings → Enterprise → Audit Logs** and accessible via the API. +Once enabled, audit logs are viewable in **Settings → Organization → Audit logs** and accessible via the API. + +`GET /api/v1/audit-logs` requires a session and an organization on an Enterprise plan, so it is unreachable on a deployment that has no organization yet. The admin-key equivalent has neither requirement: + +```http +GET /api/v1/admin/audit-logs +x-admin-key: +``` + +It accepts the same filters as the organization endpoint plus `limit` (max 250) and `offset`, and returns entries across the whole deployment rather than one organization. `GET /api/v1/admin/audit-logs/` returns a single entry. Set `ADMIN_API_KEY` to use it — see the [self-hosted enterprise guide](/platform/enterprise/self-hosted). diff --git a/apps/docs/content/docs/platform/enterprise/custom-blocks.mdx b/apps/docs/content/docs/platform/enterprise/custom-blocks.mdx index a528416612d..2def2542e72 100644 --- a/apps/docs/content/docs/platform/enterprise/custom-blocks.mdx +++ b/apps/docs/content/docs/platform/enterprise/custom-blocks.mdx @@ -35,7 +35,7 @@ Custom blocks turn a workflow one team owns into infrastructure the whole organi ### 1. Open Custom blocks settings -Go to **Settings → Enterprise → Custom blocks** and click **Create block**. +Go to **Settings → Organization → Custom blocks** and click **Create block**. Custom blocks settings page listing a published block with its icon, name, and description, with a Create block button in the header @@ -102,7 +102,7 @@ Consumers don't need any access to the source workflow. The block runs on its ow ## Managing blocks -Open a block from **Settings → Enterprise → Custom blocks** to edit or delete it. +Open a block from **Settings → Organization → Custom blocks** to edit or delete it. - **Editing** changes only the block's presentation, interface, and trace policy — name, description, icon, input placeholders, exposed outputs, and whether runs are traced in consumer logs. The source workflow can't be re-pointed. - **Changing what the block does** is done by editing and **redeploying the source workflow**. The block picks up the new deployment automatically; there's nothing to republish. diff --git a/apps/docs/content/docs/platform/enterprise/data-drains.mdx b/apps/docs/content/docs/platform/enterprise/data-drains.mdx index 78c9b6c2686..ee10d56e32b 100644 --- a/apps/docs/content/docs/platform/enterprise/data-drains.mdx +++ b/apps/docs/content/docs/platform/enterprise/data-drains.mdx @@ -13,7 +13,7 @@ Drains are independent of [Data Retention](/platform/enterprise/data-retention) ## Setup -Go to **Settings → Enterprise → Data Drains** in your workspace, then click **New drain**. +Go to **Settings → Organization → Data drains** in your workspace, then click **New drain**. ![Data Drains settings page showing two configured drains — one exporting workflow logs to Amazon S3 daily, another exporting Copilot chats to an HTTPS webhook hourly](/static/enterprise/data-drains-list.png) @@ -207,10 +207,6 @@ After data lands in your bucket or webhook system, archive lifecycle (transition question: "Who can configure data drains?", answer: "Only organization owners and admins can view, create, edit, run, or delete drains. On Sim Cloud, the organization must be on an Enterprise plan." }, - { - question: "Will drained data be duplicated if a run fails?", - answer: "The drain cursor only advances on overall success, so a failure replays the same chunks on the next run. Every row has a stable `id` field and every webhook chunk has an `Idempotency-Key` header so receivers can dedupe." - }, { question: "Can I export multiple sources to the same destination?", answer: "Yes — create one drain per source, all pointing at the same bucket or endpoint. S3 destinations namespace by source automatically; webhook receivers can branch on the `X-Sim-Source` header." @@ -240,6 +236,27 @@ DATA_DRAINS_ENABLED=true NEXT_PUBLIC_DATA_DRAINS_ENABLED=true ``` -`NEXT_PUBLIC_DATA_DRAINS_ENABLED` shows the **Settings → Enterprise → Data Drains** page in the UI. `DATA_DRAINS_ENABLED` gates the server-side mutating endpoints and the cron dispatcher — when unset on a self-hosted deployment, drain create/update/delete/run requests return `404` and the dispatcher is a no-op. Both should be set to `true` together. +`NEXT_PUBLIC_DATA_DRAINS_ENABLED` shows the **Settings → Organization → Data drains** page in the UI. `DATA_DRAINS_ENABLED` gates the server-side mutating endpoints and the cron dispatcher — when unset on a self-hosted deployment, drain create/update/delete/run requests return `404` and the dispatcher is a no-op. Both should be set to `true` together. + +### Scheduling the dispatcher + +The dispatcher is an HTTP endpoint, not a self-scheduling job — something has to call it: + +``` +GET /api/cron/run-data-drains +``` + +It authenticates with a bearer token equal to `CRON_SECRET` and returns `401` when that variable is unset, so a self-hosted deployment must set it: + +```bash +CRON_SECRET=$(openssl rand -hex 32) +``` + +The Helm chart schedules this endpoint hourly for you (`cronjobs.jobs.runDataDrains`). Outside Helm, schedule it yourself: + +```bash +curl -H "Authorization: Bearer $CRON_SECRET" \ + https://sim.example.com/api/cron/run-data-drains +``` -Data Drains otherwise rely on the standard Trigger.dev background job infrastructure used elsewhere in Sim — no additional setup is required. The cron dispatcher runs hourly and fans out due drains as background jobs. +Each due drain is then fanned out as a `run-data-drain` background job, so the deployment also needs `TRIGGER_DEV_ENABLED` with a configured Trigger.dev project, or the local job backend. See [background jobs](/platform/self-hosting/background-jobs). diff --git a/apps/docs/content/docs/platform/enterprise/data-retention.mdx b/apps/docs/content/docs/platform/enterprise/data-retention.mdx index 819ab84ee01..800fede7d0e 100644 --- a/apps/docs/content/docs/platform/enterprise/data-retention.mdx +++ b/apps/docs/content/docs/platform/enterprise/data-retention.mdx @@ -18,7 +18,7 @@ Both are configured once at the **organization level** and apply to every worksp ## Setup -Go to **Settings → Enterprise → Data Retention** in your workspace. +Go to **Settings → Organization → Data retention** in your workspace. Data Retention settings showing the Retention policies list with the Organization default row and its summary of retention periods and PII stages @@ -97,10 +97,10 @@ The **Workflow input** and **Block outputs** stages alter what the workflow comp For each stage, choose the **entity types** to redact from the searchable grid. They are grouped as: -- **Common** — person name, email, phone, credit card, IP address, URL, IBAN, crypto wallet, medical license, VIN +- **Common** — person name, email, phone, credit card, IP address, location, date or time, URL, IBAN, crypto wallet, nationality/religious/political group, medical license, VIN - **United States** — SSN, passport, driver's license, bank account, ITIN - **United Kingdom** — NHS number, National Insurance number -- **Other regions** — Singapore, Australian, and Indian identifiers +- **Other regions** — Spanish (NIF, NIE), Italian (fiscal code, driver's licence, VAT code, passport, identity card), Polish (PESEL), Singaporean (NRIC/FIN, UEN), Australian (ABN, ACN, TFN, Medicare), Indian (PAN, Aadhaar, vehicle registration, voter ID, passport), and Finnish (personal identity code) identifiers The **Block outputs** stage is restricted to regex- and checksum-based recognizers, so it can run in-flight over large payloads without a performance penalty. Types that need name-model detection — person name, location, date or time — are not offered for that stage. @@ -182,7 +182,30 @@ NEXT_PUBLIC_DATA_RETENTION_ENABLED=true DATA_RETENTION_ENABLED=true ``` -Once enabled, retention settings are configurable through **Settings → Enterprise → Data Retention** the same way as Sim Cloud. +Once enabled, retention settings are configurable through **Settings → Organization → Data retention** the same way as Sim Cloud. + +### Scheduling the deletion pass + +`DATA_RETENTION_ENABLED` permits deletion; it does not perform it. Deletion runs when a scheduled request reaches one of three endpoints, each authenticated with a bearer token equal to `CRON_SECRET`: + +| Category | Endpoint | +|----------|----------| +| Execution and job logs | `GET /api/logs/cleanup` | +| Soft-deleted resources | `GET /api/cron/cleanup-soft-deletes` | +| Chats and Chat runs | `GET /api/cron/cleanup-tasks` | + + +The Helm chart does **not** schedule these three endpoints. An operator who sets `DATA_RETENTION_ENABLED=true` on the chart alone still deletes nothing. Add them to `cronjobs.jobs`, or call them daily from an external scheduler. + + +```bash +CRON_SECRET=$(openssl rand -hex 32) + +curl -H "Authorization: Bearer $CRON_SECRET" \ + https://sim.example.com/api/logs/cleanup +``` + +Each call fans the work out as background jobs, so the deployment also needs `TRIGGER_DEV_ENABLED` with a configured Trigger.dev project, or the local job backend. ### PII redaction @@ -193,4 +216,4 @@ PII redaction runs against a standalone [Presidio](https://microsoft.github.io/p PII_URL=http://localhost:5001 ``` -All PII stages are configurable under **Settings → Enterprise → Data Retention**. +All PII stages are configurable under **Settings → Organization → Data retention**. diff --git a/apps/docs/content/docs/platform/enterprise/forks.mdx b/apps/docs/content/docs/platform/enterprise/forks.mdx index 4301284fd9a..35d63a92d0b 100644 --- a/apps/docs/content/docs/platform/enterprise/forks.mdx +++ b/apps/docs/content/docs/platform/enterprise/forks.mdx @@ -29,7 +29,7 @@ On Sim Cloud, your organization may also need the feature turned on for your acc ### 1. Open Forks -Go to **Settings → Enterprise → Workspace Forks** in the workspace you want to fork from (or manage). +Go to **Settings → Organization → Workspace forks** in the workspace you want to fork from (or manage). Workspace Forks settings page showing Parent and Forks sections with Docs, See activity, and Create fork actions @@ -62,7 +62,7 @@ Click **Fork**. The child workspace is created immediately. Deployed workflows l ### 3. Open the parent edge (from the child) -Open the **child** workspace → **Settings → Enterprise → Workspace Forks**. On the **Parent** row, open the menu and choose **Edit mappings**. +Open the **child** workspace → **Settings → Organization → Workspace forks**. On the **Parent** row, open the menu and choose **Edit mappings**. Child rows (when you are on the parent) only offer **Open workspace** and **Disconnect** — mapping and sync are owned by the child configuring how it relates to its parent. @@ -373,13 +373,8 @@ Schedules, webhooks, and triggers are not live in the child until you **deploy** --- @@ -392,4 +387,4 @@ Self-hosted deployments turn Forks on with an environment variable instead of th |----------|-------------| | `FORKING_ENABLED`, `NEXT_PUBLIC_FORKING_ENABLED` | Enables workspace forking when billing is not used as the entitlement gate | -Once enabled, use the same **Settings → Enterprise → Workspace Forks** UI as Sim Cloud. Only workspace admins can manage forks. +Once enabled, use the same **Settings → Organization → Workspace forks** UI as Sim Cloud. Only workspace admins can manage forks. diff --git a/apps/docs/content/docs/platform/enterprise/index.mdx b/apps/docs/content/docs/platform/enterprise/index.mdx index fa3f62af3b1..79ac7ce46f6 100644 --- a/apps/docs/content/docs/platform/enterprise/index.mdx +++ b/apps/docs/content/docs/platform/enterprise/index.mdx @@ -3,31 +3,29 @@ title: Enterprise description: Enterprise features for business organizations --- -import { FAQ } from '@/components/ui/faq' - Sim Enterprise adds fine-grained access control, SSO, audit logging, compliance features, and workspace forking on top of Team plans. --- -## Access Control +## Permission groups -Define permission groups on a workspace to control what features and integrations its members can use. Permission groups are scoped to a single workspace — a user can belong to different groups (or no group) in different workspaces. +Define permission groups to control what features, models, blocks, and integrations your members can use. A permission group belongs to an **organization**. The organization's single **default group** governs everyone org-wide; every other group targets a specific set of workspaces and, by default, governs all members of those workspaces — or only named members once you add them. A user is governed by exactly one group in any given workspace. -External workspace members can be assigned to permission groups just like internal organization members, but they remain outside the organization roster and do not consume seats. +External workspace members can be governed by permission groups just like internal organization members, but they remain outside the organization roster and do not consume seats. -### Features +### What a group controls -- **Allowed Model Providers** - Restrict which AI providers users can access (OpenAI, Anthropic, Google, etc.) -- **Allowed Blocks** - Control which workflow blocks are available -- **Platform Settings** - Hide Knowledge Base, disable MCP tools, disable custom tools, or disable invitations +- **Model providers** - Restrict which AI providers members can use, and deny individual models within an allowed provider +- **Blocks** - Control which workflow blocks are available, and deny individual tools within an allowed integration +- **Platform** - Revoke modules (Chat, Knowledge Base, Tables, Files, Sim Mailer), deployment surfaces, tool types, log detail, collaboration actions, and credential access ### Setup -1. Navigate to **Settings** → **Access Control** in the workspace you want to manage +1. Navigate to **Settings** → **Organization** → **Permission groups** from any workspace in your organization 2. Create a permission group with your desired restrictions -3. Add workspace members to the permission group +3. Scope it to workspaces, and optionally add named members -Any workspace admin on an Enterprise-entitled workspace can manage permission groups. Users not assigned to any group have full access. Restrictions are enforced at both UI and execution time, based on the workflow's workspace. +Only organization owners and admins can manage permission groups. Users not governed by any group have full access. Restrictions are enforced at both UI and execution time, based on the organization that owns the workflow's workspace. See the [Access Control guide](/platform/enterprise/access-control) for full details. @@ -41,9 +39,9 @@ See the [SSO setup guide](/platform/enterprise/sso) for step-by-step instruction --- -## Whitelabeling +## White-labeling -Replace Sim's default branding — logos, product name, and favicons — with your own. See the [whitelabeling guide](/platform/enterprise/whitelabeling). +Replace Sim's default branding — logos, wordmark, product name, and theme colors — with your own. Instance-wide branding environment variables additionally cover the favicon and custom CSS. See the [white-labeling guide](/platform/enterprise/whitelabeling). --- @@ -77,14 +75,6 @@ Clone a workspace into a linked child, then push or pull **deployed** workflow c --- - - ---- - ## Self-hosted setup Self-hosted deployments unlock enterprise features through environment configuration instead of billing. One switch turns on the whole set: diff --git a/apps/docs/content/docs/platform/enterprise/self-hosted.mdx b/apps/docs/content/docs/platform/enterprise/self-hosted.mdx index b059b7df99d..c3e9ac3f4bf 100644 --- a/apps/docs/content/docs/platform/enterprise/self-hosted.mdx +++ b/apps/docs/content/docs/platform/enterprise/self-hosted.mdx @@ -12,7 +12,7 @@ On Sim Cloud, enterprise features are unlocked by an Enterprise subscription. Se There are two parts to getting this right, and skipping the second is the most common reason features appear to do nothing: 1. **Enable the features** with `ENTERPRISE_ENABLED`. -2. **Give them an organization to apply to.** Whitelabeling, PII redaction, permission groups, custom blocks, data drains, and audit scoping all read their settings from the organization that owns a workspace. A deployment where everyone works in personal workspaces has no organization for those settings to come from. +2. **Give them an organization to apply to.** White-labeling, PII redaction, permission groups, custom blocks, data drains, and audit scoping all read their settings from the organization that owns a workspace. A deployment where everyone works in personal workspaces has no organization for those settings to come from. ## Enable the feature set @@ -23,10 +23,10 @@ ENTERPRISE_ENABLED=true NEXT_PUBLIC_ENTERPRISE_ENABLED=true ``` -That turns on organizations, permission groups, SSO, whitelabeling, audit logs, -custom blocks, session policies, data retention, data drains, workspace forks, the Sandbox -entitlement, and the inbox. Sandboxes remain unavailable until their remote -provider and dedicated Function base are configured. +That turns on organizations, permission groups, SSO, white-labeling, audit logs, +usage tracking, custom blocks, session policies, data retention, data drains, workspace +forks, the Sandbox entitlement, and the inbox. Sandboxes remain unavailable until their +remote provider and dedicated Function base are configured. ### Turning one feature off @@ -49,130 +49,50 @@ The individual flags also work on their own if you would rather opt in one at a | SAML and OIDC sign-in | `SSO_ENABLED` | `NEXT_PUBLIC_SSO_ENABLED` | | Custom branding | `WHITELABELING_ENABLED` | `NEXT_PUBLIC_WHITELABELING_ENABLED` | | Audit logs | `AUDIT_LOGS_ENABLED` | `NEXT_PUBLIC_AUDIT_LOGS_ENABLED` | +| Usage tracking | `USAGE_MONITORING_ENABLED` | `NEXT_PUBLIC_USAGE_MONITORING_ENABLED` | | Custom blocks | `CUSTOM_BLOCKS_ENABLED` | `NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED` | | Session policies | `SESSION_POLICIES_ENABLED` | `NEXT_PUBLIC_SESSION_POLICIES_ENABLED` | | Data retention deletion | `DATA_RETENTION_ENABLED` | `NEXT_PUBLIC_DATA_RETENTION_ENABLED` | | Data drains | `DATA_DRAINS_ENABLED` | `NEXT_PUBLIC_DATA_DRAINS_ENABLED` | -| Workspace forks | `FORKING_ENABLED` | — | +| Workspace forks | `FORKING_ENABLED` | `NEXT_PUBLIC_FORKING_ENABLED` | | Sim Mailer inbox | `INBOX_ENABLED` | `NEXT_PUBLIC_INBOX_ENABLED` | | Sandboxes | `SANDBOXES_ENABLED` | `NEXT_PUBLIC_SANDBOXES_ENABLED` | -Sandboxes also need a remote execution provider and a dedicated Function base -image. Build and configure that base before enabling the UI; custom workspace -sandboxes layer their packages on top of it. +Sandboxes also need a remote execution provider and a dedicated Function base image before they can run anything. `SANDBOXES_ENABLED` grants the server-side entitlement; `NEXT_PUBLIC_SANDBOXES_ENABLED` projects provider readiness to the browser and exposes Shell plus custom Sandbox management. Set the public flag only after the selected provider has credentials and a valid immutable Function base configured. -For E2B: +JavaScript without `import` or `require` does not use the remote provider and continues to run in the local isolated VM when all Sandbox flags are off. Python, Shell, JavaScript with external imports, and selected custom Sandboxes fail with an explicit configuration error until the remote Function base is ready. -```bash -E2B_API_KEY=... \ - bun run apps/sim/scripts/build-function-e2b-template.ts \ - --name sim-function - -SANDBOX_PROVIDER=e2b -E2B_ENABLED=true -E2B_API_KEY=... -E2B_FUNCTION_TEMPLATE_ID=: -E2B_FUNCTION_TEMPLATE_GENERATION= -SANDBOXES_ENABLED=true -NEXT_PUBLIC_SANDBOXES_ENABLED=true -``` - -The builder uses E2B's maintained `code-interpreter-v1` base, assigns a fresh -release generation, and prints both runtime values. `--generation` remains -available for release automation, and `--base-template` accepts an immutable -base override when a deployment deliberately owns one. +See [Sandboxes](/platform/self-hosting/sandboxes) for the provider credentials, the Function base-image build, and the promotion procedure. -For Daytona, use the immutable snapshot ID printed by the builder. The API key needs -`write:snapshots` to build and `write:sandboxes` to execute: - -```bash -DAYTONA_API_KEY=... \ - bun run apps/sim/scripts/build-function-daytona-snapshot.ts \ - --name sim-function-2026-08-03 \ - --parity-manifest /tmp/function-sandbox-manifest.json - -SANDBOX_PROVIDER=daytona -DAYTONA_API_KEY=... -DAYTONA_FUNCTION_SNAPSHOT_ID= -SANDBOXES_ENABLED=true -NEXT_PUBLIC_SANDBOXES_ENABLED=true -``` - -`SANDBOXES_ENABLED` grants the server-side self-hosted entitlement. -`NEXT_PUBLIC_SANDBOXES_ENABLED` projects provider readiness to the browser and -exposes Shell plus custom Sandbox management. Set the public flag only after the -selected provider has credentials and a valid immutable Function base configured. -The Function language value itself is never conditioned on these flags, so a -saved Python block cannot be silently serialized or executed as JavaScript. + + Data retention is the one feature that deletes data. Its flag controls the cleanup pass, not the settings screen — retention windows are always configurable. Nothing is ever deleted until you enable it, and even then only against windows you configured explicitly. Sim never applies the hosted plan defaults to a self-hosted deployment. + -JavaScript without `import` or `require` does not use this remote provider and -continues to run in the local isolated VM when all Sandbox flags are off. Python, -Shell, JavaScript with external imports, and selected custom Sandboxes fail with -an explicit configuration error until the remote Function base is ready. +## Schedule the background jobs -Mothership's `function_execute` and `run_code` tools use Mothership's separate -shell image, including for JavaScript without imports. If the deployment uses -Mothership code tools, also configure the image produced by the Mothership -release process for the selected provider: +Two enterprise features do their work from a cron-driven HTTP endpoint rather than from the app process. Both endpoints authenticate with a bearer token equal to `CRON_SECRET`, and both return `401` when `CRON_SECRET` is unset: ```bash -# E2B -MOTHERSHIP_E2B_TEMPLATE_ID= - -# Daytona -DAYTONA_SHELL_SNAPSHOT_ID= +CRON_SECRET=$(openssl rand -hex 32) ``` -These values are selected only for workflow Copilot and workspace Mothership -code-tool calls. They never replace or act as a fallback for -`E2B_FUNCTION_TEMPLATE_ID` or -`DAYTONA_FUNCTION_SNAPSHOT_ID`; Function blocks and custom workspace sandboxes -continue to use the dedicated Function base. - -Use E2B as the release baseline before building or promoting Daytona: - -```bash -# 1. Verify the exact E2B Function build and capture its accepted package/runtime surface. -E2B_ENABLED=true \ -E2B_API_KEY=... \ -E2B_FUNCTION_TEMPLATE_ID=: \ -E2B_FUNCTION_TEMPLATE_GENERATION= \ -SANDBOX_PARITY_MANIFEST_OUT=/tmp/function-sandbox-manifest.json \ - bun run apps/sim/scripts/verify-sandbox-parity.ts - -# 2. Pin Daytona's reconstructed packages to that accepted E2B manifest. -DAYTONA_API_KEY=... \ - bun run apps/sim/scripts/build-function-daytona-snapshot.ts \ - --name sim-function-2026-08-03 \ - --parity-manifest /tmp/function-sandbox-manifest.json - -# 3. Verify the immutable Daytona snapshot against the same baseline before promotion. -SANDBOX_PROVIDER=daytona \ -DAYTONA_API_KEY=... \ -DAYTONA_FUNCTION_SNAPSHOT_ID= \ -SANDBOX_PARITY_MANIFEST_BASELINE=/tmp/function-sandbox-manifest.json \ - bun run apps/sim/scripts/verify-sandbox-parity.ts -``` +| Feature | Endpoint | Suggested schedule | Scheduled by the Helm chart | +|---------|----------|--------------------|-----------------------------| +| Data drains | `GET /api/cron/run-data-drains` | Hourly | Yes | +| Retention — logs | `GET /api/logs/cleanup` | Daily | **No** | +| Retention — soft deletes | `GET /api/cron/cleanup-soft-deletes` | Daily | **No** | +| Retention — Chat tasks | `GET /api/cron/cleanup-tasks` | Daily | **No** | - `E2B_FUNCTION_TEMPLATE_ID` and `DAYTONA_FUNCTION_SNAPSHOT_ID` fail closed when - unset or mutable. The E2B value must be an exact `