Skip to content

Commit db622ae

Browse files
waleedlatif1claude
andcommitted
fix(supply-chain): attest the real published tags, and make the GPU image mirrorable
Review was right on both blocking points, and both needed code rather than wording. `imagetools create` always writes an index, even from a single manifest — so `:<version>-amd64` is a single-entry index whose digest differs from the `:<sha>-amd64` manifest it wraps. Verified against a local registry: pushing a manifest at bd44eb13 and retagging it produced e857a74f. Attesting the manifest therefore left the tag people actually pin unverifiable, and a static matrix could not fix it: resolving the live tags for one image returns five distinct digests, and which of them exist depends on whether the run is a release and whether the latest guard passed. A new attest-subjects job resolves the published tags to digests, de-duplicates, and feeds attest-images as a dynamic matrix. Tags this run did not publish are skipped rather than failing it. The NVIDIA device plugin was hardcoded in the template, so the previous guidance — mirror it to the same path, or patch the DaemonSet — was not durable: copying to another registry does not redirect the pull, and a patch is reverted by the next helm upgrade. It now takes its image from ollama.gpu.devicePlugin.image and goes through the shared helper, so it honors global.imageRegistry with useRegistryForAllImages like every other third-party image. Default renders byte-identical; chart minor bumped for the new key. Docs corrections from the same round, several of them regressions from my own previous commit: - The ADFS example set SSO_ISSUER to the IdP identifier, and SSO_SAML_AUDIENCE defaults to SSO_ISSUER — so it silently broke audience validation. The example now sets both. - Compose does not pass MIGRATION_DATABASE_URL through from .env: its migrations service declares an explicit environment list. - CLI authentication requires Redis; it does not fall back, and redis.mdx already said so. - The rollback snippet assigned SIM_VERSION in a subshell that Compose never saw. - Custom blocks hide their internals only when tracing is off. - A new workspace is still governed by the organization's default group. - doctor is not a Kubernetes tool, so it cannot be the first step for Helm users. - ENTERPRISE_ENABLED needs its NEXT_PUBLIC twin. - The 100 MB response cap does not cover MCP's standalone SSE stream. - Generating CRON_SECRET in a shell configures nothing by itself. Renaming two headings broke published anchors that an in-repo library article links to; both now point at the new slugs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BwsJTEQRzWJaY4BRCkPZt
1 parent 433f859 commit db622ae

17 files changed

Lines changed: 123 additions & 79 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 85 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -620,26 +620,93 @@ jobs:
620620
# Attaching attestations here instead leaves the index itself untouched — they
621621
# are stored as separate referrer manifests that point at it.
622622
#
623-
# Every pullable tag is covered. `create-ghcr-manifests` publishes both
624-
# multi-arch tags (`:sha`, `:version`, `:latest`) and single-arch ones
625-
# (`:version-amd64`, `:latest-arm64`, …), and those resolve to different
626-
# digests — an index digest and the two per-arch manifest digests. The matrix
627-
# therefore attests all three subjects per image. `imagetools create` is
628-
# deterministic, so the version and latest indexes share the sha index's
629-
# digest and need no separate attestation.
623+
# Resolve the set of digests that actually got published, so the attestation
624+
# job below covers every tag a customer can pull.
630625
#
631-
# Per-arch subjects also give a truthful SBOM: the amd64 and arm64 images
632-
# contain different packages, and a single SBOM attached to the index cannot
633-
# describe both.
626+
# A static list is not enough. `imagetools create` always writes an INDEX, so
627+
# `:<version>-amd64` is a single-entry index whose digest differs from the
628+
# `:<sha>-amd64` manifest it wraps — attesting the manifest leaves the tag
629+
# people actually pin unverifiable. Which tags exist also varies per run:
630+
# version tags only on a release, and the latest tags only when the monotonic
631+
# guard in create-ghcr-manifests passed. Resolving tag -> digest here and
632+
# de-duplicating is what keeps the two in step without hardcoding that logic
633+
# twice.
634+
attest-subjects:
635+
name: Resolve Attestation Subjects
636+
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
637+
timeout-minutes: 10
638+
needs: [create-ghcr-manifests, detect-version]
639+
if: >-
640+
!cancelled() &&
641+
needs.create-ghcr-manifests.result == 'success' &&
642+
needs.detect-version.result == 'success' &&
643+
github.event_name == 'push' && github.ref == 'refs/heads/main'
644+
permissions:
645+
contents: read
646+
packages: read
647+
outputs:
648+
subjects: ${{ steps.resolve.outputs.subjects }}
649+
steps:
650+
- name: Login to GHCR
651+
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
652+
with:
653+
registry: ghcr.io
654+
username: ${{ github.repository_owner }}
655+
password: ${{ secrets.GITHUB_TOKEN }}
656+
657+
- name: Resolve published tags to digests
658+
id: resolve
659+
env:
660+
IS_RELEASE: ${{ needs.detect-version.outputs.is_release }}
661+
VERSION: ${{ needs.detect-version.outputs.version }}
662+
SHA: ${{ github.sha }}
663+
run: |
664+
set -euo pipefail
665+
666+
IMAGES="simstudio migrations realtime pii cron"
667+
TAGS="${SHA} ${SHA}-amd64 ${SHA}-arm64 latest latest-amd64 latest-arm64"
668+
if [ "${IS_RELEASE}" = "true" ]; then
669+
TAGS="${TAGS} ${VERSION} ${VERSION}-amd64 ${VERSION}-arm64"
670+
fi
671+
672+
: > /tmp/subjects.jsonl
673+
for name in $IMAGES; do
674+
image="ghcr.io/simstudioai/${name}"
675+
seen=""
676+
for tag in $TAGS; do
677+
# A tag that this run did not publish (no release, or the latest
678+
# guard held it back) is expected to be missing — skip it rather
679+
# than fail the run.
680+
digest="$(docker buildx imagetools inspect "${image}:${tag}" \
681+
--format '{{json .Manifest}}' 2>/dev/null | jq -r '.digest // empty')"
682+
[ -n "$digest" ] || continue
683+
case " $seen " in *" $digest "*) continue ;; esac
684+
seen="$seen $digest"
685+
jq -nc --arg image "$image" --arg digest "$digest" \
686+
'{image: $image, digest: $digest}' >> /tmp/subjects.jsonl
687+
done
688+
done
689+
690+
if [ ! -s /tmp/subjects.jsonl ]; then
691+
echo "::error::Resolved no image digests to attest"
692+
exit 1
693+
fi
694+
695+
echo "Resolved $(wc -l < /tmp/subjects.jsonl) distinct subjects:"
696+
cat /tmp/subjects.jsonl
697+
echo "subjects=$(jq -sc . /tmp/subjects.jsonl)" >> "$GITHUB_OUTPUT"
698+
699+
# One leg per distinct published digest. Attesting each subject separately is
700+
# also what makes the SBOMs truthful: the amd64 and arm64 images contain
701+
# different packages, and one SBOM attached to the index cannot describe both.
634702
attest-images:
635703
name: Attest Images
636704
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
637705
timeout-minutes: 15
638-
needs: [create-ghcr-manifests]
706+
needs: [attest-subjects]
639707
if: >-
640708
!cancelled() &&
641-
needs.create-ghcr-manifests.result == 'success' &&
642-
github.event_name == 'push' && github.ref == 'refs/heads/main'
709+
needs.attest-subjects.result == 'success'
643710
permissions:
644711
contents: read
645712
packages: write
@@ -649,15 +716,7 @@ jobs:
649716
strategy:
650717
fail-fast: false
651718
matrix:
652-
image:
653-
- ghcr.io/simstudioai/simstudio
654-
- ghcr.io/simstudioai/migrations
655-
- ghcr.io/simstudioai/realtime
656-
- ghcr.io/simstudioai/pii
657-
- ghcr.io/simstudioai/cron
658-
# The published tag suffixes, and so the distinct digests a customer can
659-
# pull. Empty is the multi-arch index.
660-
suffix: ['', '-amd64', '-arm64']
719+
include: ${{ fromJSON(needs.attest-subjects.outputs.subjects) }}
661720

662721
steps:
663722
- name: Login to GHCR
@@ -667,30 +726,10 @@ jobs:
667726
username: ${{ github.repository_owner }}
668727
password: ${{ secrets.GITHUB_TOKEN }}
669728

670-
# Resolved once and reused by every step below: signing a tag would sign
671-
# whatever that tag points at when the step runs, which is not necessarily
672-
# what this run published.
673-
#
674-
# `{{json .Manifest}}` piped through jq, not a bare `{{.Manifest.Digest}}`:
675-
# buildx renders a format string consisting only of a `.Manifest` field as
676-
# the human-readable inspect block rather than the field value, so the bare
677-
# form does not produce a parseable digest.
678-
- name: Resolve digest
679-
id: digest
680-
run: |
681-
REF="${{ matrix.image }}:${{ github.sha }}${{ matrix.suffix }}"
682-
DIGEST="$(docker buildx imagetools inspect "$REF" \
683-
--format '{{json .Manifest}}' | jq -r '.digest')"
684-
if [ -z "$DIGEST" ] || [ "$DIGEST" = "null" ]; then
685-
echo "::error::Could not resolve a digest for ${REF}"
686-
exit 1
687-
fi
688-
echo "value=${DIGEST}" >> "$GITHUB_OUTPUT"
689-
690729
- name: Generate SBOM
691730
uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2
692731
with:
693-
image: ${{ matrix.image }}@${{ steps.digest.outputs.value }}
732+
image: ${{ matrix.image }}@${{ matrix.digest }}
694733
format: spdx-json
695734
output-file: sbom.spdx.json
696735
# The action's own release upload is for workflows triggered by a
@@ -702,7 +741,7 @@ jobs:
702741
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
703742
with:
704743
subject-name: ${{ matrix.image }}
705-
subject-digest: ${{ steps.digest.outputs.value }}
744+
subject-digest: ${{ matrix.digest }}
706745
sbom-path: sbom.spdx.json
707746
# Stored alongside the image so a mirrored registry carries the
708747
# attestation with it, rather than only being retrievable from GitHub.
@@ -712,7 +751,7 @@ jobs:
712751
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
713752
with:
714753
subject-name: ${{ matrix.image }}
715-
subject-digest: ${{ steps.digest.outputs.value }}
754+
subject-digest: ${{ matrix.digest }}
716755
push-to-registry: true
717756

718757
- name: Install Cosign
@@ -722,7 +761,7 @@ jobs:
722761
# signature that admission controllers (Kyverno, the Sigstore policy
723762
# controller) verify before admitting a pod.
724763
- name: Sign image
725-
run: cosign sign --yes "${{ matrix.image }}@${{ steps.digest.outputs.value }}"
764+
run: cosign sign --yes "${{ matrix.image }}@${{ matrix.digest }}"
726765

727766
# Check if docs changed
728767
# Smallest runner on purpose: a depth-2 checkout plus a path filter, no

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ The **Chat Deployment** row also carries an **auth-mode allowlist** — *Auth mo
160160
| Feature | What clearing it withholds |
161161
|---------|---------------------------|
162162
| Invitations | Prevents inviting anyone to a workspace or to the organization. |
163-
| Workspace Creation | Prevents creating new workspaces. A new workspace falls outside every existing group's scope. |
163+
| Workspace Creation | Prevents creating new workspaces. A new one is not covered by any workspace-scoped group until you add it, though the organization's default group still governs it. |
164164
| Member Directory | Withholds the member directory. Members cannot see the names or email addresses of other members. |
165165

166166
**Credentials & Access**

‎apps/docs/content/docs/platform/enterprise/custom-blocks.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ A custom block always runs the **latest deployed version** of its source workflo
1414

1515
## Common uses
1616

17-
The block's author keeps the credentials and the workflow logic; consumers see only the inputs and outputs. Common patterns:
17+
The block's author keeps the credentials and the workflow logic; by default consumers see only the inputs and outputs, unless the author enables **Trace runs in consumer logs**. Common patterns:
1818

1919
- **Internal API gateway.** Wrap an authenticated internal or partner endpoint — "Create Ticket", "Charge Account", "Provision User" — behind a block that takes only the business inputs. Teammates call it without the base URL, API key, or auth headers, and when the endpoint changes you update one workflow instead of every consumer's.
2020
- **Blessed knowledge lookup.** Package a vetted retrieval pipeline — chunking, filters, reranking — as "Search Company Docs" with a single query input, so teams reuse the approved retrieval instead of each rebuilding it.

‎apps/docs/content/docs/platform/enterprise/data-drains.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,11 @@ GET /api/cron/run-data-drains
250250
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:
251251

252252
```bash
253-
CRON_SECRET=$(openssl rand -hex 32)
253+
openssl rand -hex 32
254254
```
255255

256+
Set the same value as `CRON_SECRET` on both the app and whatever invokes the endpoint. Generating it in your shell does not configure either one.
257+
256258
The Helm chart schedules this endpoint hourly for you (`cronjobs.jobs.runDataDrains`). Outside Helm, schedule it yourself:
257259

258260
```bash

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,10 @@ PII redaction runs against a standalone [Presidio](https://microsoft.github.io/p
213213

214214
```bash
215215
# The Presidio service exposing /analyze and /anonymize
216-
# Helm: http://<release>-pii.<namespace>.svc.cluster.local:5001
217-
# Docker Compose: http://<pii service name>:5001
218-
PII_URL=http://sim-pii.simstudio.svc.cluster.local:5001
216+
# Helm — substitute your release name and namespace
217+
PII_URL=http://<release>-pii.<namespace>.svc.cluster.local:5001
218+
# Docker Compose — the PII service name on your network
219+
# PII_URL=http://pii:5001
219220
```
220221

221222
All PII stages are configurable under **Settings → Organization → Data retention**.

‎apps/docs/content/docs/platform/enterprise/sso.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ NEXT_PUBLIC_APP_URL=https://your-instance.com \
387387
SSO_PROVIDER_TYPE=saml \
388388
SSO_PROVIDER_ID=adfs \
389389
SSO_ISSUER=https://adfs.company.com/adfs/services/trust \
390+
SSO_SAML_AUDIENCE=https://your-instance.com \
390391
SSO_DOMAIN=company.com \
391392
SSO_USER_EMAIL=admin@company.com \
392393
SSO_SAML_ENTRY_POINT=https://adfs.company.com/adfs/ls \

‎apps/docs/content/docs/platform/enterprise/verified-domains.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,6 @@ SSO_ENABLED=true
7171
NEXT_PUBLIC_SSO_ENABLED=true
7272
```
7373

74-
`ENTERPRISE_ENABLED` turns both on together. See the [self-hosted enterprise guide](/platform/enterprise/self-hosted).
74+
`ENTERPRISE_ENABLED` turns both on together, but it needs its own browser twin — set `NEXT_PUBLIC_ENTERPRISE_ENABLED` alongside it, or the server enables SSO while the browser still hides it. See the [self-hosted enterprise guide](/platform/enterprise/self-hosted).
7575

7676
Once enabled, verify domains from **Settings → Organization → Single sign-on**, in the **Verified domains** section above the identity provider configuration. The older `/workspace/<workspaceId>/settings/domains` path still resolves to the same page.

‎apps/docs/content/docs/platform/self-hosting/architecture.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ This holds essentially all durable state: workflows, runs, logs, users, organiza
5252

5353
### redis
5454

55-
Backs pub/sub, the Socket.IO adapter, the idempotency store, execution progress markers, distributed execution limits, and the CLI-auth approval store. The idempotency store, progress markers, and approval store fall back to Postgres or in-process state. Pub/sub falls back to a **process-local** emitter, which is fine on one replica and drops every cross-pod event on more than one. See [Redis](/platform/self-hosting/redis).
55+
Backs pub/sub, the Socket.IO adapter, the idempotency store, execution progress markers, distributed execution limits, and the CLI-auth approval store. The idempotency store and execution progress markers fall back to Postgres or in-process state. The CLI-auth approval store does not — CLI authentication requires Redis at any replica count. Pub/sub falls back to a **process-local** emitter, which is fine on one replica and drops every cross-pod event on more than one. See [Redis](/platform/self-hosting/redis).
5656

5757
### cron
5858

‎apps/docs/content/docs/platform/self-hosting/security.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ The allowlist replaces four separate escape hatches, so a few deployments that w
220220

221221
Every guarded outbound response is bounded. A caller that does not set its own limit gets the default of **100 MB**; exceeding it rejects the request with a payload-size error and destroys the socket rather than buffering the rest.
222222

223-
This is an easily misread cause of "a large download from an integration fails" — the failure looks like a broken connection to the third-party service rather than a limit Sim imposed. It applies to the guarded provenances above, not to the presigned object-storage upload path.
223+
This is an easily misread cause of "a large download from an integration fails" — the failure looks like a broken connection to the third-party service rather than a limit Sim imposed. It applies to the guarded provenances above, not to the presigned object-storage upload path, and not to MCP's standalone SSE stream, which is deliberately unbounded so a long-lived stream is not cut off.
224224

225225
## Client IP and forwarded headers
226226

‎apps/docs/content/docs/platform/self-hosting/troubleshooting.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Common issues and solutions
55

66
## Start here: `sim-setup doctor`
77

8-
Before working through anything below, run the built-in checker. It catches most self-hosting failures without you having to guess which subsystem is broken.
8+
On a Docker Compose or source install, run the built-in checker first. It catches most failures without you having to guess which subsystem is broken. It reads env files, so it is not a Kubernetes tool — on Helm, skip to the checks below.
99

1010
```bash
1111
npx sim-setup doctor

0 commit comments

Comments
 (0)