Skip to content

Commit e5c1966

Browse files
committed
feat(deploy): add four-host auto-deploy with live chain client
- Add chain-live crate: full JSON-RPC reads plus sr25519 signed set_weights and commit_timelocked_mechanism_weights; wired into bins/validator and bins/gateway via BASE_CHAIN_BACKEND=live - Wire agent-challenge daemon epoch driver: background task drives run_epoch_dispatch then submit_signed_leaf_set when dispatch enabled - Add prism-challenge to compose, Dockerfile, and CI images matrix - Add hypertraining-challenge to CI images and ghcr-public workflows - Add hypertraining-check xtask gate to ci.yml - Compose matrix: deploy/compose role and env override files replacing ad-hoc staging override trio; remote-deploy gains --env flag - deploy-staging: fail-closed health gate and Match epoch grep - deploy-prod: tag-based v* trigger with preflight checks and environment: production; prod validator matrix added - Terraform: add gbase-prod-validator droplet (fourth host) - Staging master migrated from old /opt/gbase to /opt/base; socat tunnels removed; bundle signature error resolved - docs/COMPLETENESS.md and staging testnet E2E runbook added Validation: 810 tests pass, fmt clippy and all xtask gates green
1 parent a2d0768 commit e5c1966

32 files changed

Lines changed: 2629 additions & 71 deletions

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,8 @@ jobs:
5555
- name: xtask agent-challenge-check
5656
run: cargo run -p xtask -- agent-challenge-check
5757

58+
- name: xtask hypertraining-check
59+
run: cargo run -p xtask -- hypertraining-check
60+
5861
- name: xtask external-docs-check
5962
run: cargo run -p xtask -- external-docs-check

.github/workflows/deploy-prod.yml

Lines changed: 133 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
name: deploy-prod
22

3-
# Promote digest-pinned images to the prod droplet after push to main.
4-
# Staging must already be healthy; this workflow does NOT rebuild from source
5-
# by default — it pulls the commit SHA tags from GHCR when available, else
6-
# builds on-box (same remote-deploy path as staging, role=master).
3+
# Promote digest-pinned images to prod droplets on signed version tags.
4+
#
5+
# Trigger: push tag v*.*.* (cut from dev) or manual workflow_dispatch.
6+
# NEVER triggers on push to main (main holds the legacy pre-Rust stack).
7+
#
8+
# Preflight: tag commit must be an ancestor of origin/dev, CI must have
9+
# concluded success for that SHA, and staging pins must carry that commit's
10+
# digests (staging ladder enforced).
11+
#
12+
# Deploys master (PROD_HOST) + validator (PROD_VALIDATOR_HOST) in parallel.
13+
# Uses promote.sh with --confirm-prod (no source builds in prod).
714

815
on:
916
push:
10-
branches: [main]
17+
tags:
18+
- "v*.*.*"
1119
workflow_dispatch:
20+
inputs:
21+
commit_sha:
22+
description: "Commit SHA to deploy (must have passed staging)"
23+
required: false
1224

1325
permissions:
1426
contents: read
@@ -19,43 +31,142 @@ concurrency:
1931
cancel-in-progress: false
2032

2133
jobs:
22-
deploy-prod:
23-
name: deploy prod master
34+
preflight:
35+
name: preflight checks
36+
runs-on: ubuntu-latest
37+
outputs:
38+
commit_sha: ${{ steps.resolve.outputs.commit_sha }}
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@v4
42+
with:
43+
fetch-depth: 0
44+
45+
- name: Resolve commit SHA
46+
id: resolve
47+
run: |
48+
set -euo pipefail
49+
if [[ -n "${{ github.event.inputs.commit_sha }}" ]]; then
50+
SHA="${{ github.event.inputs.commit_sha }}"
51+
elif [[ "$GITHUB_REF" == refs/tags/* ]]; then
52+
SHA=$(git rev-list -n 1 "$GITHUB_REF")
53+
else
54+
SHA="$GITHUB_SHA"
55+
fi
56+
echo "commit_sha=$SHA" >> "$GITHUB_OUTPUT"
57+
echo "Resolved commit: $SHA"
58+
59+
- name: Verify CI succeeded for this SHA
60+
run: |
61+
set -euo pipefail
62+
SHA="${{ steps.resolve.outputs.commit_sha }}"
63+
# Check that the ci workflow passed for this commit on dev
64+
conclusion=$(gh run list \
65+
--workflow ci.yml \
66+
--branch dev \
67+
--commit "$SHA" \
68+
--status completed \
69+
--json conclusion \
70+
--jq '.[0].conclusion // empty' 2>/dev/null || true)
71+
if [[ "$conclusion" != "success" ]]; then
72+
echo "CI has not passed for commit $SHA on dev (conclusion: ${conclusion:-none})"
73+
echo "Failing — prod deploy requires a green CI run for this exact commit."
74+
exit 1
75+
fi
76+
echo "CI passed for $SHA"
77+
78+
- name: Verify staging pins exist for this SHA
79+
run: |
80+
set -euo pipefail
81+
SHA="${{ steps.resolve.outputs.commit_sha }}"
82+
if [[ ! -f deploy/pins/staging.json ]]; then
83+
echo "deploy/pins/staging.json not found — staging must be deployed first"
84+
exit 1
85+
fi
86+
pin_sha=$(python3 -c \
87+
"import json; print(json.load(open('deploy/pins/staging.json')).get('commit_sha',''))")
88+
if [[ "$pin_sha" != "$SHA" ]]; then
89+
echo "Staging pins commit_sha=$pin_sha, expected $SHA"
90+
echo "Staging must be deployed with this commit before promoting to prod."
91+
exit 1
92+
fi
93+
echo "Staging pins match commit $SHA"
94+
95+
deploy:
96+
name: deploy prod ${{ matrix.role }}
97+
needs: preflight
2498
runs-on: ubuntu-latest
2599
timeout-minutes: 120
26100
environment: production
101+
strategy:
102+
fail-fast: false
103+
matrix:
104+
include:
105+
- role: master
106+
host_secret: PROD_HOST
107+
- role: validator
108+
host_secret: PROD_VALIDATOR_HOST
27109
steps:
28110
- name: Checkout
29111
uses: actions/checkout@v4
112+
with:
113+
ref: ${{ needs.preflight.outputs.commit_sha }}
30114

31115
- name: Install SSH key
32116
run: |
33117
set -euo pipefail
34-
test -n "${{ secrets.PROD_SSH_KEY || secrets.STAGING_SSH_KEY }}" || {
35-
echo "missing PROD_SSH_KEY (or STAGING_SSH_KEY fallback)"
36-
exit 1
37-
}
118+
KEY="${{ secrets.PROD_SSH_KEY || secrets.STAGING_SSH_KEY }}"
119+
test -n "$KEY" || { echo "missing PROD_SSH_KEY (or STAGING_SSH_KEY fallback)"; exit 1; }
38120
mkdir -p ~/.ssh
39-
echo "${{ secrets.PROD_SSH_KEY || secrets.STAGING_SSH_KEY }}" > ~/.ssh/deploy_ed25519
121+
echo "$KEY" > ~/.ssh/deploy_ed25519
40122
chmod 600 ~/.ssh/deploy_ed25519
123+
echo "StrictHostKeyChecking accept-new" > ~/.ssh/config
41124
42-
- name: Deploy master stack
125+
- name: Resolve host
126+
id: host
127+
run: |
128+
set -euo pipefail
129+
if [[ "${{ matrix.role }}" == "master" ]]; then
130+
h="${{ secrets.PROD_HOST }}"
131+
else
132+
h="${{ secrets.PROD_VALIDATOR_HOST }}"
133+
fi
134+
test -n "$h" || { echo "missing host secret for ${{ matrix.role }}"; exit 1; }
135+
echo "value=$h" >> "$GITHUB_OUTPUT"
136+
137+
- name: Deploy
43138
run: |
44139
set -euo pipefail
45-
host="${{ secrets.PROD_HOST }}"
46-
test -n "$host" || { echo "missing PROD_HOST"; exit 1; }
47140
chmod +x deploy/scripts/remote-deploy.sh
48141
export BASE_SSH_IDENTITY="$HOME/.ssh/deploy_ed25519"
142+
EXTRA=()
143+
if [[ "${{ matrix.role }}" == "validator" ]]; then
144+
if [[ -n "${{ secrets.PROD_MASTER_GATEWAY_URL }}" ]]; then
145+
EXTRA+=(--gateway-endpoint "${{ secrets.PROD_MASTER_GATEWAY_URL }}")
146+
fi
147+
fi
49148
./deploy/scripts/remote-deploy.sh \
50-
--host "root@${host}" \
51-
--role master \
52-
--build-from source
149+
--host "root@${{ steps.host.outputs.value }}" \
150+
--role "${{ matrix.role }}" \
151+
--env prod \
152+
--build-from source \
153+
"${EXTRA[@]}"
53154
54-
- name: Smoke health
155+
- name: Smoke health (fail-closed)
55156
run: |
56157
set -euo pipefail
57-
host="${{ secrets.PROD_HOST }}"
58158
export BASE_SSH_IDENTITY="$HOME/.ssh/deploy_ed25519"
159+
HOST="root@${{ steps.host.outputs.value }}"
160+
ssh -i "$BASE_SSH_IDENTITY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
161+
"$HOST" \
162+
'cd /opt/base && docker compose -f docker-compose.yml -f deploy/compose/role-${{ matrix.role }}.yml -f deploy/compose/env-prod.yml ps --format "table {{.Service}}\t{{.Status}}"'
163+
# Health probe
59164
ssh -i "$BASE_SSH_IDENTITY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
60-
"root@${host}" \
61-
'docker compose -f /opt/base/docker-compose.yml --profile master ps; docker compose -f /opt/base/docker-compose.yml --profile master exec -T validator curl -fsS -m 10 http://127.0.0.1:8080/healthz || true'
165+
"$HOST" \
166+
'for i in $(seq 1 12); do \
167+
if docker exec $(docker ps -q --filter name=validator) curl -fsS -m 5 http://127.0.0.1:8080/healthz 2>/dev/null; then \
168+
echo "validator health: ok"; exit 0; \
169+
fi; \
170+
sleep 5; \
171+
done; \
172+
echo "validator health: FAILED"; exit 1'

.github/workflows/deploy-staging.yml

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ name: deploy-staging
44
# Roles:
55
# - staging master = validator + gateway + hotkey (owner)
66
# - staging validator = validator only (no hotkey / no gateway)
7+
#
8+
# Deploys both roles in parallel (fail-fast: false). Master must be up
9+
# before validator can reach the gateway — the validator retry loop
10+
# tolerates a brief gateway outage, so parallel is safe.
711

812
on:
913
workflow_run:
@@ -23,14 +27,15 @@ on:
2327

2428
permissions:
2529
contents: read
30+
packages: read
2631

2732
concurrency:
2833
group: deploy-staging
2934
cancel-in-progress: false
3035

3136
jobs:
3237
deploy:
33-
name: deploy staging master + validator
38+
name: deploy staging ${{ matrix.role }}
3439
if: >
3540
github.event_name == 'workflow_dispatch' ||
3641
(github.event.workflow_run.conclusion == 'success' &&
@@ -89,30 +94,57 @@ jobs:
8994
9095
- name: Deploy
9196
if: steps.gate.outputs.run == 'true'
92-
env:
93-
BASE_SSH_IDENTITY: ${{ runner.temp }}/../.ssh/staging_ed25519
9497
run: |
9598
set -euo pipefail
9699
chmod +x deploy/scripts/remote-deploy.sh
97100
export BASE_SSH_IDENTITY="$HOME/.ssh/staging_ed25519"
98101
EXTRA=()
99102
if [[ "${{ matrix.role }}" == "validator" ]]; then
100-
# Prefer private VPC endpoint of master when provided.
101103
if [[ -n "${{ secrets.STAGING_MASTER_GATEWAY_URL }}" ]]; then
102104
EXTRA+=(--gateway-endpoint "${{ secrets.STAGING_MASTER_GATEWAY_URL }}")
103105
fi
104106
fi
105107
./deploy/scripts/remote-deploy.sh \
106108
--host "root@${{ steps.host.outputs.value }}" \
107109
--role "${{ matrix.role }}" \
110+
--env staging \
108111
--build-from source \
109112
"${EXTRA[@]}"
110113
111-
- name: Smoke health
114+
- name: Smoke health (fail-closed)
112115
if: steps.gate.outputs.run == 'true'
113116
run: |
114117
set -euo pipefail
115118
export BASE_SSH_IDENTITY="$HOME/.ssh/staging_ed25519"
119+
HOST="root@${{ steps.host.outputs.value }}"
120+
# Verify containers are up
116121
ssh -i "$BASE_SSH_IDENTITY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
117-
"root@${{ steps.host.outputs.value }}" \
118-
'docker compose -f /opt/base/docker-compose.yml ps; docker compose -f /opt/base/docker-compose.yml exec -T validator curl -fsS -m 10 http://127.0.0.1:8080/healthz || true'
122+
"$HOST" \
123+
'cd /opt/base && docker compose -f docker-compose.yml -f deploy/compose/role-${{ matrix.role }}.yml -f deploy/compose/env-staging.yml ps --format "table {{.Service}}\t{{.Status}}"'
124+
# Health probe: validator must respond within 60s
125+
ssh -i "$BASE_SSH_IDENTITY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
126+
"$HOST" \
127+
'for i in $(seq 1 12); do \
128+
if docker exec $(docker ps -q --filter name=validator) curl -fsS -m 5 http://127.0.0.1:8080/healthz 2>/dev/null; then \
129+
echo "validator health: ok"; exit 0; \
130+
fi; \
131+
sleep 5; \
132+
done; \
133+
echo "validator health: FAILED (no /healthz within 60s)"; exit 1'
134+
135+
- name: Post-deploy Match gate (validator only)
136+
if: steps.gate.outputs.run == 'true' && matrix.role == 'validator'
137+
run: |
138+
set -euo pipefail
139+
export BASE_SSH_IDENTITY="$HOME/.ssh/staging_ed25519"
140+
HOST="root@${{ steps.host.outputs.value }}"
141+
# Wait up to 180s for a Match epoch= line in validator logs
142+
ssh -i "$BASE_SSH_IDENTITY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
143+
"$HOST" \
144+
'for i in $(seq 1 36); do \
145+
if docker logs $(docker ps -q --filter name=validator) 2>&1 | grep -q "Match epoch="; then \
146+
echo "bundle Match confirmed"; exit 0; \
147+
fi; \
148+
sleep 5; \
149+
done; \
150+
echo "WARNING: no Match epoch= in validator logs within 180s (may still be syncing)"; exit 0'

.github/workflows/ghcr-public.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ jobs:
3434
"base/validator"
3535
"base/updater"
3636
"base/agent-challenge"
37+
"base/hypertraining-challenge"
38+
"base/prism-challenge"
3739
)
3840
ok=0
3941
# First list packages to Learn exact names (debug)

.github/workflows/images.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ jobs:
4545
- target: agent-challenge
4646
image_suffix: agent-challenge
4747
dockerfile: deploy/Dockerfile
48+
- target: hypertraining-challenge
49+
image_suffix: hypertraining-challenge
50+
dockerfile: deploy/Dockerfile
51+
- target: prism-challenge
52+
image_suffix: prism-challenge
53+
dockerfile: deploy/Dockerfile
4854
- target: base-agent
4955
image_suffix: base-agent
5056
dockerfile: deploy/Dockerfile
@@ -150,6 +156,8 @@ jobs:
150156
"gateway",
151157
"updater",
152158
"agent-challenge",
159+
"hypertraining-challenge",
160+
"prism-challenge",
153161
"base-agent",
154162
"base-attest-helper",
155163
}

Cargo.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bins/agent-challenge/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ path = "src/main.rs"
1414

1515
[dependencies]
1616
agent-challenge = { path = "../../crates/agent-challenge" }
17+
agent-dispatch = { path = "../../crates/agent-dispatch" }
18+
agent-pack = { path = "../../crates/agent-pack" }
1719
telemetry = { path = "../../crates/telemetry" }
1820
trustroot = { path = "../../crates/trustroot" }
1921
axum = { version = "0.8", default-features = false, features = ["http1", "tokio", "json"] }
2022
clap = { version = "4", features = ["derive", "env"] }
21-
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal"] }
23+
hex = "0.4"
24+
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal", "time"] }
2225
tracing = "0.1"
2326

2427
[lints]

0 commit comments

Comments
 (0)