Skip to content
Open
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
67 changes: 36 additions & 31 deletions .github/workflows/stagex.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
- name: parser_cli
label: "parser_cli (Solana)"
- name: parser_gateway
- name: parser_grpc_server
steps:
- name: Checkout sources
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Expand Down Expand Up @@ -135,9 +136,10 @@ jobs:
docker image push --all-tags "ghcr.io/anchorageoss/${{ matrix.target.name }}"

- name: TVC deployment details
if: matrix.target.name == 'parser_app'
if: matrix.target.name == 'parser_app' || matrix.target.name == 'parser_gateway'
shell: bash
env:
TARGET_NAME: ${{ matrix.target.name }}
SEMVER_TAG: ${{ steps.build.outputs.semver_tag }}
SHA_TAG: ${{ steps.build.outputs.sha_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -146,26 +148,33 @@ jobs:
# the always-unique commit SHA on PR/branch builds where SEMVER
# isn't reproducible. Both reference the same digest below.
tvc_tag="${SEMVER_TAG:-$SHA_TAG}"
container_url="ghcr.io/anchorageoss/parser_app:${tvc_tag}"
container_url="ghcr.io/anchorageoss/${TARGET_NAME}:${tvc_tag}"
container_digest=$(docker inspect --format='{{index .RepoDigests 0}}' "${container_url}" | cut -d '@' -f 2)
docker create --name tmp-extract "${container_url}" /bin/true
docker cp tmp-extract:/parser_app /tmp/binary
docker rm tmp-extract
digest=$(sha256sum /tmp/binary | awk '{print $1}')
docker create --name tmp-extract-${TARGET_NAME} "${container_url}" /bin/true
docker cp "tmp-extract-${TARGET_NAME}:/${TARGET_NAME}" "/tmp/${TARGET_NAME}"
docker rm "tmp-extract-${TARGET_NAME}"
digest=$(sha256sum "/tmp/${TARGET_NAME}" | awk '{print $1}')
pinned_url="${container_url}@${container_digest}"
# Hardcoded: TVC currently only supports this QOS version.
qos_version="v2026.2.6"

# Build the deployment block once, then fan it out to both the run
# summary and (when a matching GitHub release exists) the release
# body. Operators reading either surface get the same paste-ready
# values.
deploy_md="${RUNNER_TEMP}/tvc_deploy.md"
# parser_app is the enclave-side binary (signs responses).
# parser_gateway is the host-side binary (verifies the signature
# against a pinned pubkey + handles x402 settlement). Operators
# need both digests pinned to actually establish the trust pair.
if [ "${TARGET_NAME}" = "parser_app" ]; then
role_note=$'\nThe enclave-side binary. Its ephemeral public key, signed by QOS during boot, becomes the gateway-side `TVC_DEMO_PINNED_PUBKEY_HEX`.'
else
role_note=$'\nThe host-side gateway. Pin the matching `parser_app` digest as the enclave it talks to; the gateway verifies every response against the enclave\'s ephemeral pubkey (pinned at boot via `TVC_DEMO_PINNED_PUBKEY_HEX` or `TVC_DEMO_PINNED_PUBKEY_FILE`).'
fi

deploy_md="${RUNNER_TEMP}/tvc_deploy_${TARGET_NAME}.md"
{
echo "## TVC Deployment Details"
echo "## TVC Deployment Details — ${TARGET_NAME}"
echo "${role_note}"
echo ""
echo "- **Container Image URL**: \`${pinned_url}\`"
echo "- **Executable path**: \`/parser_app\`"
echo "- **Executable path**: \`/${TARGET_NAME}\`"
echo "- **Expected Executable Digest**: \`sha256:${digest}\`"
echo "- **QOS Version**: \`${qos_version}\`"
echo ""
Expand All @@ -174,7 +183,7 @@ jobs:
echo '```bash'
echo "export CONTAINER_URL=\"${pinned_url}\""
echo 'cid=$(docker create "$CONTAINER_URL" /bin/true)'
echo 'docker cp "$cid:/parser_app" /tmp/binary'
echo "docker cp \"\$cid:/${TARGET_NAME}\" /tmp/binary"
echo 'docker rm "$cid"'
echo 'sha256sum /tmp/binary'
echo '```'
Expand All @@ -187,7 +196,7 @@ jobs:
echo 'tvc deploy init -o tvc-deploy.json'
echo '# Edit tvc-deploy.json to set:'
echo "# \"pivotContainerImageUrl\": \"${pinned_url}\""
echo '# "pivotPath": "/parser_app"'
echo "# \"pivotPath\": \"/${TARGET_NAME}\""
echo "# \"expectedPivotDigest\": \"sha256:${digest}\""
echo "# \"qosVersion\": \"${qos_version}\""
echo '# "appId": "<env-specific>"'
Expand All @@ -198,32 +207,28 @@ jobs:
cat "$deploy_md" >> "$GITHUB_STEP_SUMMARY"

# Mirror onto the GitHub release for this semver, when it exists.
# PR/branch builds have no SEMVER_TAG and no matching release;
# push-triggered stagex on main may race release.yml (which creates
# the release for the same commit); in either case just skip — the
# release-dispatched stagex run will populate it.
#
# Re-running stagex for the same release (manual UI re-run) must be
# idempotent: strip any existing block bracketed by our sentinel
# comments before appending the freshly-built one. The sentinels
# render as nothing in the release body.
# Each target uses its own sentinel pair so parser_app and
# parser_gateway can publish independently without clobbering each
# other when stagex runs them in parallel.
begin_sentinel="<!-- BEGIN_TVC_DEPLOY_${TARGET_NAME} -->"
end_sentinel="<!-- END_TVC_DEPLOY_${TARGET_NAME} -->"
if [ -n "$SEMVER_TAG" ] && gh release view "$SEMVER_TAG" >/dev/null 2>&1; then
existing_body=$(gh release view "$SEMVER_TAG" --json body --jq .body)
preserved=$(printf '%s\n' "$existing_body" | awk '
/<!-- BEGIN_TVC_DEPLOY -->/ { skipping = 1; next }
/<!-- END_TVC_DEPLOY -->/ { skipping = 0; next }
preserved=$(printf '%s\n' "$existing_body" | awk -v b="$begin_sentinel" -v e="$end_sentinel" '
$0 == b { skipping = 1; next }
$0 == e { skipping = 0; next }
!skipping
')
new_body="${RUNNER_TEMP}/release_notes.md"
new_body="${RUNNER_TEMP}/release_notes_${TARGET_NAME}.md"
{
printf '%s\n\n' "$preserved"
echo "<!-- BEGIN_TVC_DEPLOY -->"
echo "$begin_sentinel"
cat "$deploy_md"
echo "<!-- END_TVC_DEPLOY -->"
echo "$end_sentinel"
} > "$new_body"
gh release edit "$SEMVER_TAG" --notes-file "$new_body" \
--repo "${GITHUB_REPOSITORY}"
echo "Updated release $SEMVER_TAG with TVC deployment details."
echo "Updated release $SEMVER_TAG with TVC deployment details for ${TARGET_NAME}."
else
echo "No release to update (SEMVER_TAG='${SEMVER_TAG:-<unset>}'); skipping release-notes update."
fi
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
out
docs/superpowers/
.surfpool/
node_modules/
*.log
45 changes: 45 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,55 @@ out/parser_gateway/index.json: \
$(shell git ls-files images/parser_gateway src)
$(call build,parser_gateway)

out/parser_grpc_server/index.json: \
$(shell git ls-files images/parser_grpc_server src)
$(call build,parser_grpc_server)

out/mock_facilitator/index.json: \
$(shell git ls-files images/mock_facilitator src)
$(call build,mock_facilitator)

.PHONY: non-oci-docker-images
non-oci-docker-images:
docker buildx build --load --tag anchorageoss-visualsign-parser/parser_app -f images/parser_app/Containerfile .
docker buildx build --load --tag anchorageoss-visualsign-parser/parser_gateway -f images/parser_gateway/Containerfile .
docker buildx build --load --tag anchorageoss-visualsign-parser/parser_grpc_server -f images/parser_grpc_server/Containerfile .
docker buildx build --load --tag anchorageoss-visualsign-parser/mock_facilitator -f images/mock_facilitator/Containerfile .

# ── Local dev stacks ────────────────────────────────────────────────────────
#
# `dev-up-mock` — offline stack: parser_grpc_server + parser_gateway pointed
# at the bundled mock_facilitator. Useful when network egress
# to facilitator.payai.network isn't available.
# `dev-up-payai` — real-facilitator stack: parser_grpc_server + parser_gateway
# pointed at https://facilitator.payai.network with
# X402_NETWORK=solana-devnet. Requires public egress.
# Set TVC_DEMO_PINNED_PUBKEY_HEX before running this
# target; otherwise the gateway fail-closes.
# Both compose files consume the locally-built stagex images. Build them
# first with `make non-oci-docker-images`.
.PHONY: dev-up-mock dev-up-payai dev-down dev-logs

dev-up-mock: non-oci-docker-images
docker compose -f compose.mock.yml up -d

dev-up-payai: non-oci-docker-images
@if [ -z "$$TVC_DEMO_PINNED_PUBKEY_HEX" ]; then \
echo "ERROR: TVC_DEMO_PINNED_PUBKEY_HEX must be set (the gateway fail-closes without it for X402_PROFILE=payai)."; \
exit 1; \
fi
docker compose -f compose.payai.yml up -d

dev-down:
-docker compose -f compose.mock.yml down --remove-orphans 2>/dev/null || true
-docker compose -f compose.payai.yml down --remove-orphans 2>/dev/null || true

dev-logs:
@if docker compose -f compose.payai.yml ps -q 2>/dev/null | grep -q .; then \
docker compose -f compose.payai.yml logs -f --tail=100; \
else \
docker compose -f compose.mock.yml logs -f --tail=100; \
fi

define build_context
$$( \
Expand Down
46 changes: 46 additions & 0 deletions compose.mock.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Local-dev x402 stack with the bundled mock_facilitator (no network egress
# required). Builds the same stagex-built OCI images used in production —
# `make non-oci-docker-images` loads them into the docker daemon first.
#
# Use: `make dev-up-mock` (or `docker compose -f compose.mock.yml up`)
# Tear down: `make dev-down`

services:
mock_facilitator:
image: anchorageoss-visualsign-parser/mock_facilitator:latest
# stagex/core-busybox sets ENTRYPOINT ["/bin/sh"], so we override to
# exec the static-musl binary directly.
entrypoint: ["/mock_facilitator"]
environment:
MOCK_FACILITATOR_PORT: "8090"
ports:
- "8090:8090"

parser_grpc_server:
image: anchorageoss-visualsign-parser/parser_grpc_server:latest
entrypoint: ["/parser_grpc_server"]
environment:
EPHEMERAL_FILE: "/etc/parser/ephemeral.secret"
volumes:
# The test fixture key is fine for local dev; production TVC injects a
# real provisioned ephemeral key. NEVER ship this fixture into prod.
- ./src/integration/fixtures/ephemeral.secret:/etc/parser/ephemeral.secret:ro
ports:
- "44020:44020"

parser_gateway:
image: anchorageoss-visualsign-parser/parser_gateway:latest
entrypoint: ["/parser_gateway"]
depends_on:
- mock_facilitator
- parser_grpc_server
environment:
GATEWAY_PORT: "8080"
GRPC_ADDR: "http://parser_grpc_server:44020"
X402_PROFILE: "local"
X402_FACILITATOR_URL: "http://mock_facilitator:8090"
# TVC_DEMO_PINNED_PUBKEY_HEX intentionally omitted: local profile
# allows running without attestation. Set this env var here to opt
# into verification locally (the gateway will fail on mismatch).
ports:
- "8080:8080"
48 changes: 48 additions & 0 deletions compose.payai.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Local-dev x402 stack against the **real** payai facilitator on Solana
# **devnet**. Builds the same stagex-built OCI images used in production —
# `make non-oci-docker-images` loads them into the docker daemon first.
#
# Required env at host shell level (set before `make dev-up-payai`):
# X402_PAYTO=<base58 Solana receiver pubkey>
# — where payai will route the USDC. For local testing this can be
# the buyer wallet itself for a self-transfer.
# TVC_DEMO_PINNED_PUBKEY_HEX=<hex of qos_p256 P256Public::to_bytes()>
# — pinned ephemeral key the gateway will verify against. Without this
# the gateway fail-closes (X402_PROFILE != local).
#
# To target a TVC-deployed gateway image instead of a local build, replace
# the `image:` field with the GHCR digest-pinned form, e.g.
# image: ghcr.io/anchorageoss/parser_gateway:v0.1.2@sha256:<digest>

services:
parser_grpc_server:
image: anchorageoss-visualsign-parser/parser_grpc_server:latest
# stagex/core-busybox sets ENTRYPOINT ["/bin/sh"], so we override to
# exec the static-musl binary directly.
entrypoint: ["/parser_grpc_server"]
environment:
EPHEMERAL_FILE: "/etc/parser/ephemeral.secret"
volumes:
# Local-dev only. Production TVC provisions the real key via QoS host
# boot; the public half ends up as TVC_DEMO_PINNED_PUBKEY_HEX on the
# gateway side.
- ./src/integration/fixtures/ephemeral.secret:/etc/parser/ephemeral.secret:ro
ports:
- "44020:44020"

parser_gateway:
image: anchorageoss-visualsign-parser/parser_gateway:latest
entrypoint: ["/parser_gateway"]
depends_on:
- parser_grpc_server
environment:
GATEWAY_PORT: "8080"
GRPC_ADDR: "http://parser_grpc_server:44020"
X402_PROFILE: "payai"
X402_NETWORK: "solana-devnet"
X402_FACILITATOR_URL: "https://facilitator.payai.network"
X402_FACILITATOR_TIMEOUT_SECS: "10"
X402_PAYTO: "${X402_PAYTO:?X402_PAYTO must be set in your shell before docker compose up}"
TVC_DEMO_PINNED_PUBKEY_HEX: "${TVC_DEMO_PINNED_PUBKEY_HEX:?required: pin the TVC enclave's P256 public key here}"
ports:
- "8080:8080"
Loading
Loading