Skip to content

Commit f7ab61d

Browse files
waleedlatif1claude
andcommitted
fix(supply-chain): scan the plain manifest so the SBOM step can run at all
The SBOM step could not have succeeded. Half the per-arch subjects are single-entry indexes — `imagetools create` writes an index even from one manifest — and Syft resolves an index against the runner's platform, so an arm64-only index fails outright on an amd64 runner with "no child with platform linux/arm64". That is 5 of 25 legs failing on every push to main, taking attest-images red and, through its gate, preventing any release from being created. Scanning by the `<sha>-<arch>` tag fixes it: that is the plain manifest the alias index wraps, so no platform resolution happens. It also halves the pulls, since both subjects for an architecture now share one scan of identical content. An empty subject list would also have opened the release gate — a matrix built from an empty include produces no legs and still reports success — so the gate now checks a published count rather than trusting a sibling job's guard. Two smaller corrections: `digest_of` no longer sleeps after its final attempt, which was costing 18 seconds per legitimately absent tag, and the alias comment now says what the code proves (content identity with this run's index) rather than implying provenance. The release gate's comment also now says what it gates. CodePipeline fires from the ECR tags that promote-images moves, upstream of attestation, and only the GHCR mirrors are signed — so this gates the GitHub release, not the deploy. Docs, each verified against source: - Restricted MCP, custom-tool and skill types fail the block; they are not silently skipped, and the same page already said so 180 lines later. - NO_PROXY needs every internal destination, not just model endpoints — the app calls realtime, Presidio, and itself over the same default fetch. - The proxy table omitted Google/Gemini and every other OpenAI-compatible provider, and omitted Azure Communication Services from the email row. - activeDeadlineSeconds caps a failing CronJob at five minutes, so it gets about one attempt rather than six retries. - Docker Compose does not schedule the retention endpoints either; the callout blamed Helm alone while the table beside it was already right. - Import-free JavaScript does leave the local VM when a block selects a sandbox or references a file path. - The GCS CORS list named headers that do not exist; the real key is sent server-side and never by the browser. - CRON_SECRET is optional, and nothing enforces the 32-character floor at runtime because env validation is skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BwsJTEQRzWJaY4BRCkPZt
1 parent ce7b3c2 commit f7ab61d

10 files changed

Lines changed: 50 additions & 23 deletions

File tree

.github/workflows/ci.yml

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,7 @@ jobs:
646646
packages: read
647647
outputs:
648648
subjects: ${{ steps.resolve.outputs.subjects }}
649+
count: ${{ steps.resolve.outputs.count }}
649650
steps:
650651
- name: Login to GHCR
651652
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
@@ -687,7 +688,9 @@ jobs:
687688
*"not found"*|*MANIFEST_UNKNOWN*|*"no such manifest"*|*"NAME_UNKNOWN"*) absent=1 ;;
688689
*) absent=0 ;;
689690
esac
690-
sleep "$((attempt * 3))"
691+
if [ "$attempt" -lt 3 ]; then
692+
sleep "$((attempt * 3))"
693+
fi
691694
done
692695
# Only call it absent if the registry said so on the final attempt.
693696
if [ "$absent" -eq 1 ]; then
@@ -729,8 +732,9 @@ jobs:
729732
emit "$image" "$digest" "$platform"
730733
done
731734
732-
# A moving alias is only ours if it resolves to the index this run
733-
# published. create-ghcr-manifests holds the latest tags back when
735+
# A moving alias is only taken when it resolves to the same index
736+
# digest this run published — content identity, which is what a
737+
# digest can prove. create-ghcr-manifests holds the latest tags back when
734738
# its monotonic guard sees a newer commit, and they then still point
735739
# at an older build — attesting those would put this run's signature
736740
# and provenance on an image it did not produce. The per-arch
@@ -781,6 +785,7 @@ jobs:
781785
echo "Resolved $(wc -l < /tmp/subjects.jsonl) distinct subjects:"
782786
cat /tmp/subjects.jsonl
783787
echo "subjects=$(jq -sc . /tmp/subjects.jsonl)" >> "$GITHUB_OUTPUT"
788+
echo "count=$(wc -l < /tmp/subjects.jsonl | tr -d ' ')" >> "$GITHUB_OUTPUT"
784789
785790
# One leg per distinct published digest. Attesting each subject separately is
786791
# also what makes the SBOMs truthful: the amd64 and arm64 images contain
@@ -816,11 +821,19 @@ jobs:
816821
# the SBOM it produces would describe amd64 while the index also serves
817822
# arm64. The per-arch subjects below carry an accurate SBOM each, and the
818823
# index still gets a signature and provenance.
824+
#
825+
# Scanned by the `<sha>-<arch>` tag rather than by `matrix.digest`. Half
826+
# the per-arch subjects are single-entry INDEXES (`imagetools create`
827+
# writes an index even from one manifest), and Syft resolves an index
828+
# against the RUNNER's platform — so an arm64-only index fails outright on
829+
# an amd64 runner with "no child with platform linux/arm64". The sha tag is
830+
# the plain manifest that index wraps: identical content, no platform
831+
# resolution, and one pull shared by both subjects instead of two.
819832
- name: Generate SBOM
820833
if: matrix.platform != 'index'
821834
uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2
822835
with:
823-
image: ${{ matrix.image }}@${{ matrix.digest }}
836+
image: ${{ matrix.image }}:${{ github.sha }}-${{ matrix.platform }}
824837
format: spdx-json
825838
output-file: sbom.spdx.json
826839
# The action's own release upload is for workflows triggered by a
@@ -896,13 +909,21 @@ jobs:
896909
name: Create GitHub Release
897910
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
898911
timeout-minutes: 10
899-
needs: [create-ghcr-manifests, attest-images, detect-version]
912+
needs: [create-ghcr-manifests, attest-subjects, attest-images, detect-version]
900913
# Explicit results: see migrate's comment. attest-images is a gate, not just
901914
# an ordering edge — a release must not advertise images whose signature or
902-
# attestation failed to publish.
915+
# attestation failed to publish. The count check is not redundant: a matrix
916+
# built from an empty include list produces no legs and still reports
917+
# success, so without it an empty subject set would open the gate.
918+
#
919+
# Note this gates the GitHub release, not the production deploy: CodePipeline
920+
# fires from the ECR tags moved by promote-images, upstream of this job, and
921+
# only the GHCR mirrors are attested.
903922
if: >-
904923
!cancelled() &&
905924
needs.create-ghcr-manifests.result == 'success' &&
925+
needs.attest-subjects.result == 'success' &&
926+
needs.attest-subjects.outputs.count != '0' &&
906927
needs.attest-images.result == 'success' &&
907928
needs.detect-version.result == 'success' &&
908929
needs.detect-version.outputs.is_release == 'true'

apps/docs/content/docs/platform/enterprise/access-control.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Assignment-time checks keep this unambiguous: a workspace has at most one all-me
2727
When a user runs a workflow or uses Chat, Sim reads the resolved group's configuration and applies it:
2828

2929
- **In the executor:** If a workflow uses a disallowed block type or model provider, execution halts immediately with an error. This applies to both manual runs and scheduled or API-triggered deployments.
30-
- **In Chat:** Disallowed blocks are filtered out of the block list so they cannot be added to a workflow. Disallowed tool types (MCP, custom tools, skills) are skipped if Sim attempts to use them.
30+
- **In Chat:** Disallowed blocks are filtered out of the block list so they cannot be added to a workflow. Disallowed tool types (MCP, custom tools, skills) make the block fail with an error if Sim attempts to use them — they are refused, not silently dropped.
3131

3232
---
3333

@@ -209,7 +209,7 @@ This applies regardless of how the workflow is triggered — manually, via API,
209209
When a user opens Chat, their permission group is read before any block or tool suggestions are made:
210210

211211
- Blocks not in the allowed list are filtered out of the block picker entirely — they do not appear as options.
212-
- If Sim generates a workflow step that would use a disallowed tool (MCP, custom, or skills), that step is skipped and the reason is noted.
212+
- If Sim generates a workflow step that would use a disallowed tool (MCP, custom, or skills), the run fails with an error naming the restriction.
213213

214214
---
215215

apps/docs/content/docs/platform/enterprise/data-retention.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ Once enabled, retention settings are configurable through **Settings → Organiz
195195
| Chats and Chat runs | `GET /api/cron/cleanup-tasks` |
196196

197197
<Callout type="warn">
198-
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.
198+
Neither shipped deployment schedules these three endpoints — not the Helm chart, not Docker Compose's `cron` service. An operator who sets `DATA_RETENTION_ENABLED=true` alone still deletes nothing. Add them to `cronjobs.jobs`, or call them daily from an external scheduler.
199199
</Callout>
200200

201201
```bash

apps/docs/content/docs/platform/enterprise/self-hosted.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Persist that value as `CRON_SECRET` on the app **and** on whatever calls these e
8585
| Retention — Chat tasks | `GET /api/cron/cleanup-tasks` | Daily | **No** — schedule it yourself |
8686

8787
<Callout type="warn">
88-
The Helm chart schedules the data-drain dispatcher but **not** the three retention cleanup endpoints. Setting `DATA_RETENTION_ENABLED=true` on the chart alone deletes nothing — the windows are evaluated only when one of those endpoints is called. Add them to `cronjobs.jobs` yourself, or drive them from an external scheduler.
88+
Both shipped deployments schedule the data-drain dispatcher but **not** the three retention cleanup endpoints — neither the Helm chart nor Docker Compose's `cron` service. Setting `DATA_RETENTION_ENABLED=true` alone deletes nothing — the windows are evaluated only when one of those endpoints is called. Add them to `cronjobs.jobs` yourself, or drive them from an external scheduler.
8989
</Callout>
9090

9191
```bash

apps/docs/content/docs/platform/self-hosting/background-jobs.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ cronjobs:
7171
enabled: true
7272
```
7373
74-
Each job runs a small `curlimages/curl` pod that calls the app's **in-cluster Service** (not the ingress), with `concurrencyPolicy: Forbid` so a slow run never overlaps itself. The Service load-balances that single call to one replica, so a job never fans out across pods. The chart keeps three completed and one failed Job per CronJob, but `ttlSecondsAfterFinished` defaults to 600, so a finished Job is deleted ten minutes after it ends — inspect a failure promptly or collect the logs. It sets no `backoffLimit`, so a failing job retries up to the Kubernetes default of six times before it is marked failed.
74+
Each job runs a small `curlimages/curl` pod that calls the app's **in-cluster Service** (not the ingress), with `concurrencyPolicy: Forbid` so a slow run never overlaps itself. The Service load-balances that single call to one replica, so a job never fans out across pods. The chart keeps three completed and one failed Job per CronJob, but `ttlSecondsAfterFinished` defaults to 600, so a finished Job is deleted ten minutes after it ends — inspect a failure promptly or collect the logs. It sets no `backoffLimit`, but `cronjobs.activeDeadlineSeconds` (default `300`) terminates the whole Job after five minutes, and the pod's own script already retries three times inside that window — so a failing job gets roughly one pod attempt and is then marked `DeadlineExceeded`, not six retries.
7575

7676
Disable individual jobs you do not need — billing reconciliation is the obvious one on a self-hosted install:
7777

apps/docs/content/docs/platform/self-hosting/environment-variables.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ import { Callout } from 'fumadocs-ui/components/callout'
1515
| `ENCRYPTION_KEY` | Encryption key, exactly 64 hex characters (32 bytes): `openssl rand -hex 32` |
1616
| `INTERNAL_API_SECRET` | Internal API secret, at least 32 characters: `openssl rand -hex 32` |
1717
| `NEXT_PUBLIC_APP_URL` | Public app URL |
18-
| `CRON_SECRET` | Bearer token for the background job endpoints: `openssl rand -hex 32`. **Required whenever background jobs are enabled** — the Helm chart's default, and the chart will not render without it. See [Background Jobs](/platform/self-hosting/background-jobs) |
1918

20-
`openssl rand -hex 32` prints 64 hex characters. `ENCRYPTION_KEY` and `API_ENCRYPTION_KEY` must be exactly that — a value of any other shape throws the first time Sim encrypts or decrypts, not at startup. The rest are secrets of no fixed shape and only need 32 characters or more.
19+
`openssl rand -hex 32` prints 64 hex characters. `ENCRYPTION_KEY` and `API_ENCRYPTION_KEY` must be exactly that — a value of any other shape throws the first time Sim encrypts or decrypts, not at startup. The rest are secrets of no fixed shape and only need 32 characters or more — a floor nothing enforces at runtime, so meeting it is on you.
2120

2221
<Callout type="error">
2322
`ENCRYPTION_KEY` and `API_ENCRYPTION_KEY` cannot be rotated or recovered. Losing either makes the data it protects permanently unreadable — workspace and personal environment variables, stored provider API keys, MCP OAuth credentials, and deployment/chat secrets in the first case, user-generated Sim API keys in the second. Back them up separately from the database.
@@ -28,6 +27,7 @@ import { Callout } from 'fumadocs-ui/components/callout'
2827
| Variable | Description |
2928
|----------|-------------|
3029
| `API_ENCRYPTION_KEY` | Encrypts user-generated Sim API keys at rest, exactly 64 hex characters (32 bytes): `openssl rand -hex 32`. **When unset, API keys are stored in plain text** — the app logs one warning and keeps working |
30+
| `CRON_SECRET` | Bearer token for the background job endpoints: `openssl rand -hex 32`. **Required whenever background jobs are enabled** — the Helm chart's default, and the chart will not render without it. See [Background Jobs](/platform/self-hosting/background-jobs) |
3131
| `REDIS_URL` | Redis connection string. Optional on a single replica; **required** past one app or realtime replica — see [Redis](/platform/self-hosting/redis) |
3232
| `REDIS_TLS_SERVERNAME` | TLS SNI override. Required when `REDIS_URL` uses `rediss://` with a bare IP, or the app throws the first time it opens a Redis connection |
3333
| `NEXT_PUBLIC_SOCKET_URL` | WebSocket URL — defaults to the page origin; set only if realtime is on a separate host |

apps/docs/content/docs/platform/self-hosting/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,4 @@ Sim is self-contained for the core editor and execution engine. A few features r
113113
| **Agent blocks** | An API key for at least one model provider | Or a self-hosted OpenAI-compatible endpoint: Ollama, vLLM, LM Studio, or LiteLLM. |
114114
| **Chat module** | `COPILOT_API_KEY` from sim.ai | Set `NEXT_PUBLIC_CHAT_DISABLED=true` to hide the module instead. |
115115
| **Integrations** | Your own OAuth app per service | See [Integrations & OAuth](/platform/self-hosting/integrations-oauth). |
116-
| **Remote Function / Pi execution** | Optional E2B or Daytona key, plus a Function base image | Without one, JavaScript Function code that has no `import` or `require` still runs in the in-process isolated VM. Python, Shell, JavaScript with external imports, and custom Function Sandboxes require a configured remote provider **and** an immutable Function base image — the key alone is not enough. Pi uses a separate image, pinned with `E2B_PI_TEMPLATE_ID` or `DAYTONA_PI_SNAPSHOT_ID`. See [Sandboxes](/platform/self-hosting/sandboxes). |
116+
| **Remote Function / Pi execution** | Optional E2B or Daytona key, plus a Function base image | Without one, JavaScript Function code that has no `import` or `require` still runs in the in-process isolated VM — unless it selects a sandbox or references a file path, which need a sandbox filesystem and are refused with a 422. Python, Shell, JavaScript with external imports, and custom Function Sandboxes require a configured remote provider **and** an immutable Function base image — the key alone is not enough. Pi uses a separate image, pinned with `E2B_PI_TEMPLATE_ID` or `DAYTONA_PI_SNAPSHOT_ID`. See [Sandboxes](/platform/self-hosting/sandboxes). |

apps/docs/content/docs/platform/self-hosting/networking.mdx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ TLS on GKE typically uses a **ManagedCertificate**, which the chart references b
237237
apiVersion: networking.gke.io/v1
238238
kind: ManagedCertificate
239239
metadata:
240-
name: sim-ssl-cert
240+
name: simstudio-ssl-cert
241241
namespace: simstudio
242242
spec:
243243
domains:
@@ -250,7 +250,7 @@ ingress:
250250
className: gce
251251
annotations:
252252
kubernetes.io/ingress.global-static-ip-name: "sim-ip"
253-
networking.gke.io/managed-certificates: "sim-ssl-cert"
253+
networking.gke.io/managed-certificates: "simstudio-ssl-cert"
254254
kubernetes.io/ingress.allow-http: "false"
255255
# TLS comes from the ManagedCertificate — leaving the chart's secret-based
256256
# TLS on makes the ingress reference a Secret that does not exist.
@@ -307,8 +307,8 @@ The server runs on Bun, and Bun's native `fetch` honors `$HTTP_PROXY`, `$HTTPS_P
307307
308308
| Outbound path | Honors `HTTP_PROXY` / `HTTPS_PROXY` |
309309
|---|---|
310-
| Model providers reached over the default `fetch` — Anthropic, OpenAI, Groq, Cerebras, xAI, Vertex, Ollama, LiteLLM | Yes |
311-
| Email via Resend, and Gmail sending | Yes |
310+
| Model providers reached over the default `fetch` — every provider except the three below: Anthropic, OpenAI, Google/Gemini, Vertex, Groq, Cerebras, xAI, Mistral, DeepSeek, OpenRouter, Together, Fireworks, Ollama, LiteLLM, and the other OpenAI-compatible providers | Yes |
311+
| Email via Resend, Azure Communication Services, and Gmail sending | Yes |
312312
| The desktop update feed's calls to GitHub | Yes |
313313
| Object storage — Azure Blob and GCS | Yes — their SDK pipelines read the proxy variables |
314314
| Everything through the SSRF guard — the HTTP block, tools, connectors, outbound webhooks, content fetches, MCP servers | No |
@@ -322,7 +322,14 @@ The server runs on Bun, and Bun's native `fetch` honors `$HTTP_PROXY`, `$HTTPS_P
322322
The practical consequence: a mandatory-egress-proxy environment can route most LLM traffic, Resend mail, and Azure/GCS storage through the proxy, but guarded integration calls, S3, Bedrock, SMTP, telemetry, and datastore traffic still need a transparent proxy or NAT-based egress.
323323
324324
<Callout type="info">
325-
Set `NO_PROXY` for any on-premises model endpoint. Self-hosted Ollama, LiteLLM, and vLLM are dialed over the default `fetch`, so a proxy that cannot reach your internal network will otherwise break them.
325+
Set `NO_PROXY` for every destination that is not on the public internet, not just model endpoints. The app reaches the realtime server (`SOCKET_SERVER_URL`), the Presidio PII service (`PII_URL`), and itself (`INTERNAL_API_BASE_URL`) over the same default `fetch`, alongside self-hosted Ollama, LiteLLM, and vLLM — so a proxy that cannot reach your internal network breaks live updates and PII redaction, not only inference.
326+
327+
```bash
328+
# Kubernetes
329+
NO_PROXY=localhost,127.0.0.1,.svc.cluster.local,sim-app,sim-realtime,sim-pii
330+
# Docker Compose
331+
NO_PROXY=localhost,127.0.0.1,simstudio,realtime
332+
```
326333
</Callout>
327334
328335
### The per-request escape hatch

apps/docs/content/docs/platform/self-hosting/object-storage.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,7 @@ cat > /tmp/cors.json <<'EOF'
311311
"x-goog-meta-knowledgebaseid",
312312
"x-goog-meta-folderid",
313313
"x-goog-meta-workflowid",
314-
"x-goog-meta-executionid",
315-
"x-goog-meta-simuploadid"
314+
"x-goog-meta-executionid"
316315
],
317316
"maxAgeSeconds": 3600
318317
}
@@ -325,7 +324,7 @@ done
325324
```
326325

327326
<Callout type="info">
328-
Header names must be listed individually — GCS CORS matches `responseHeader` entries exactly and does not support wildcards like `x-goog-meta-*`. `ETag` is required because large-file multipart uploads read each part's `ETag` from the browser, and CORS hides the header otherwise. `x-goog-if-generation-match` is required by Sim's create-only signed uploads, which prevent a reused upload URL from replacing existing bytes. `x-goog-meta-simuploadid` carries the opaque receipt used to verify an upload after an ambiguous network response.
327+
Header names must be listed individually — GCS CORS matches `responseHeader` entries exactly and does not support wildcards like `x-goog-meta-*`. `ETag` is required because large-file multipart uploads read each part's `ETag` from the browser, and CORS hides the header otherwise. `x-goog-if-generation-match` is required by Sim's create-only signed uploads, which prevent a reused upload URL from replacing existing bytes.
329328
</Callout>
330329

331330
</Step>

apps/docs/content/docs/platform/self-hosting/sandboxes.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ description: Configure the remote execution provider and Function base image tha
66
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
77
import { Callout } from 'fumadocs-ui/components/callout'
88

9-
Function blocks run in one of two places. JavaScript with no `import` or `require` runs in a local isolated VM and needs nothing configured. Python, Shell, JavaScript that imports, and custom workspace sandboxes run on a remote provider, and that provider needs both credentials and a dedicated Function base image.
9+
Function blocks run in one of two places. JavaScript with no `import` or `require` runs in a local isolated VM and needs nothing configured — unless the block selects a sandbox or references a file path from another block or a sandbox output path. Those need the sandbox filesystem and are refused with a 422 when no provider is configured. Python, Shell, JavaScript that imports, and custom workspace sandboxes run on a remote provider, and that provider needs both credentials and a dedicated Function base image.
1010

1111
Until the base image is configured, those languages fail with an explicit configuration error rather than falling back. A saved Python block is never silently serialized or executed as JavaScript.
1212

0 commit comments

Comments
 (0)