Skip to content

docs: make Lit Action examples standalone (#420) #1572

docs: make Lit Action examples standalone (#420)

docs: make Lit Action examples standalone (#420) #1572

# Phala / dstack simulator validation
#
# Builds the dstack guest-agent from source, runs it as a local simulator,
# and exercises the dstack::v1::dstack integration tests against it.
# Then runs the dstack-verifier against a live simulator quote to confirm
# that quote_verified=true — validating the get_quote() → dstack-verifier pipeline.
#
# Note on is_valid: the simulator's attestation.bin contains a synthetic OS image
# hash that is not published on download.dstack.org, so os_image_hash_verified and
# is_valid will always be false with the simulator. This is expected: the simulator
# is a developer tool for testing the attestation API, not a real TDX environment.
# We assert only quote_verified=true, which proves the pipeline is wired correctly.
#
# This job gates Phase 1 Workflow A: attestation mechanism (FR-2.5).
# It runs on every push and pull_request so simulator regressions are caught early.
#
# Isolation: each run gets a unique mktemp dir for the simulator's sockets so
# concurrent runners on the same self-hosted host do not conflict.
name: Phala Simulator Validation
on:
push:
branches: [main, next]
pull_request:
permissions:
contents: read
concurrency:
group: phala-simulator-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
simulator:
if: >
github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name == github.repository
name: dstack simulator attestation
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
# ── 1. Build the dstack simulator (cached by dstack HEAD SHA) ───────────
# The verifier is pulled as the dstacktee/dstack-verifier Docker image
# (same as verify-attestation.yml) — no source build or cache needed.
- name: Get dstack HEAD SHA
id: dstack-sha
run: |
SHA=$(git ls-remote https://github.com/Dstack-TEE/dstack.git HEAD | head -1 | cut -f1 | tr -d '[:space:]')
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
- name: Restore simulator cache
id: sim-cache
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/dstack-cache/simulator
key: dstack-simulator-${{ steps.dstack-sha.outputs.sha }}-${{ runner.os }}
- name: Fix dstack cache permissions
if: steps.sim-cache.outputs.cache-hit == 'true'
run: |
CACHE_DIR="${{ github.workspace }}/dstack-cache"
[ -d "$CACHE_DIR" ] && chmod -R u+rwX "$CACHE_DIR" || true
- name: Clone dstack repository
if: steps.sim-cache.outputs.cache-hit != 'true'
run: |
DSTACK_BUILD=$(mktemp -d)
echo "DSTACK_BUILD=$DSTACK_BUILD" >> "$GITHUB_ENV"
git clone --depth 1 https://github.com/Dstack-TEE/dstack.git "$DSTACK_BUILD"
# Use stable Rust for the dstack build (it may require features newer than 1.91).
- name: Install Rust (stable, for dstack)
if: steps.sim-cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@stable
- name: Build dstack simulator
if: steps.sim-cache.outputs.cache-hit != 'true'
run: cd "$DSTACK_BUILD/sdk/simulator" && bash build.sh
- name: Populate simulator cache
if: steps.sim-cache.outputs.cache-hit != 'true'
run: |
mkdir -p "${{ github.workspace }}/dstack-cache/simulator"
cp "$DSTACK_BUILD/sdk/simulator/dstack-simulator" \
"$DSTACK_BUILD/sdk/simulator/appkeys.json" \
"$DSTACK_BUILD/sdk/simulator/app-compose.json" \
"$DSTACK_BUILD/sdk/simulator/sys-config.json" \
"$DSTACK_BUILD/sdk/simulator/attestation.bin" \
"$DSTACK_BUILD/sdk/simulator/dstack.toml" \
"${{ github.workspace }}/dstack-cache/simulator/"
- name: Pull dstack-verifier image
run: docker pull --platform linux/amd64 dstacktee/dstack-verifier:latest
- name: Remove dstack build directory
if: always() && env.DSTACK_BUILD != ''
run: rm -rf "$DSTACK_BUILD"
# Switch to the pinned toolchain the project declares in rust-toolchain.toml.
- name: Install Rust 1.91 (lit-api-server toolchain)
uses: dtolnay/rust-toolchain@1.91
- name: Install protobuf-compiler
run: |
for i in 1 2 3 4 5; do
sudo apt-get update -qq && sudo apt-get install -y protobuf-compiler && break
echo "apt lock held, waiting 10s (attempt $i/5)…"
sleep 10
done
# ── 2. Unique-socket simulator run, unit tests ──────────────────────────
# Each CI run creates a fresh mktemp dir for the simulator's sockets.
# This allows multiple runners on the same host to operate in parallel
# without competing for /tmp/dstack/sdk/simulator/dstack.sock.
- name: Run phala simulator tests
run: |
set -eu
SIM_SRC="${{ github.workspace }}/dstack-cache/simulator"
# Create an isolated temp dir for this run's sockets and data files.
SIM_TMP=$(mktemp -d /tmp/dstack-sim-XXXXXX)
SIM_SOCK="$SIM_TMP/dstack.sock"
# Copy data files that the simulator reads via relative paths.
cp "$SIM_SRC/appkeys.json" "$SIM_SRC/app-compose.json" \
"$SIM_SRC/sys-config.json" "$SIM_SRC/attestation.bin" \
"$SIM_SRC/dstack.toml" "$SIM_TMP/"
# Start simulator; sockets are created relative to CWD ($SIM_TMP).
# exec replaces shell with simulator so SIM_PID refers to actual process (avoids zombies).
echo "Starting dstack simulator in $SIM_TMP..."
sh -c "cd '$SIM_TMP' && exec '$SIM_SRC/dstack-simulator'" >> "$SIM_TMP/dstack-simulator.log" 2>&1 &
SIM_PID=$!
for i in $(seq 1 15); do
[ -S "$SIM_SOCK" ] && echo "Socket ready." && break
echo " waiting for dstack.sock ($i/15)..."
sleep 1
done
[ -S "$SIM_SOCK" ] || {
echo "ERROR: dstack.sock never appeared"
cat "$SIM_TMP/dstack-simulator.log"
kill "$SIM_PID" 2>/dev/null || true
rm -rf "$SIM_TMP"
exit 1
}
# Run the Rust unit tests against the simulator socket.
(cd lit-api-server && DSTACK_SOCKET="$SIM_SOCK" cargo test --locked --features dstack -- dstack::v1::dstack::tests --nocapture)
STATUS=$?
# Teardown.
kill "$SIM_PID" 2>/dev/null || true
rm -rf "$SIM_TMP"
exit "$STATUS"
- name: Build lit-api-server
run: cargo build --locked --manifest-path=lit-api-server/Cargo.toml --features dstack --bin lit-api-server
# ── 3. dstack-verifier end-to-end attestation pipeline ─────────────────
# Start simulator, start lit-api-server (attestation-only; fetches from simulator),
# get attestation from lit-api-server's /attestation endpoint, run dstack-verifier.
# Assert quote_verified=true; is_valid will be false (simulator uses a synthetic
# OS image hash not published on download.dstack.org) — that is expected.
- name: Verify attestation pipeline with dstack-verifier
run: |
set -eu
SIM_SRC="${{ github.workspace }}/dstack-cache/simulator"
API_BIN="$(pwd)/lit-api-server/target/debug/lit-api-server"
SIM_TMP=$(mktemp -d /tmp/dstack-sim-XXXXXX)
SIM_SOCK="$SIM_TMP/dstack.sock"
cp "$SIM_SRC/appkeys.json" "$SIM_SRC/app-compose.json" \
"$SIM_SRC/sys-config.json" "$SIM_SRC/attestation.bin" \
"$SIM_SRC/dstack.toml" "$SIM_TMP/"
# Start simulator (exec avoids zombie processes).
sh -c "cd '$SIM_TMP' && exec '$SIM_SRC/dstack-simulator'" >> "$SIM_TMP/dstack-simulator.log" 2>&1 &
SIM_PID=$!
for i in $(seq 1 15); do
[ -S "$SIM_SOCK" ] && break
echo " waiting for simulator ($i/15)..."
sleep 1
done
[ -S "$SIM_SOCK" ] || {
echo "ERROR: dstack.sock never appeared"
cat "$SIM_TMP/dstack-simulator.log"
kill "$SIM_PID" 2>/dev/null || true
rm -rf "$SIM_TMP"
exit 1
}
# Copy branch-appropriate config (NodeConfig.toml is gitignored); main uses main.toml, others use next.toml.
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
cp lit-api-server/NodeConfig.main.toml lit-api-server/NodeConfig.toml
else
cp lit-api-server/NodeConfig.next.toml lit-api-server/NodeConfig.toml
fi
# Pick a random free TCP port per run so concurrent jobs on the
# same self-hosted host don't collide on the default Rocket port
# (8000). ROCKET_PORT overrides lit-api-server/Rocket.toml.
API_PORT=$(python3 -c 'import socket; s=socket.socket(); s.bind(("",0)); print(s.getsockname()[1]); s.close()')
echo "lit-api-server will bind to port $API_PORT"
(cd lit-api-server && DSTACK_SOCKET="$SIM_SOCK" ROCKET_PORT="$API_PORT" "$API_BIN") >> "$SIM_TMP/lit-api-server.log" 2>&1 &
API_PID=$!
if ! kill -0 "$API_PID" 2>/dev/null; then
echo "ERROR: lit-api-server failed to start"
cat "$SIM_TMP/lit-api-server.log"
kill "$SIM_PID" 2>/dev/null || true
rm -rf "$SIM_TMP"
exit 1
fi
# Wait for /attestation to respond (up to 5 min).
for i in $(seq 1 300); do
if curl -sf "http://localhost:$API_PORT/attestation" >/dev/null 2>&1; then
echo "lit-api-server /attestation ready."
break
fi
echo " waiting for lit-api-server ($i/300)..."
sleep 1
done
if ! curl -sf "http://localhost:$API_PORT/attestation" >/dev/null 2>&1; then
echo "ERROR: lit-api-server /attestation never responded"
cat "$SIM_TMP/lit-api-server.log"
kill "$SIM_PID" "$API_PID" 2>/dev/null || true
rm -rf "$SIM_TMP"
exit 1
fi
# Get attestation from lit-api-server (which got it from the simulator).
curl -sf "http://localhost:$API_PORT/attestation" > "$SIM_TMP/attestation.json"
# Teardown simulator and lit-api-server.
kill "$SIM_PID" "$API_PID" 2>/dev/null || true
# Fix empty digests and normalise format (same script used by verify-attestation.yml).
# Then inject spec_version=1 into vm_config if missing: the simulator omits it but
# the Docker verifier's serde parser requires it to be present in VmConfig.
# (The verifier will still report os_image_hash_verified=false and is_valid=false
# because the simulator's synthetic OS hash is not published — that is expected.)
python3 scripts/fix-attestation-event-log.py "$SIM_TMP/attestation.json" \
| python3 scripts/inject-spec-version.py \
> "$SIM_TMP/verify-request.json"
# Run verifier via Docker (same image as verify-attestation.yml).
# Exits 1 when is_valid=false (expected for simulator) — ignore and check quote_verified.
docker run --rm \
-v "$SIM_TMP:/verify" \
-w /verify \
--platform linux/amd64 \
dstacktee/dstack-verifier:latest \
--verify /verify/verify-request.json || true
# Assertion: quote_verified must be true.
RESULT_FILE="$SIM_TMP/verify-request.json.verification.json"
QUOTE_OK=$(python3 -c \
'import sys,json; v=json.load(open(sys.argv[1]))["details"]["quote_verified"]; print("true" if v else "false")' \
"$RESULT_FILE")
rm -rf "$SIM_TMP"
[ "$QUOTE_OK" = "true" ] || {
echo "ERROR: quote_verified=false — attestation pipeline is broken"
exit 1
}
echo "Attestation pipeline verified: quote_verified=true."