Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions .github/workflows/package_sandbox_kit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
name: Package Sandbox Kit

on:
# Only the specs, not the README beside them: the release bump PR rewrites each
# spec.yaml `version:`, and that is what should trigger a publish.
push:
branches:
- main
paths:
- devel/sandbox-kit/*/spec.yaml

permissions: read-all

jobs:
# Every directory under devel/sandbox-kit/ holding a spec.yaml is a kit, so
# adding one needs no change here.
discover:
name: Discover kits to publish
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
kits: ${{ steps.find.outputs.kits }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false
fetch-depth: 0 # needs the pushed range to diff each spec's version
- id: find
env:
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.sha }}
run: |
# Publish a kit only when its `version:` actually changed in this push.
# Editing a comment in a spec must not republish a released tag under a
# version that already means something, and a re-run of a partly failed
# publish still selects the kit, so `latest` and the attestation get
# reconciled rather than skipped.
kits=()
for spec in devel/sandbox-kit/*/spec.yaml; do
Comment thread
migmartri marked this conversation as resolved.
[ -e "${spec}" ] || continue
kit=$(basename "$(dirname "${spec}")")
new=$(yq -r '.version // ""' "${spec}")
if [[ -z "${new}" || "${new}" == "null" ]]; then
echo "::error::${spec} declares no version:"
exit 1
fi
# An unknown or absent `before` (new branch, force push) counts as changed.
old=""
if git cat-file -e "${BEFORE}:${spec}" 2>/dev/null; then
old=$(git show "${BEFORE}:${spec}" | yq -r '.version // ""')
fi
if [[ "${new}" != "${old}" ]]; then
echo "${kit}: ${old:-<none>} -> ${new}"
kits+=("${kit}")
else
echo "${kit}: unchanged at ${new}, skipping"
fi
done
printf '%s\n' "${kits[@]+"${kits[@]}"}" | jq -Rsc 'split("\n") | map(select(length > 0))' \
| sed 's/^/kits=/' >> $GITHUB_OUTPUT

package:
name: Package and push ${{ matrix.kit }}
needs: discover
# An empty matrix vector is an error, not a skip, so guard the whole job.
if: needs.discover.outputs.kits != '[]'
runs-on: ubuntu-latest
strategy:
# One kit's failure must not cancel the others mid-publish.
fail-fast: false
matrix:
kit: ${{ fromJSON(needs.discover.outputs.kits) }}
permissions:
contents: read
id-token: write # Docker Hub OIDC login, SLSA provenance and keyless kit signing
env:
CHAINLOOP_WORKFLOW_NAME: "sandbox-kit-package"
CHAINLOOP_PROJECT: "chainloop"
# Docker Sandboxes ships Linux packages only on tagged releases, not on
# nightly, so this is pinned to a stable tag and bumped by hand.
SBX_VERSION: "v0.43.0"
KIT_DIR: "devel/sandbox-kit/${{ matrix.kit }}"
KIT_REPO: "docker.io/chainloop/sbx-kit-${{ matrix.kit }}"
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false

- name: Read kit version
id: kit_version
run: |
# The kit's own spec.yaml is the source of truth; bump-chart-and-dagger-version.sh
# keeps it in step with the chart's appVersion on every release.
kit_version=$(yq -r '.version' "${KIT_DIR}/spec.yaml")
if [[ -z "${kit_version}" || "${kit_version}" == "null" ]]; then
echo "::error::${KIT_DIR}/spec.yaml declares no version:"
exit 1
fi
echo "kit_version=${kit_version}" >> $GITHUB_OUTPUT

- name: Install Chainloop
# Deliberately NOT `curl ... | bash`: this job holds an OIDC token that can
# mint Docker Hub credentials and sign artifacts, so the installer is
# fetched, pinned by digest and only then executed. The installer itself
# verifies the CLI's checksums (and cosign-verifies their signature), so
# this closes the remaining gap, which is the script in transit.
# If dl.chainloop.dev publishes a new installer this step fails with a
# digest mismatch; re-pin with:
# curl -sfL https://dl.chainloop.dev/cli/install.sh | sha256sum
env:
INSTALLER_SHA256: 6ebcdb8edc6f22c92b6ac31a363f7d60da6e04c60c4fadda44169f18492ba46e
run: |
Comment thread
migmartri marked this conversation as resolved.
curl -sfL https://dl.chainloop.dev/cli/install.sh -o /tmp/chainloop-install.sh
echo "${INSTALLER_SHA256} /tmp/chainloop-install.sh" | sha256sum -c -
bash /tmp/chainloop-install.sh

- name: Install Docker Sandboxes CLI
run: |
curl -sfL -o /tmp/sbx.deb \
"https://github.com/docker/sbx-releases/releases/download/${SBX_VERSION}/DockerSandboxes-linux-amd64-ubuntu2404.deb"
sudo apt-get install -y /tmp/sbx.deb
sbx version

# OIDC rather than a stored token: GitHub mints a short-lived identity token
# per run and Docker exchanges it for a registry token that expires with the
# job, so there is no long-lived Docker Hub credential in this repo. Access is
# governed by the connection's ruleset, which matches the subject claim
# repo:chainloop-dev/chainloop:ref:refs/heads/main - adding an `environment:`
# to this job would change that claim and stop it matching.
- name: Docker login to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
env:
DOCKERHUB_OIDC_CONNECTIONID: 361a9222-f648-4f62-baf4-5f500f7fbf54
with:
username: chainloop

- name: Validate kit
# Fails loudly here rather than halfway through a push if the pinned sbx
# release does not understand something the spec declares.
run: sbx kit validate "./${KIT_DIR}"

- name: Add Attestation (Sandbox Kit) and Push Kit
run: |
# KIT_VERSION arrives through env, not ${{ }} interpolation, so the value
# is never expanded into this script's source.

# Force the version declared in the kit spec and make sure it exists in
# the project by passing --existing-version; if it does not exist the
# attestation fails, and the version needs creating before a re-run.
chainloop attestation init --org chainloop --workflow ${CHAINLOOP_WORKFLOW_NAME} --project ${CHAINLOOP_PROJECT} --version ${KIT_VERSION} --existing-version

# Push the kit. --sign is keyless (Fulcio + Rekor) off the ambient
# GitHub OIDC token, and every push also attaches SLSA provenance.
sbx kit push "./${KIT_DIR}" "${KIT_REPO}:${KIT_VERSION}" --sign

# Move the floating tag to the same release. Note this is a second push
# rather than a retag, so :latest gets its own manifest digest even though
# the content is identical - compare the kit's `version:`, not the digest,
# to tell which release :latest currently points at.
sbx kit push "./${KIT_DIR}" "${KIT_REPO}:latest" --sign
Comment thread
migmartri marked this conversation as resolved.

# Attest the published kit. The immutable tag, not :latest, since the
# attestation should keep naming this artifact after the tag moves on.
chainloop attestation add --name sandbox-kit --value "${KIT_REPO}:${KIT_VERSION}"
env:
KIT_VERSION: ${{ steps.kit_version.outputs.kit_version }}
# Needed for commit signature verification: https://docs.chainloop.dev/concepts/attestations#commit-verification
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Finish and Record Attestation
if: ${{ success() }}
run: |
chainloop attestation push

- name: Mark attestation as failed
if: ${{ failure() }}
run: |
chainloop attestation reset

- name: Mark attestation as cancelled
if: ${{ cancelled() }}
run: |
chainloop attestation reset --trigger cancellation
14 changes: 13 additions & 1 deletion .github/workflows/utils/bump-chart-and-dagger-version.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash

# Bump Helm Chart version, appVersion to a given version number
# Bump Helm Chart version, appVersion, Dagger and sandbox kit versions to a given version number

set -e

Expand Down Expand Up @@ -59,3 +59,15 @@ if [[ -n "${platform_version}" && "${platform_version}" != "null" ]]; then
sed -i "s/platformVersion = \"v.*\"/platformVersion = \"${platform_version}\"/" "${dagger_main}"
fi

## Update the Docker Sandboxes kit versions
# Each kit declares the Chainloop release it belongs to, tracking semVer like
# appVersion does. `schemaVersion:` is left alone by the ^version anchor.
# Matching nothing is an error, not a no-op: a silent skip here would leave the
# specs at the old version and the publish workflow would never fire.
shopt -s nullglob
kit_specs=(devel/sandbox-kit/*/spec.yaml)
[ "${#kit_specs[@]}" -gt 0 ] || die "no kit specs found under devel/sandbox-kit (run from the repo root)"
for kit_spec in "${kit_specs[@]}"; do
sed -i "s#^version:.*#version: ${semVer}#" "${kit_spec}"
done

36 changes: 25 additions & 11 deletions devel/sandbox-kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ already wired in, so a session working on this repo is recorded as an
tools and MCP servers called, AI-vs-human line attribution — and attested to Chainloop without the developer
setting anything up.

`spec.yaml` here is the kit — self-contained, no secrets, and heavily commented; read it for the design
Full guide: **https://docs.chainloop.dev/guides/docker-sandboxes** _(publishing shortly — until it lands, this file is the reference)_

`claude/spec.yaml` is the kit — self-contained, no secrets, and heavily commented; read it for the design
rationale, the `extends: claude` inheritance notes, and the gRPC-vs-egress-proxy analysis. It started life in
the `chainloop-trace-docker-sandbox` PoC repo, which additionally carries the long-form write-up and the
running list of upstream Docker bugs.
Expand All @@ -31,12 +33,19 @@ interactive login inside the sandbox. Independent of everything Chainloop.

Every command here runs **from the repository root**, and both ways end up in the same place: this repo is
already initialized for `chainloop trace` (`.chainloop.yml` + the hooks in `.claude/settings.json`), so the
kit runs in **persistent** mode, takes its identity — org, project, workflow — from `.chainloop.yml`, and
pushes the attestation on `git push`.
kit takes its identity — org, project, workflow — from `.chainloop.yml` and pushes the attestation on
`git push`.

**The kit supports persistent tracing only, and refuses to start without it.** Point it at a repository that
has not been initialized and it exits with instructions to run `chainloop trace init` there first. The CLI's
other mode, `chainloop trace run`, is deliberately not offered: it ignores `.chainloop.yml` by design and its
teardown wipes `.git/chainloop-trace/` and strips the committed hooks — destructive on exactly the repos this
kit accepts. The trade-off is that **a session whose work is never pushed attests nothing**, so push from
inside the sandbox before it is reclaimed.

### 1. Through the environment file

The repo's `sbxenv.yaml` declares the agent, the kit, the clone-mode workspace and the trace mode, so the
The repo's `sbxenv.yaml` declares the agent, the kit and the clone-mode workspace, so the
only thing left to pass is the token:

```bash
Expand All @@ -51,7 +60,7 @@ value from your shell, and it overrides the kit's own default:
```bash
export CHAINLOOP_TOKEN=cl_...

sbx run --clone --kit ./devel/sandbox-kit -e CHAINLOOP_TOKEN chainloop-trace-claude
sbx run --clone -e CHAINLOOP_TOKEN ./devel/sandbox-kit/claude
```

…or skip the token entirely and **authenticate from your existing `chainloop auth login` session**, by
Expand All @@ -61,9 +70,9 @@ mounting the config the CLI already wrote on your machine:
CFG="$HOME/Library/Application Support/chainloop" # macOS
# CFG="$HOME/.config/chainloop" # Linux

sbx run --clone --kit ./devel/sandbox-kit \
sbx run --clone \
--kit-arg chainloopConfig="$CFG/config.toml" \
chainloop-trace-claude \
./devel/sandbox-kit/claude \
. "${CFG}:ro"
```

Expand All @@ -74,15 +83,15 @@ attach:

```
[chainloop-trace] Adopted chainloop config from /Users/…/chainloop/config.toml
[chainloop-trace] Repo already initialized for chainloop trace - persistent mode
[chainloop-trace] Repo initialized for chainloop trace - persistent mode
```

### Which to use

| | exported `$CHAINLOOP_TOKEN` | API token, explicit | `config.toml` |
| --- | --- | --- | --- |
| `sbx env run` | not supported — no `-e` flag, and nothing interpolates in the file | `--env-arg chainloopToken=…` | needs an overlay file (below) |
| `sbx run --kit` | `-e CHAINLOOP_TOKEN` | `--kit-arg chainloopToken=…` | `--kit-arg chainloopConfig=…` + a `:ro` mount |
| `sbx run ./devel/sandbox-kit/claude` | `-e CHAINLOOP_TOKEN` | `--kit-arg chainloopToken=…` | `--kit-arg chainloopConfig=…` + a `:ro` mount |

Supply one of them. With no token **and** no config the sandbox refuses to start rather than run an untraced
session — a session that records nothing is worse than one that never began, because you only find out when
Expand Down Expand Up @@ -111,9 +120,9 @@ same injection path, so it would flip the Chainloop hosts to the intercepted pat
printf 'chainloopToken=%s\n' "$CHAINLOOP_TOKEN" > ~/.config/chainloop/kit-args
chmod 600 ~/.config/chainloop/kit-args

sbx run --clone --kit ./devel/sandbox-kit \
sbx run --clone \
--kit-args-file ~/.config/chainloop/kit-args \
chainloop-trace-claude
./devel/sandbox-kit/claude
```

`sbx env run` has the equivalent `--env-args-file`.
Expand Down Expand Up @@ -167,6 +176,11 @@ in the entrypoint wrapper, so attach once first.
- `chainloopConfig` does not appear in `sbx env plan` — the plan only renders args pinned in a file. The
value still reaches the sandbox; its absence is not a failure.
- Mounts are fixed at creation. `sbx env run` on an existing sandbox re-attaches without re-provisioning.
- **This kit is `kind: sandbox`, so it *is* the agent** — it goes in `sbx run`'s positional slot, not behind
`--kit`, which takes mixins only. `sbx run --clone --kit ./devel/sandbox-kit/claude` fails with the unhelpful
`'sbx run' requires at least 1 argument`, because the flag swallowed the reference and left no agent to run.
`sbxenv.yaml` splits the same thing across two keys — `kits:` loads the artifact, `agent:` names what to run
from it — which is why the kit's own name appears there and nowhere on an `sbx run` line.

## Why nightly

Expand Down
Loading
Loading