Skip to content

draft(ci): automate og-tdx CVM build → publish → attest (version×env×owner)#32

Merged
Wilbert957 merged 10 commits into
mainfrom
draft/cvm-ci-automation
Jul 13, 2026
Merged

draft(ci): automate og-tdx CVM build → publish → attest (version×env×owner)#32
Wilbert957 merged 10 commits into
mainfrom
draft/cvm-ci-automation

Conversation

@Wilbert957

Copy link
Copy Markdown
Collaborator

Draft / WIP — the pieces + design are here and syntax/YAML-checked; the pipeline has not been run end-to-end as a whole yet (no self-hosted runner wired, no secrets). Opening as a draft for review + tracking. Do not merge.

What this automates

Turns "tapp releases a version" into a reproducible, attestable og-tdx CVM image, on the same Alinux/al8 host we build on today. Follow-up to #5 / #29.

Dimensions: version × env × owner

  • version = tapp-server release tag (e.g. v0.1.0) — tapp-server + image name suffix.
  • env = dev (HARDEN=0, SSH-able) / prod (HARDEN=1, hardened) — the two image variants.
  • owner = OWNER_ADDRESS, baked into /etc/tapp/config.toml. policy.rego folds rootfs integrity into the initrd measurement, so a different owner ⇒ a different measurement.initrd ⇒ a distinct reference set → owner is an attested dimension. KBS/KMS is fixed (vars.TAPP_KBS_URLS).

Flow (.github/workflows/build-cvm.yml, workflow_dispatch)

inputs(version, env, owner, publish) → on [self-hosted, alinux, cvm-builder]:

  1. fetch inputs (base image + fde deb; tapp-server from the release)
  2. Stage A+B build (build-gcp-tapp.sh, HARDEN per env, OWNER baked, KBS fixed)
  3. boot smoke test (gate)
  4. Stage C publish (publish-gcp-image.sh) → og-tdx[-dev]-<version> (+ family)
  5. build the #128-fixed cryptpilot-fde from a pinned fork commit (ci/build-cryptpilot-fde.sh) — transparent, not a prebuilt blob
  6. reference values: show-reference-valueverifier/reference-values/<version>/<env>/<owner>.json (ci/gen-reference-values.sh) → register on the shared AS (register-shared-as.sh, now owner-dimensioned)
  7. open a PR with the reference values → dev

Files

  • .github/workflows/build-cvm.yml
  • verifier/register-shared-as.sh — add owner dimension (path + policy id 0g-tapp-<version>-<env>-<owner>)
  • gcp-cvm/ci/{build-cryptpilot-fde,gen-reference-values,fetch-inputs,open-refvalues-pr}.sh
  • verifier/reference-values/README.md — owner-dimensioned layout + fork-build flow

Tested / not

  • gen-reference-values.sh validated end-to-end against a real image (fixed fork binary emits the measurement.* JSON directly).
  • build-gcp-tapp.sh (A/B) and publish-gcp-image.sh (C) already proven on real GCP images this cycle.
  • 🟡 Not yet run as a workflow: fetch-inputs, open-refvalues-pr, register-shared-as (owner), the smoke-test gate, and the workflow itself.

Open items before "Ready for review"

  • Stand up the self-hosted al8 runner; add secret GCP_SA_KEY + var TAPP_KBS_URLS.
  • First end-to-end dry-run with publish=false.
  • AS auth: register-shared-as.sh uses grpcurl -plaintext with no token — confirm how the shared AS SetAttestationPolicy is authorized (network ACL vs token) and align.
  • Migrate the existing flat verifier/reference-values/v0.1.0/{dev,prod}.json to the owner-dimensioned layout.
  • Auth hardening deferred by design for now (write-permission trigger only; Environment reviewers / WIF later).
  • Pin CRYPTPILOT_REF to a full SHA/tag (currently short 52a77a2).

🤖 Generated with Claude Code

@Wilbert957
Wilbert957 changed the base branch from dev to main July 13, 2026 07:37
Wei and others added 8 commits July 13, 2026 15:38
DRAFT for review — not wired to a runner yet.
- .github/workflows/build-cvm.yml: dispatch(version,env,owner,publish) on a self-hosted
  al8 runner → build (HARDEN per env, OWNER baked, KBS fixed) → smoke gate → publish
  versioned image+family → compute+register reference values → PR them into dev.
- verifier/register-shared-as.sh: add the owner dimension (path <version>/<env>/<owner>.json,
  policy id 0g-tapp-<version>-<env>-<owner>) — owner is measured (folded into initrd).
- gcp-cvm/ci/{gen-reference-values,fetch-inputs,open-refvalues-pr}.sh helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e; real ref-value generator

- gcp-cvm/ci/build-cryptpilot-fde.sh: compile cryptpilot-fde-host from the 0gfoundation
  cryptpilot fork at a pinned commit (transparent/reproducible vs a prebuilt blob). The stock
  0.7.0 errors "saved_entry not found" on a never-booted image; the fork falls back to the
  default grub.cfg menuentry.
- gcp-cvm/ci/gen-reference-values.sh: rewritten to use the fixed binary's real JSON output
  (filter to the five measurement.<component>.SHA-384 keys via python; fail if any is empty).
  Validated end-to-end against og-tdx-dev.qcow2.
- build-cvm.yml: build the fixed cryptpilot-fde as a step, pass it to the ref-value step.
- reference-values/README.md: owner dimension (<version>/<env>/<owner>.json) + fork-build flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ucket/AS/fde-ref)

All tunables now come from repo/org Variables (vars.*) with in-file defaults, so KBS URLs,
GCP project/bucket, AS endpoint and the cryptpilot-fde fork commit can be changed in GitHub
settings without editing the workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t dispatched ref

- fetch-inputs.sh: support BASE_IMAGE_URL as file:// or absolute local path (COPY, never symlink,
  since Stage A mutates the input) — use the persistent runner cache /root/base-noble.qcow2.
- workflow: BASE_IMAGE_URL from vars; smoke test continue-on-error (advisory until validated on a
  KVM runner); checkout the dispatched ref (not hardcoded dev) so the draft can be dry-run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…allback), not the ESP-convert branch

Dry-run caught it: CRYPTPILOT_REF=52a77a2 (fix/gcp-convert-esp-sync) has no show-reference-value
fallback, so gen-reference-values errored 'saved_entry not found' on a never-booted image. The
fallback lives on fix/srv-default-entry @ 64fe4e744574c8e0273dd1e5d54ceae9714748a0 (falls back to
the default grub.cfg menuentry; also has --hash-algo). Pin to the full SHA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d (drop fix-B & keep-generic workarounds)

Realizes what #130 was for: let cryptpilot-convert do the ESP-grub sync itself instead of our scripts.
Validated end-to-end on GCP TDX hardware (boot → /data auto-provision → docker/sysbox → tapp-server).

Toolchain (ci/setup-toolchain.sh, reproducible): install released cryptpilot 0.8.0 (crypt/verity/
fde-host + dracut) and overlay convert (#130) + fde-host (#128) built from the pinned fork commit
0gfoundation/cryptpilot@57d7512 (feat/0g-gcp-cvm = 0.8.0 + #128 + #130). In-image runtime = released
0.8.0 cryptpilot-fde-guest deb (fetch-inputs).

Script simplification:
- prepare-gcp-tapp.sh: DROP fix-B (convert #130 syncs the ESP grub.cfg + modules itself); add
  CRYPTPILOT_CONVERT override.
- build-gcp-tapp.sh: FDE_PACKAGE -> 0.8.0 guest deb; config.toml gains max_log_files=7 (bounds
  /var/log/tapp on the RAM rootfs — issue #23).
- ci/gen-reference-values.sh: use /usr/bin/cryptpilot-fde-host (from setup-toolchain).
- ci/build-cryptpilot-fde.sh: removed (subsumed by setup-toolchain).
- workflow: add "Set up 0.8.0 toolchain" step; ref-value step uses the installed fde-host.
- add publish-gcp-image.sh (Stage C) to this branch.

TODO (deferred): drop the leftover -generic kernel for a gcp-only image; wire tapp-server from dev
in the real pipeline; re-verify prod (HARDEN=1) variant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…for custom owner

- on: release (published) -> build the DEFAULT deployment (vars.DEFAULT_OWNER_ADDRESS), both env,
  publish + register. workflow_dispatch -> custom per-owner (owner input; blank = default).
- resolve version/owner/publish for both triggers; matrix env defaults to both on release.
- header comment updated to the 0.8.0 toolchain (ci/setup-toolchain.sh).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consistent naming with the other vars.* config (no DEFAULT_ prefix); it still
serves as the default/fallback owner for release builds and blank dispatch input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Wilbert957
Wilbert957 force-pushed the draft/cvm-ci-automation branch from b90b4b2 to 849c2e8 Compare July 13, 2026 07:41
Wei and others added 2 commits July 13, 2026 15:53
Point gcloud/gsutil at a job-scoped CLOUDSDK_CONFIG under RUNNER_TEMP, write the
key there only long enough to activate, delete it immediately, and revoke +
wipe the config dir in cleanup. Nothing lands in the runner's ~/.config/gcloud.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
runner context isn't available in jobs.<id>.env — GitHub rejected the workflow
file. Set CLOUDSDK_CONFIG in the auth step via $RUNNER_TEMP and export it into
$GITHUB_ENV for later steps; guard cleanup when it's unset (publish=false).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Wilbert957
Wilbert957 marked this pull request as ready for review July 13, 2026 08:21
@Wilbert957
Wilbert957 merged commit 81628ab into main Jul 13, 2026
Wilbert957 added a commit that referenced this pull request Jul 16, 2026
… RPC (#58)

* feat: runtime CLI↔server/contract version-compatibility check (#26)

* feat: runtime CLI↔server/contract version-compatibility check

Implements the compatibility mechanism from docs/VERSIONING.md.

Rust (gRPC edge):
- tapp-common::compat::interface_warning compares MAJOR.MINOR (PATCH ignored),
  returning a warning when the peer's interface looks incompatible.
- tapp-cli build.rs stamps the expected server interface version (the
  workspace-root tapp-server MAJOR.MINOR) at build time — no number to maintain.
- create_client does a best-effort GetTappInfo on connect and warns to stderr on
  mismatch; it never hard-blocks (a genuinely missing RPC fails at the call).
- Bump tapp-cli 0.1.0 -> 0.1.1 (adds the on-connect warning behavior).

Contract (on-chain edge):
- Add a version() view to TappRegistry returning "0.1.0" so callers can read the
  live implementation version behind the stable beacon proxy address.
- Rename local `version` vars to `ackVersion` to avoid shadowing the new view.

Server needs no change: GetTappInfo already reports the full tapp-server version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(contract): record TappRegistry v0.1.0 upgrade (adds version())

Deployed new implementation 0x9Ea52Ef383e8eA3fe7F0890309D3C62b2FC1Ac2B behind
the stable beacon proxy 0x2Ce8...165c9 on 0G Galileo testnet; beacon.upgradeTo
tx 0x8a003a1a. version() now returns "0.1.0" on-chain, storage preserved,
source-verified on chainscan-galileo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: sync Cargo.lock (tempfile dev-dependency)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(gcp-cvm): add Stage C (publish to GCP) + document the 3-stage pipeline (#29)

The build produced a qcow2 but stopped short of a GCP image. Add the "last mile":

- publish-gcp-image.sh (Stage C): qemu-img convert qcow2 -> raw -> sparse oldgnu tar.gz ->
  gsutil cp -> gcloud compute images create with UEFI_COMPATIBLE,GVNIC,SEV_CAPABLE,TDX_CAPABLE.
  Parameterized (GCS_BUCKET/GCP_PROJECT/GUEST_OS_FEATURES, defaults gs://tapp-image + g-devops);
  refuses to clobber an existing image name. Kept separate from the build because it needs
  gcloud/gsutil auth + network upload.
- build-gcp-tapp.sh: opt-in Stage C via PUBLISH_AS=<image-name> (runs publish after Stage B);
  also fix the now-stale tapp-data-provision.service Description (format-or-adopt, not "carve").
- Docs: README pipeline overview (Stage 0 base prep / A / B / optional smoke test / C),
  publish-gcp-image.sh in the file table, PUBLISH_AS env var, a "Publish to GCP (Stage C)"
  section; main doc §14 (Stage C steps + stages-at-a-glance).

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: start-app --register-onchain — idempotent on-chain registration before the app starts (#28)

* feat: start-app --register-onchain registers the app before it starts

New optional flow for 'start-app': with --register-onchain (+ --rpc-url,
--contract, --stake-wei), the CLI idempotently ensures the app is
registered on-chain BEFORE its containers start:

- app not registered            -> registerApp (this node = first node)
- registered, signer not a node -> addNode (per-node override iff differs)
- signer already in node list   -> skip, just start

To get real hashes before anything runs, StartAppRequest gains a
measure_only flag: the server stages files, 'docker compose pull's the
images and computes compose/volumes/image hashes without starting
containers, returning them via new TaskResult fields. The follow-up
normal StartApp starts from the cached images.

GetAppKey now creates the app key if missing (same as GetEvidence), so
the signer address is available before the app starts; the key lives in
server memory so the app gets the same key when it runs.

e2e-tested against 0G Galileo testnet (registerApp + idempotent skip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: document start-app --register-onchain in README, runbook, and skill

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix: make start-service idempotent, dropping the already-running pre-check (#30)

start-service failed forever with 'Service <svc> is already running' after
stop-service, whenever a running sibling service's name contained the stopped
one (e.g. sandbox vs sandbox-redis): the pre-check did a substring match on
docker compose ps output. (#27)

Rather than fixing the match, drop the pre-check: docker compose up -d is
already idempotent (running + unchanged config = no-op), and removing it also
unblocks 'start-service --pull' on a running service, whose re-measure path
existed downstream but was unreachable. Typo'd service names still fail the
task with docker's own 'no such service' error.

e2e-verified: restart-after-stop with prefix sibling running, no-op on running
service, --pull on running service, and bad-name failure.

Fixes #27

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* chore: bump tapp-server and tapp-cli to 0.2.0

Per docs/VERSIONING.md, for the changes merged to dev:
- tapp-server 0.1.1 -> 0.2.0: gRPC interface changed (#28 adds
  StartAppRequest.measure_only + TaskResult measurement fields; #30
  changes StartService semantics for already-running services)
- tapp-cli 0.1.1 -> 0.2.0: new user-facing flags (#28:
  --register-onchain / --rpc-url / --contract / --stake-wei on start-app)
- contract unchanged (0.1.0); tapp-common internal, additive only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(logging): bound file-log growth + move CVM logs off the RAM rootfs (#34)

* fix(logging): bound file-log growth + move CVM logs off the RAM rootfs (#23)

- RollingFileAppender now keeps at most logging.max_log_files daily files
  (new config field, default 7) — daily rotation alone never deletes old
  files, so total size was unbounded
- gcp-cvm build: tapp-server file logs go to /data/log/tapp/ (persistent
  disk) instead of /var/log/tapp/ on the RAM-backed rootfs overlay, and
  tapp-server.service gains RequiresMountsFor=/data (same fail-loud policy
  as docker/containerd)

Closes #23

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(logging): prune stale daily logs at startup, not only at in-process rotation

tracing-appender's max_log_files only prunes inside refresh_writer (when the
date rolls over while the process runs) — verified empirically: files left by
previous runs survive until the first in-process midnight, and a process that
never lives past midnight never prunes. Add a best-effort startup prune of
{prefix}.* files before building the appender, with a unit test.

Also fix the stale KmsClient::new arity in kms_client tests (predates the
retry-config parameter; lib tests did not compile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(server): pass derivation material through GetSecretResource to KMS /app-key (#35)

- proto: GetSecretResourceRequest gains optional 'string material = 2' —
  hex-encoded derivation material, opaque to tapp, forwarded verbatim
- kms_client: KmsRequest carries material; omitted from the JSON body when
  empty so the request stays byte-identical to the pre-material format
  (KMS derives purely from app_id, existing callers untouched)
- tests: new wiremock test asserting material is forwarded verbatim and
  omitted when empty; also fix stale KmsClient::new arity in existing tests

Closes #33

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(readme): document max_log_files + GetSecretResource material; fix stale config sample

- [logging] sample: path -> file_path (actual config key), add format and
  the new max_log_files retention cap (#34)
- config sample and KMS section: [kms] cluster_urls -> [kbs] node_urls
  (the section/key tapp-server actually reads), port 8080 -> 9091
- KMS Integration: document the optional derivation material passthrough (#35)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* draft(ci): automate og-tdx CVM build → publish → attest (version×env×owner) (#32)

* draft(ci): automate og-tdx CVM build/publish/attest (version×env×owner)

DRAFT for review — not wired to a runner yet.
- .github/workflows/build-cvm.yml: dispatch(version,env,owner,publish) on a self-hosted
  al8 runner → build (HARDEN per env, OWNER baked, KBS fixed) → smoke gate → publish
  versioned image+family → compute+register reference values → PR them into dev.
- verifier/register-shared-as.sh: add the owner dimension (path <version>/<env>/<owner>.json,
  policy id 0g-tapp-<version>-<env>-<owner>) — owner is measured (folded into initrd).
- gcp-cvm/ci/{gen-reference-values,fetch-inputs,open-refvalues-pr}.sh helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* draft(ci): build the #128-fixed cryptpilot-fde from pinned fork source; real ref-value generator

- gcp-cvm/ci/build-cryptpilot-fde.sh: compile cryptpilot-fde-host from the 0gfoundation
  cryptpilot fork at a pinned commit (transparent/reproducible vs a prebuilt blob). The stock
  0.7.0 errors "saved_entry not found" on a never-booted image; the fork falls back to the
  default grub.cfg menuentry.
- gcp-cvm/ci/gen-reference-values.sh: rewritten to use the fixed binary's real JSON output
  (filter to the five measurement.<component>.SHA-384 keys via python; fail if any is empty).
  Validated end-to-end against og-tdx-dev.qcow2.
- build-cvm.yml: build the fixed cryptpilot-fde as a step, pass it to the ref-value step.
- reference-values/README.md: owner dimension (<version>/<env>/<owner>.json) + fork-build flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* draft(ci): move fixed config to repo Actions Variables (KBS/project/bucket/AS/fde-ref)

All tunables now come from repo/org Variables (vars.*) with in-file defaults, so KBS URLs,
GCP project/bucket, AS endpoint and the cryptpilot-fde fork commit can be changed in GitHub
settings without editing the workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* draft(ci): local base-image cache + non-blocking smoke test + checkout dispatched ref

- fetch-inputs.sh: support BASE_IMAGE_URL as file:// or absolute local path (COPY, never symlink,
  since Stage A mutates the input) — use the persistent runner cache /root/base-noble.qcow2.
- workflow: BASE_IMAGE_URL from vars; smoke test continue-on-error (advisory until validated on a
  KVM runner); checkout the dispatched ref (not hardcoded dev) so the draft can be dry-run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* draft(ci): pin cryptpilot-fde to fix/srv-default-entry (saved_entry fallback), not the ESP-convert branch

Dry-run caught it: CRYPTPILOT_REF=52a77a2 (fix/gcp-convert-esp-sync) has no show-reference-value
fallback, so gen-reference-values errored 'saved_entry not found' on a never-booted image. The
fallback lives on fix/srv-default-entry @ 64fe4e744574c8e0273dd1e5d54ceae9714748a0 (falls back to
the default grub.cfg menuentry; also has --hash-algo). Pin to the full SHA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(gcp-cvm): migrate to cryptpilot 0.8.0 + #128/#130; simplify build (drop fix-B & keep-generic workarounds)

Realizes what #130 was for: let cryptpilot-convert do the ESP-grub sync itself instead of our scripts.
Validated end-to-end on GCP TDX hardware (boot → /data auto-provision → docker/sysbox → tapp-server).

Toolchain (ci/setup-toolchain.sh, reproducible): install released cryptpilot 0.8.0 (crypt/verity/
fde-host + dracut) and overlay convert (#130) + fde-host (#128) built from the pinned fork commit
0gfoundation/cryptpilot@57d7512 (feat/0g-gcp-cvm = 0.8.0 + #128 + #130). In-image runtime = released
0.8.0 cryptpilot-fde-guest deb (fetch-inputs).

Script simplification:
- prepare-gcp-tapp.sh: DROP fix-B (convert #130 syncs the ESP grub.cfg + modules itself); add
  CRYPTPILOT_CONVERT override.
- build-gcp-tapp.sh: FDE_PACKAGE -> 0.8.0 guest deb; config.toml gains max_log_files=7 (bounds
  /var/log/tapp on the RAM rootfs — issue #23).
- ci/gen-reference-values.sh: use /usr/bin/cryptpilot-fde-host (from setup-toolchain).
- ci/build-cryptpilot-fde.sh: removed (subsumed by setup-toolchain).
- workflow: add "Set up 0.8.0 toolchain" step; ref-value step uses the installed fde-host.
- add publish-gcp-image.sh (Stage C) to this branch.

TODO (deferred): drop the leftover -generic kernel for a gcp-only image; wire tapp-server from dev
in the real pipeline; re-verify prod (HARDEN=1) variant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ci): dual trigger — release auto-builds default owner; dispatch for custom owner

- on: release (published) -> build the DEFAULT deployment (vars.DEFAULT_OWNER_ADDRESS), both env,
  publish + register. workflow_dispatch -> custom per-owner (owner input; blank = default).
- resolve version/owner/publish for both triggers; matrix env defaults to both on release.
- header comment updated to the 0.8.0 toolchain (ci/setup-toolchain.sh).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(ci): rename DEFAULT_OWNER_ADDRESS var -> OWNER_ADDRESS

Consistent naming with the other vars.* config (no DEFAULT_ prefix); it still
serves as the default/fallback owner for release builds and blank dispatch input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* harden(ci): keep GCP SA credential off the persistent runner

Point gcloud/gsutil at a job-scoped CLOUDSDK_CONFIG under RUNNER_TEMP, write the
key there only long enough to activate, delete it immediately, and revoke +
wipe the config dir in cleanup. Nothing lands in the runner's ~/.config/gcloud.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): don't use runner.temp in job-level env (startup failure)

runner context isn't available in jobs.<id>.env — GitHub rejected the workflow
file. Set CLOUDSDK_CONFIG in the auth step via $RUNNER_TEMP and export it into
$GITHUB_ENV for later steps; guard cleanup when it's unset (publish=false).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): install grpcurl in setup-toolchain (register-shared-as needs it) (#37)

The reference-value registration step (verifier/register-shared-as.sh) calls grpcurl
for SetAttestationPolicy, but grpcurl isn't in the al8 repos and wasn't provisioned,
so publish=true runs failed at 'grpcurl: command not found'. Install the release
binary to /usr/local/bin if missing.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(verifier): add cloud dimension to reference values + AS policy (#39)

Each cloud builds its own image (GCP uses linux-image-gcp/gVNIC/GCP boot fixes; Ali
uses its own kernel/drivers), so boot-chain measurements genuinely differ per cloud.
Without a cloud dimension, clouds collide on the shared repo path and shared AS policy id.

- path:   verifier/reference-values/<cloud>/<version>/<env>/<owner>.json
- policy: 0g-tapp-<cloud>-<version>-<env>-<owner>
- gen-reference-values.sh / register-shared-as.sh / open-refvalues-pr.sh take <cloud> first
- build-cvm.yml sets CLOUD=gcp (this kit is GCP-specific; an Ali pipeline sets ali)
- README + register usage updated; also fixed stale build-cryptpilot-fde.sh reference

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): reference-values PR write perms + release auto-trigger (RELEASE_PAT) (#40)

* fix(ci): grant contents/pull-requests write for the reference-values PR

The Open-PR step pushes a refvalues/* branch and opens a PR into dev via GITHUB_TOKEN,
but the default token is read-only -> 'Permission denied to github-actions[bot]' 403 on
git push. Add a workflow permissions block granting contents:write + pull-requests:write.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): fix release auto-trigger + refvalues PR via RELEASE_PAT

- build.yml: create the GitHub Release with RELEASE_PAT (falls back to GITHUB_TOKEN).
  Releases created by GITHUB_TOKEN do NOT fire on:release in other workflows (anti-recursion),
  so build-cvm's release auto-trigger never ran. A PAT-created release fires it.
- build-cvm.yml: Open-PR step uses RELEASE_PAT for 'gh pr create' (covers orgs that disable
  Actions-created PRs); git push still uses GITHUB_TOKEN via the permissions block.

Requires repo secret RELEASE_PAT (fine-grained PAT: Contents+Pull-requests write on 0g-tapp).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(main): CLI material compile fix + CI build speed (cherry-picks from dev) (#41)

* fix(cli): update GetSecretResourceRequest for new material field

PR #35 added the material field to GetSecretResourceRequest and updated
the server, but tapp-cli's initializer was left behind, breaking
cargo build --workspace (E0063) — this is what failed the release
pipeline on v0.2.0-test. Expose it as an optional --material flag
(default empty, unchanged behavior).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(ci): cache toolchain layers, use official rustup source, drop -j1

Release run 29242499831 took 2h23m: rustup via the USTC mirror crawled
for 88 min on the US-based runner, yum via Aliyun OSS took 11 min, and
CARGO_BUILD_JOBS=1 made the compile itself 13 min.

- Split the Dockerfile into a toolchain stage (yum + rustup, no sources)
  and a builder stage; cache the toolchain stage in the GHA cache so it
  only rebuilds when the Dockerfile changes.
- Default rustup to static.rust-lang.org; domestic mirrors stay available
  via --build-arg for builds from inside China. Mirror choice does not
  affect the installed bits.
- Drop CARGO_BUILD_JOBS=1: job parallelism only affects scheduling, not
  codegen. Verified empirically — clean -j1 and -j4 builds with identical
  paths produce sha256-identical tapp-server and tapp-cli.

Expected: ~5-8 min warm cache, ~15 min cold, vs 27 min baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): include owner in the published GCP image name/family (avoid collision) (#44)

The GCP image name was og-tdx[-dev]-<version> with no owner, so two owners building the same
version×env would collide on the image name (publish refuses an existing name). owner is a measured
dimension, so bake it into the name + family: og-tdx[-dev]-<version>-<owner(lowercased)>.
Add a name length/pattern guard to publish-gcp-image.sh (name now carries the ~42-char owner; GCP
image names are max 63 chars / [a-z][-a-z0-9]*).

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Multi-cloud CVM build kit (GCP + Alibaba Cloud) (#46)

* feat(cvm): parameterize build/prepare by CLOUD (gcp|ali); fix branch to ali for testing

Integrate (not fork) Ali support by branching the few cloud-specific points on $CLOUD:
- prepare-*.sh: gcp swaps in linux-image-gcp + fix A + verifies GCP ESP; ali keeps the base's
  generic kernel (Ali ECS boots generic via virtio) — skips kernel swap/fix A/ESP verify.
  convert / DNS / nbd are shared. GCP behavior unchanged (CLOUD defaults to gcp).
- build-*.sh: export CLOUD to prepare (stage B); stage C picks publish-<cloud>-image.sh.
- build-cvm.yml: CLOUD temporarily FIXED to 'ali' on this branch to test the Ali build path
  (per '固定走 aliyun 流程'); becomes an input/matrix before merging to main.

Shared (unchanged, cloud-agnostic): tapp-server/config.toml/docker/sysbox//data/DNS/netplan +
ci/gen-reference-values, ci/open-refvalues-pr, ci/setup-toolchain, register-shared-as.

TODO: publish-ali-image.sh (OSS + aliyun ecs ImportImage); ali dev-variant guest agent
(google-guest-agent is gcp-only); validate on a real Ali TDX instance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cvm): ali dev variant relies on cloud-init for SSH/network (skip google-guest-agent)

HARDEN=0 dev keeps cloud-init (only HARDEN=1 purges it); on Alibaba Cloud cloud-init injects the
instance SSH key + configures networking from the Ali metadata service, so an ali dev image is
SSH-able without the GCP-only google-guest-agent. GCP dev unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cvm): ali uses UKI/systemd-boot (--uki) + pins cloud-init datasource to AliYun

Per the Alibaba confidential-disk guide, Ali's cryptpilot flow is UKI/systemd-boot, not grub:
- prepare (ali): keep generic kernel; install dracut/dracut-core/systemd-boot-efi (UKI prereqs);
  cryptpilot-convert gets --uki for ali (gcp stays grub + #130 ESP sync). No GCP ESP handling on ali.
- build (ali dev): pin cloud-init datasource_list: [ AliYun ] so it reliably reads the Ali metadata
  service and injects the instance SSH key + network (Alibaba recommends pinning over ds-identify).

GCP path unchanged (CLOUD=gcp branches untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cvm): gpg --dearmor with --batch --no-tty (fails in tty-less env)

The SGX/docker repo-key setup piped into 'gpg --dearmor' without --batch/--no-tty; in a fully
detached (no controlling tty) environment gpg errors 'cannot open /dev/tty', aborting the guest
provisioning. --batch --no-tty is the correct non-interactive form (identical dearmor output).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cvm): pre-install dracut-network for ali (cryptpilot-fde-guest dep)

cryptpilot-fde-guest depends on dracut-network; pre-install it in the ali kernel step so the guest
deb install during convert doesn't hit a missing-dep dpkg error (convert auto-resolves, but cleaner).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): gen-reference-values captures the tool's actual components (grub 5 / UKI 1)

The reference-value component set is a property of the boot FORMAT, not the cloud: grub emits
shim/grub/kernel/initrd/kernel_cmdline (5); UKI emits a single measurement.uki. The script hardcoded
the 5 grub keys and failed on a UKI image. Make it format/cloud-agnostic: keep whatever non-empty
measurement.*.SHA-384 keys show-reference-value emits (require >=1). Verified on a real UKI image
(-> measurement.uki.SHA-384). cloud stays only the output-path dimension.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cvm): add [server] unix_socket_path to config.toml + RuntimeDirectory=tapp

tapp-server also serves gRPC on a Unix domain socket (/run/tapp/tapp.sock) that app containers
bind-mount. Add the [server] unix_socket_path config, and RuntimeDirectory=tapp on the service so
/run/tapp exists (0755, cleaned on stop) for tapp-server to bind it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(cvm): make BOOT_FORMAT (grub|uki) an independent build dimension

Decouple boot format from cloud: CLOUD picks kernel/guest (gcp kernel+fixA vs generic), BOOT_FORMAT
picks the boot format (grub vs uki -> --uki + UKI prereqs + ESP-verify gating). Default by cloud
(gcp->grub, ali->uki) but overridable (e.g. a uki image on gcp). A CVM is built as exactly one
CLOUD × one BOOT_FORMAT = one measurement chain (grub 5 components / uki 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(cvm): rename gcp-cvm -> cvm; build-gcp-tapp.sh -> build-tapp.sh; prepare-gcp-tapp.sh -> prepare-tapp.sh

The kit is now multi-cloud (gcp + ali), so the gcp-specific names are misleading. Directory and the
two shared-core scripts renamed; publish-gcp-image.sh keeps its name (GCP-specific; ali gets
publish-ali-image.sh). All references updated (build-cvm.yml paths, script internals, docs). No
behavior change — pure rename + path updates. cryptpilot-gcp-boot-fix.md keeps its name (GCP grub doc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ci): cloud + boot_format as workflow dimensions (un-fix ali)

- add workflow_dispatch inputs: cloud (gcp|ali, default gcp), boot_format (auto|grub|uki, default auto).
- CLOUD = inputs.cloud || gcp; BOOT_FORMAT = auto -> '' (build-tapp derives per cloud). release -> gcp.
- concurrency group keyed by cloud too.
- gate 'Authenticate to GCP' + 'Stage C publish' on CLOUD==gcp (ali image publish via OSS+ImportImage
  is TODO); reference-values compute/register + Open-PR stay cloud-agnostic (run for ali too).

Replaces the branch's temporary 'CLOUD: ali' pin — cloud is now a real dispatch dimension.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cvm): add publish-ali-image.sh (OSS + aliyun ecs ImportImage)

Ali Stage C per the confidential-disk flow: ossutil cp the qcow2 to OSS -> ImportImage
(Architecture=x86_64, BootMode=UEFI, Format=QCOW2) -> enable NVMe (ModifyImageAttribute
Features.NvmeSupport=supported, set after import is kicked off) -> wait until Available.
The 4 easy-to-get-wrong params (64-bit / UEFI / QCOW2 / NVMe-after-import) are pinned.
Requires aliyun + ossutil authenticated (ALIYUN_REGION + creds). DRAFT — not yet run end-to-end
(needs Ali OSS/creds); build-tapp.sh Stage C already dispatches to it for CLOUD=ali.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cvm): NVMe via --Features.NvmeSupport supported --force (CLI schema gap)

The aliyun CLI's ModifyImageAttribute schema doesn't expose the Features.NvmeSupport sub-field as a
flag (rejects both --Features.NvmeSupport and JSON --Features 'flat format required'); --force skips
client validation and sends the flat param the server wants. Validated end-to-end: og-tdx-ali-dev-v0-1-0
imported Available with BootMode=UEFI, Arch=x86_64, NvmeSupport=supported.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ci): wire ali publish into the workflow (EcsRamRole, no secret)

The al8 build runner is an Ali ECS instance, so add:
- 'Authenticate to Ali' (CLOUD==ali): aliyun configure --mode EcsRamRole (ossutil already uses the
  instance role) — no AK/SK secret needed.
- 'Stage C (ali)' (CLOUD==ali): ./publish-ali-image.sh (OSS + ImportImage), image name carries owner.
- ali config as vars: ALIYUN_REGION (ap-southeast-1), ALIYUN_ECS_ROLE (0g-confidential-disk-oss),
  OSS_BUCKET (0g-confidential-disk).
gcp Stage C unchanged (owner-in-name handled by PR #44). Both clouds now publish from one workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(cvm): rewrite README for multi-cloud (gcp+ali) + dimension/platform tables

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(cvm): fix .gitignore paths after gcp-cvm->cvm rename; untrack fde debs

The rename left .gitignore pointing at gcp-cvm/*, so the two binary FDE .debs
under cvm/ got tracked. Retarget the patterns to cvm/ and drop the debs from
git (they stay on disk; placed locally per README prerequisites).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: drop build-cvm on:release auto-trigger (dispatch-only) (#47)

The release auto-trigger only ever built one slice (cloud=gcp + default owner)
of the now multi-cloud/multi-owner matrix, and was the sole reason build.yml
needed a repo-scoped RELEASE_PAT (GITHUB_TOKEN-created releases don't fire
'on: release'). Remove it: workflow_dispatch is the single entry point, and
build.yml's release step goes back to the default GITHUB_TOKEN.

The reference-values Open-PR step keeps its RELEASE_PAT || GITHUB_TOKEN fallback
(separate use; falls back cleanly if the secret is removed).

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* verifier: support UKI reference values in registration + policy (#48)

The gcp+uki validation run failed at 'register on AS' with 'measurement.shim
empty': register-shared-as.sh hard-required the 5 grub components, and
policy.rego only verified the grub chain. UKI images carry a single
measurement.uki instead.

- policy.rego: add ref_uki + an all_bsa_digests helper + a second boot_chain_ok
  branch that matches the (specific, known) UKI digest against any boot-services
  application event. One branch per format; only the format whose ref set is
  populated fires, so an image verifies as exactly one (fail-closed otherwise).
- register-shared-as.sh: inject a literal set per known rule (empty set() when the
  key is absent) instead of erroring on missing grub keys; require >=1 value overall.

Validated with opa eval: grub positive=3 (no regression), uki positive=3,
digest-mismatch/cross-format=33 (fail-closed both ways).

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* cvm: default boot_format to grub for any cloud (uki opt-in) (#49)

Drop the per-cloud auto derivation (gcp->grub, ali->uki). boot_format is an
independent axis; 'auto' now resolves to grub regardless of cloud, and uki is an
explicit opt-in (BOOT_FORMAT=uki / boot_format=uki), for any cloud. Updates
build-tapp.sh, prepare-tapp.sh, the build-cvm.yml input description, and README.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: drop redundant boot_format 'auto' option, default grub directly (#50)

'auto' resolved to grub anyway (per #49), so it was identical to the 'grub'
option. Make the input options [grub, uki] with default grub, and simplify the
BOOT_FORMAT env to inputs.boot_format directly.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* cvm: add opt-in OVERWRITE for publish (delete+recreate existing image) (#51)

Version images are immutable by default — a published image may back running
confidential instances, and silently replacing it changes measurements and breaks
their attestation. But re-running the same version during testing hit a hard
'already exists' failure and required a manual delete.

Add OVERWRITE (env / dispatch input 'overwrite', default off): when the target
image name already exists, delete-then-recreate instead of failing. Wired into both
publish-gcp-image.sh (gcloud images delete) and publish-ali-image.sh (DeleteImage by
resolved ImageId), and the build-cvm.yml Stage C steps.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* cvm: make boot_format a first-class identifier dimension (name/refval/policy) (#52)

grub and uki produce different images with different measurement shapes (grub 5
components / uki 1), but boot_format wasn't in the image name, reference-value
path, branch, or AS policy id — so a gcp+uki build clobbered the gcp+grub one
(same name/path/PR #42/policy).

Add boot_format everywhere cloud is, and drop the redundant 0x owner prefix
globally so the longer names still fit GCP's 63-char cap:
- image name:  <imgbase>-<boot_format>-<version>-<owner40hex>   (og-tdx-dev-grub-v0-2-0-<40hex> = 63)
- refvalue:    verifier/reference-values/<cloud>/<boot_format>/<version>/<env>/<owner>.json
- policy id:   0g-tapp-<cloud>-<boot_format>-<version>-<env>-<owner>
- branch:      refvalues/<cloud>-<boot_format>-<version>-<env>-<owner>
- concurrency group also keyed by boot_format

owner is normalized identically in all scripts: strip 0x + lowercase. Updates
gen-reference-values.sh, open-refvalues-pr.sh, register-shared-as.sh, build-cvm.yml,
and both READMEs.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* cvm: add DISK_SIZE + image_name (custom output name) parameters (#54)

DISK_SIZE (build-tapp.sh env / build-cvm dispatch input, default 20G): grow the
working image up front (qemu-img resize + growpart /dev/sda 1 + resize2fs) before
Stage A/convert, so the produced verity-rootfs image can be larger than the cached
20G base. Only grows (>= current is a no-op; shrinking a populated fs is unsafe).

image_name (build-cvm dispatch input, default blank): override the published image
name; blank keeps the auto <imgbase>-<boot_format>-<version>-<owner>. Applies to both
gcp and ali Stage C (use a single env, not both, to avoid a name collision).

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: drop the advisory boot smoke test step from build-cvm (#55)

On this builder (no /dev/kvm) it runs under TCG, so the 600s window is truncated
before multi-user (false 'BOOT NOT CONFIRMED' on good images) and it was advisory
(continue-on-error) so it never gated — ~10min/build of noise. Keep
cvm/test/boot-smoke-test.sh for local / KVM runs; revisit as a hard gate once a
KVM-capable runner exists. Real boot + attestation validation is on TDX hardware.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: move image_name input to the top of the build-cvm dispatch form (#56)

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: runtime owner claim — unclaimed boot + measured ClaimOwner RPC

- config: owner_address now optional; initial_whitelist removed (use the
  measured add-to-whitelist RPC after claiming)
- server boots UNCLAIMED without an owner; first valid signer of ClaimOwner
  becomes owner, extended as a claim_owner runtime measurement event
- claimed owner persisted under /run (tmpfs) so a process restart within the
  same boot cannot reopen the claim; config/persisted mismatch refuses to start
- legacy baked-in owner still works and is now also measured at startup
- cli: claim-owner command with end-to-end verification; examples/claim_owner.sh

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* build/ci/docs: drop the per-owner image dimension (ownerless claim-at-runtime images)

- build-tapp.sh: OWNER_ADDRESS now optional (legacy); default builds an
  ownerless image whose owner is claimed at runtime as a measured event
- build-cvm.yml: remove the owner input/env and the owner tag from image
  names/family/concurrency; reference values register under the fixed 'any'
  owner slot (refvalues scripts unchanged)
- docs: README claim-owner section, cvm/README dimensions, reference-values
  README, EVIDENCE_AND_AS_VERIFICATION claim_owner reconciliation rule
- version: tapp-server 0.3.0 (new RPC), tapp-cli 0.3.0 (new command)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: ClaimConfig — extend claim to include chain+kbs runtime config

Renames ClaimOwner → ClaimConfig and extends it to carry the full runtime
configuration: chain_rpc_url, chain_contract_address, kbs_node_urls.

- proto: ClaimConfigRequest has chain/kbs fields; both proto copies synced
- server: measurement event is now claim_config and includes all fields;
  kms_client wrapped in Arc<RwLock> so dynamic-mode kbs_node_urls initialise
  the KMS client at claim time (no [kbs] section in config.toml needed)
- establish_owner_at_startup: passes chain/kbs from config.toml into the
  claim_config measurement so pre-baked and dynamic modes produce the same
  event shape — verifiers see the complete runtime config in both modes
- cli: claim-config command with --chain-rpc-url / --chain-contract / --kbs-urls
- examples/claim_owner.sh: updated for ClaimConfig RPC
- e2e: 14/14 with chain+kbs fields in the test claim

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* build: explicit canonical/custom build mode selection

Replaces the implicit OWNER_ADDRESS-present-or-not heuristic with an
explicit BUILD_MODE parameter (canonical | custom).

  canonical (default): owner-agnostic image — no owner baked in, one
    reference set for all owners (refvals under 'any' slot), image name
    has no owner tag. OWNER_ADDRESS is cleared even if accidentally set.

  custom: owner baked into config.toml — per-owner initrd measurement,
    per-owner refval set and image name. Requires OWNER_ADDRESS.

build-tapp.sh: BUILD_MODE env var; custom mode enforces OWNER_ADDRESS.
build-cvm.yml: build_mode input (choice); owner input (custom only);
  names step resolves img_suffix + owner_slot per mode; both CI paths
  (canonical and custom) fully supported.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* build: canonical refvals at <env>.json (no owner subdir/placeholder)

Drop the 'any' placeholder — canonical mode uses a clean path without an
owner dimension:
  canonical: verifier/reference-values/<cloud>/<boot_format>/<version>/<env>.json
             policy id: 0g-tapp-<cloud>-<boot_format>-<version>-<env>
  custom:    verifier/reference-values/<cloud>/<boot_format>/<version>/<env>/<owner>.json
             policy id: 0g-tapp-<cloud>-<boot_format>-<version>-<env>-<owner>

gen-reference-values.sh / register-shared-as.sh / open-refvalues-pr.sh:
  owner arg is now optional; omit for canonical, pass 0x address for custom.
  register-shared-as.sh auto-detects owner vs AS-endpoint for arg 5.

build-cvm.yml: owner_slot replaced by owner_tag (empty = canonical); scripts
  called with bare $OWNER_TAG so empty expands to nothing (not an empty string arg).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: require tapp-server >= v0.3.0 for canonical build mode

ClaimConfig RPC was introduced in 0.3.0; a canonical image built from
an older binary boots unclaimed with no way to claim it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: fail-fast input validation + fix custom-mode owner passing

- validate step moved to the very first step (before toolchain/download):
  canonical requires >= v0.3.0; custom requires an explicit well-formed
  0x owner (vars fallback removed — a silently-defaulted owner baked into
  a measured image is how mistakes ship); image_name+env=both rejected
  (matrix jobs would clobber each other); release binary existence probed
- fix: refvalue steps passed the 0x-stripped owner_tag, but
  register-shared-as.sh detects owner-vs-endpoint by the 0x prefix —
  custom mode would have treated the owner as the AS endpoint. Now the
  raw 0x address is passed everywhere (scripts strip it themselves) and
  the detection regex also accepts unprefixed 40-hex
- OWNER env is empty unless build_mode=custom (canonical steps can never
  see an owner); BUILD_MODE forwarded to build-tapp.sh explicitly
- docs: drop remaining 'any'-slot references; canonical/custom identifier
  tables; claim_config event shape in EVIDENCE_AND_AS_VERIFICATION.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* build: KBS_URLS optional in canonical mode

canonical images can receive kbs_node_urls at runtime via ClaimConfig;
custom mode still requires KBS_URLS to be baked in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: fix KBS_URLS passing (inline quoted assignment) + normalize literal []

- $KBS_ARG expanded from a variable is not parsed as an env assignment by
  the shell — with vars.TAPP_KBS_URLS='[]' the word KBS_URLS=[] became the
  command name ('command not found'). Keep the assignment inline and quoted.
- build-tapp.sh: treat a literal [] as empty (would interpolate into
  node_urls = [[]], broken TOML).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci: fix env passing to build-tapp.sh — no $VAR in assignment-prefix list

bash decides what is an assignment prefix BEFORE expansion: a variable
($EXTRA/$KBS_ARG, even when empty) in the prefix list turns every later
VAR=val word into the command name — the actual cause of both
'KBS_URLS=[]: command not found' failures.

BUILD_MODE/KBS_URLS are already inherited from the job env; custom mode
exports OWNER_ADDRESS explicitly. Verified locally with a simulated step
for both modes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* skill: update 0g-tapp-cli v1.4.0 — claim-config, unclaimed state, new policy id format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* cvm: RuntimeDirectoryPreserve=yes — keep claimed_owner across process restarts

systemd removes RuntimeDirectory on stop by default; restart = stop+start,
so /run/tapp/claimed_owner was wiped on every systemctl restart, allowing
re-claim. Preserve=yes keeps the dir across restarts but still clears it on
VM reboot (tmpfs) — matching the RTMR lifetime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: get-tapp-info shows runtime chain + kbs from ClaimConfig

- proto: add ChainConfigInfo message and chain field to TappConfigInfo
- server: ClaimedRuntimeConfig stores chain/kbs set at claim time;
  get_tapp_info reads runtime values, falls back to static config
- cli: display Chain and KBS sections in get-tapp-info output
- e2e: 14/14 still passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Wei <wei@0g.ai>
Wilbert957 added a commit that referenced this pull request Jul 20, 2026
…ages, verify-app owner reconcile (#66)

* feat: runtime CLI↔server/contract version-compatibility check (#26)

* feat: runtime CLI↔server/contract version-compatibility check

Implements the compatibility mechanism from docs/VERSIONING.md.

Rust (gRPC edge):
- tapp-common::compat::interface_warning compares MAJOR.MINOR (PATCH ignored),
  returning a warning when the peer's interface looks incompatible.
- tapp-cli build.rs stamps the expected server interface version (the
  workspace-root tapp-server MAJOR.MINOR) at build time — no number to maintain.
- create_client does a best-effort GetTappInfo on connect and warns to stderr on
  mismatch; it never hard-blocks (a genuinely missing RPC fails at the call).
- Bump tapp-cli 0.1.0 -> 0.1.1 (adds the on-connect warning behavior).

Contract (on-chain edge):
- Add a version() view to TappRegistry returning "0.1.0" so callers can read the
  live implementation version behind the stable beacon proxy address.
- Rename local `version` vars to `ackVersion` to avoid shadowing the new view.

Server needs no change: GetTappInfo already reports the full tapp-server version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(contract): record TappRegistry v0.1.0 upgrade (adds version())

Deployed new implementation 0x9Ea52Ef383e8eA3fe7F0890309D3C62b2FC1Ac2B behind
the stable beacon proxy 0x2Ce8...165c9 on 0G Galileo testnet; beacon.upgradeTo
tx 0x8a003a1a. version() now returns "0.1.0" on-chain, storage preserved,
source-verified on chainscan-galileo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: sync Cargo.lock (tempfile dev-dependency)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(gcp-cvm): add Stage C (publish to GCP) + document the 3-stage pipeline (#29)

The build produced a qcow2 but stopped short of a GCP image. Add the "last mile":

- publish-gcp-image.sh (Stage C): qemu-img convert qcow2 -> raw -> sparse oldgnu tar.gz ->
  gsutil cp -> gcloud compute images create with UEFI_COMPATIBLE,GVNIC,SEV_CAPABLE,TDX_CAPABLE.
  Parameterized (GCS_BUCKET/GCP_PROJECT/GUEST_OS_FEATURES, defaults gs://tapp-image + g-devops);
  refuses to clobber an existing image name. Kept separate from the build because it needs
  gcloud/gsutil auth + network upload.
- build-gcp-tapp.sh: opt-in Stage C via PUBLISH_AS=<image-name> (runs publish after Stage B);
  also fix the now-stale tapp-data-provision.service Description (format-or-adopt, not "carve").
- Docs: README pipeline overview (Stage 0 base prep / A / B / optional smoke test / C),
  publish-gcp-image.sh in the file table, PUBLISH_AS env var, a "Publish to GCP (Stage C)"
  section; main doc §14 (Stage C steps + stages-at-a-glance).

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: start-app --register-onchain — idempotent on-chain registration before the app starts (#28)

* feat: start-app --register-onchain registers the app before it starts

New optional flow for 'start-app': with --register-onchain (+ --rpc-url,
--contract, --stake-wei), the CLI idempotently ensures the app is
registered on-chain BEFORE its containers start:

- app not registered            -> registerApp (this node = first node)
- registered, signer not a node -> addNode (per-node override iff differs)
- signer already in node list   -> skip, just start

To get real hashes before anything runs, StartAppRequest gains a
measure_only flag: the server stages files, 'docker compose pull's the
images and computes compose/volumes/image hashes without starting
containers, returning them via new TaskResult fields. The follow-up
normal StartApp starts from the cached images.

GetAppKey now creates the app key if missing (same as GetEvidence), so
the signer address is available before the app starts; the key lives in
server memory so the app gets the same key when it runs.

e2e-tested against 0G Galileo testnet (registerApp + idempotent skip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: document start-app --register-onchain in README, runbook, and skill

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix: make start-service idempotent, dropping the already-running pre-check (#30)

start-service failed forever with 'Service <svc> is already running' after
stop-service, whenever a running sibling service's name contained the stopped
one (e.g. sandbox vs sandbox-redis): the pre-check did a substring match on
docker compose ps output. (#27)

Rather than fixing the match, drop the pre-check: docker compose up -d is
already idempotent (running + unchanged config = no-op), and removing it also
unblocks 'start-service --pull' on a running service, whose re-measure path
existed downstream but was unreachable. Typo'd service names still fail the
task with docker's own 'no such service' error.

e2e-verified: restart-after-stop with prefix sibling running, no-op on running
service, --pull on running service, and bad-name failure.

Fixes #27

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* chore: bump tapp-server and tapp-cli to 0.2.0

Per docs/VERSIONING.md, for the changes merged to dev:
- tapp-server 0.1.1 -> 0.2.0: gRPC interface changed (#28 adds
  StartAppRequest.measure_only + TaskResult measurement fields; #30
  changes StartService semantics for already-running services)
- tapp-cli 0.1.1 -> 0.2.0: new user-facing flags (#28:
  --register-onchain / --rpc-url / --contract / --stake-wei on start-app)
- contract unchanged (0.1.0); tapp-common internal, additive only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(logging): bound file-log growth + move CVM logs off the RAM rootfs (#34)

* fix(logging): bound file-log growth + move CVM logs off the RAM rootfs (#23)

- RollingFileAppender now keeps at most logging.max_log_files daily files
  (new config field, default 7) — daily rotation alone never deletes old
  files, so total size was unbounded
- gcp-cvm build: tapp-server file logs go to /data/log/tapp/ (persistent
  disk) instead of /var/log/tapp/ on the RAM-backed rootfs overlay, and
  tapp-server.service gains RequiresMountsFor=/data (same fail-loud policy
  as docker/containerd)

Closes #23

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(logging): prune stale daily logs at startup, not only at in-process rotation

tracing-appender's max_log_files only prunes inside refresh_writer (when the
date rolls over while the process runs) — verified empirically: files left by
previous runs survive until the first in-process midnight, and a process that
never lives past midnight never prunes. Add a best-effort startup prune of
{prefix}.* files before building the appender, with a unit test.

Also fix the stale KmsClient::new arity in kms_client tests (predates the
retry-config parameter; lib tests did not compile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(server): pass derivation material through GetSecretResource to KMS /app-key (#35)

- proto: GetSecretResourceRequest gains optional 'string material = 2' —
  hex-encoded derivation material, opaque to tapp, forwarded verbatim
- kms_client: KmsRequest carries material; omitted from the JSON body when
  empty so the request stays byte-identical to the pre-material format
  (KMS derives purely from app_id, existing callers untouched)
- tests: new wiremock test asserting material is forwarded verbatim and
  omitted when empty; also fix stale KmsClient::new arity in existing tests

Closes #33

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(readme): document max_log_files + GetSecretResource material; fix stale config sample

- [logging] sample: path -> file_path (actual config key), add format and
  the new max_log_files retention cap (#34)
- config sample and KMS section: [kms] cluster_urls -> [kbs] node_urls
  (the section/key tapp-server actually reads), port 8080 -> 9091
- KMS Integration: document the optional derivation material passthrough (#35)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): update GetSecretResourceRequest for new material field

PR #35 added the material field to GetSecretResourceRequest and updated
the server, but tapp-cli's initializer was left behind, breaking
cargo build --workspace (E0063) — this is what failed the release
pipeline on v0.2.0-test. Expose it as an optional --material flag
(default empty, unchanged behavior).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(ci): cache toolchain layers, use official rustup source, drop -j1

Release run 29242499831 took 2h23m: rustup via the USTC mirror crawled
for 88 min on the US-based runner, yum via Aliyun OSS took 11 min, and
CARGO_BUILD_JOBS=1 made the compile itself 13 min.

- Split the Dockerfile into a toolchain stage (yum + rustup, no sources)
  and a builder stage; cache the toolchain stage in the GHA cache so it
  only rebuilds when the Dockerfile changes.
- Default rustup to static.rust-lang.org; domestic mirrors stay available
  via --build-arg for builds from inside China. Mirror choice does not
  affect the installed bits.
- Drop CARGO_BUILD_JOBS=1: job parallelism only affects scheduling, not
  codegen. Verified empirically — clean -j1 and -j4 builds with identical
  paths produce sha256-identical tapp-server and tapp-cli.

Expected: ~5-8 min warm cache, ~15 min cold, vs 27 min baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(verifier): reference values for gcp/uki/v0.2.0/dev/ea695c312ce119de347425b29aff85371c9d1837 (#53)

Co-authored-by: cvm-ci <cvm-ci@0g.ai>

* feat: runtime owner claim — ownerless CVM images, measured ClaimOwner RPC (#58)

* feat: runtime CLI↔server/contract version-compatibility check (#26)

* feat: runtime CLI↔server/contract version-compatibility check

Implements the compatibility mechanism from docs/VERSIONING.md.

Rust (gRPC edge):
- tapp-common::compat::interface_warning compares MAJOR.MINOR (PATCH ignored),
  returning a warning when the peer's interface looks incompatible.
- tapp-cli build.rs stamps the expected server interface version (the
  workspace-root tapp-server MAJOR.MINOR) at build time — no number to maintain.
- create_client does a best-effort GetTappInfo on connect and warns to stderr on
  mismatch; it never hard-blocks (a genuinely missing RPC fails at the call).
- Bump tapp-cli 0.1.0 -> 0.1.1 (adds the on-connect warning behavior).

Contract (on-chain edge):
- Add a version() view to TappRegistry returning "0.1.0" so callers can read the
  live implementation version behind the stable beacon proxy address.
- Rename local `version` vars to `ackVersion` to avoid shadowing the new view.

Server needs no change: GetTappInfo already reports the full tapp-server version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(contract): record TappRegistry v0.1.0 upgrade (adds version())

Deployed new implementation 0x9Ea52Ef383e8eA3fe7F0890309D3C62b2FC1Ac2B behind
the stable beacon proxy 0x2Ce8...165c9 on 0G Galileo testnet; beacon.upgradeTo
tx 0x8a003a1a. version() now returns "0.1.0" on-chain, storage preserved,
source-verified on chainscan-galileo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: sync Cargo.lock (tempfile dev-dependency)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(gcp-cvm): add Stage C (publish to GCP) + document the 3-stage pipeline (#29)

The build produced a qcow2 but stopped short of a GCP image. Add the "last mile":

- publish-gcp-image.sh (Stage C): qemu-img convert qcow2 -> raw -> sparse oldgnu tar.gz ->
  gsutil cp -> gcloud compute images create with UEFI_COMPATIBLE,GVNIC,SEV_CAPABLE,TDX_CAPABLE.
  Parameterized (GCS_BUCKET/GCP_PROJECT/GUEST_OS_FEATURES, defaults gs://tapp-image + g-devops);
  refuses to clobber an existing image name. Kept separate from the build because it needs
  gcloud/gsutil auth + network upload.
- build-gcp-tapp.sh: opt-in Stage C via PUBLISH_AS=<image-name> (runs publish after Stage B);
  also fix the now-stale tapp-data-provision.service Description (format-or-adopt, not "carve").
- Docs: README pipeline overview (Stage 0 base prep / A / B / optional smoke test / C),
  publish-gcp-image.sh in the file table, PUBLISH_AS env var, a "Publish to GCP (Stage C)"
  section; main doc §14 (Stage C steps + stages-at-a-glance).

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: start-app --register-onchain — idempotent on-chain registration before the app starts (#28)

* feat: start-app --register-onchain registers the app before it starts

New optional flow for 'start-app': with --register-onchain (+ --rpc-url,
--contract, --stake-wei), the CLI idempotently ensures the app is
registered on-chain BEFORE its containers start:

- app not registered            -> registerApp (this node = first node)
- registered, signer not a node -> addNode (per-node override iff differs)
- signer already in node list   -> skip, just start

To get real hashes before anything runs, StartAppRequest gains a
measure_only flag: the server stages files, 'docker compose pull's the
images and computes compose/volumes/image hashes without starting
containers, returning them via new TaskResult fields. The follow-up
normal StartApp starts from the cached images.

GetAppKey now creates the app key if missing (same as GetEvidence), so
the signer address is available before the app starts; the key lives in
server memory so the app gets the same key when it runs.

e2e-tested against 0G Galileo testnet (registerApp + idempotent skip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: document start-app --register-onchain in README, runbook, and skill

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix: make start-service idempotent, dropping the already-running pre-check (#30)

start-service failed forever with 'Service <svc> is already running' after
stop-service, whenever a running sibling service's name contained the stopped
one (e.g. sandbox vs sandbox-redis): the pre-check did a substring match on
docker compose ps output. (#27)

Rather than fixing the match, drop the pre-check: docker compose up -d is
already idempotent (running + unchanged config = no-op), and removing it also
unblocks 'start-service --pull' on a running service, whose re-measure path
existed downstream but was unreachable. Typo'd service names still fail the
task with docker's own 'no such service' error.

e2e-verified: restart-after-stop with prefix sibling running, no-op on running
service, --pull on running service, and bad-name failure.

Fixes #27

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* chore: bump tapp-server and tapp-cli to 0.2.0

Per docs/VERSIONING.md, for the changes merged to dev:
- tapp-server 0.1.1 -> 0.2.0: gRPC interface changed (#28 adds
  StartAppRequest.measure_only + TaskResult measurement fields; #30
  changes StartService semantics for already-running services)
- tapp-cli 0.1.1 -> 0.2.0: new user-facing flags (#28:
  --register-onchain / --rpc-url / --contract / --stake-wei on start-app)
- contract unchanged (0.1.0); tapp-common internal, additive only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(logging): bound file-log growth + move CVM logs off the RAM rootfs (#34)

* fix(logging): bound file-log growth + move CVM logs off the RAM rootfs (#23)

- RollingFileAppender now keeps at most logging.max_log_files daily files
  (new config field, default 7) — daily rotation alone never deletes old
  files, so total size was unbounded
- gcp-cvm build: tapp-server file logs go to /data/log/tapp/ (persistent
  disk) instead of /var/log/tapp/ on the RAM-backed rootfs overlay, and
  tapp-server.service gains RequiresMountsFor=/data (same fail-loud policy
  as docker/containerd)

Closes #23

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(logging): prune stale daily logs at startup, not only at in-process rotation

tracing-appender's max_log_files only prunes inside refresh_writer (when the
date rolls over while the process runs) — verified empirically: files left by
previous runs survive until the first in-process midnight, and a process that
never lives past midnight never prunes. Add a best-effort startup prune of
{prefix}.* files before building the appender, with a unit test.

Also fix the stale KmsClient::new arity in kms_client tests (predates the
retry-config parameter; lib tests did not compile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(server): pass derivation material through GetSecretResource to KMS /app-key (#35)

- proto: GetSecretResourceRequest gains optional 'string material = 2' —
  hex-encoded derivation material, opaque to tapp, forwarded verbatim
- kms_client: KmsRequest carries material; omitted from the JSON body when
  empty so the request stays byte-identical to the pre-material format
  (KMS derives purely from app_id, existing callers untouched)
- tests: new wiremock test asserting material is forwarded verbatim and
  omitted when empty; also fix stale KmsClient::new arity in existing tests

Closes #33

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(readme): document max_log_files + GetSecretResource material; fix stale config sample

- [logging] sample: path -> file_path (actual config key), add format and
  the new max_log_files retention cap (#34)
- config sample and KMS section: [kms] cluster_urls -> [kbs] node_urls
  (the section/key tapp-server actually reads), port 8080 -> 9091
- KMS Integration: document the optional derivation material passthrough (#35)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* draft(ci): automate og-tdx CVM build → publish → attest (version×env×owner) (#32)

* draft(ci): automate og-tdx CVM build/publish/attest (version×env×owner)

DRAFT for review — not wired to a runner yet.
- .github/workflows/build-cvm.yml: dispatch(version,env,owner,publish) on a self-hosted
  al8 runner → build (HARDEN per env, OWNER baked, KBS fixed) → smoke gate → publish
  versioned image+family → compute+register reference values → PR them into dev.
- verifier/register-shared-as.sh: add the owner dimension (path <version>/<env>/<owner>.json,
  policy id 0g-tapp-<version>-<env>-<owner>) — owner is measured (folded into initrd).
- gcp-cvm/ci/{gen-reference-values,fetch-inputs,open-refvalues-pr}.sh helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* draft(ci): build the #128-fixed cryptpilot-fde from pinned fork source; real ref-value generator

- gcp-cvm/ci/build-cryptpilot-fde.sh: compile cryptpilot-fde-host from the 0gfoundation
  cryptpilot fork at a pinned commit (transparent/reproducible vs a prebuilt blob). The stock
  0.7.0 errors "saved_entry not found" on a never-booted image; the fork falls back to the
  default grub.cfg menuentry.
- gcp-cvm/ci/gen-reference-values.sh: rewritten to use the fixed binary's real JSON output
  (filter to the five measurement.<component>.SHA-384 keys via python; fail if any is empty).
  Validated end-to-end against og-tdx-dev.qcow2.
- build-cvm.yml: build the fixed cryptpilot-fde as a step, pass it to the ref-value step.
- reference-values/README.md: owner dimension (<version>/<env>/<owner>.json) + fork-build flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* draft(ci): move fixed config to repo Actions Variables (KBS/project/bucket/AS/fde-ref)

All tunables now come from repo/org Variables (vars.*) with in-file defaults, so KBS URLs,
GCP project/bucket, AS endpoint and the cryptpilot-fde fork commit can be changed in GitHub
settings without editing the workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* draft(ci): local base-image cache + non-blocking smoke test + checkout dispatched ref

- fetch-inputs.sh: support BASE_IMAGE_URL as file:// or absolute local path (COPY, never symlink,
  since Stage A mutates the input) — use the persistent runner cache /root/base-noble.qcow2.
- workflow: BASE_IMAGE_URL from vars; smoke test continue-on-error (advisory until validated on a
  KVM runner); checkout the dispatched ref (not hardcoded dev) so the draft can be dry-run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* draft(ci): pin cryptpilot-fde to fix/srv-default-entry (saved_entry fallback), not the ESP-convert branch

Dry-run caught it: CRYPTPILOT_REF=52a77a2 (fix/gcp-convert-esp-sync) has no show-reference-value
fallback, so gen-reference-values errored 'saved_entry not found' on a never-booted image. The
fallback lives on fix/srv-default-entry @ 64fe4e744574c8e0273dd1e5d54ceae9714748a0 (falls back to
the default grub.cfg menuentry; also has --hash-algo). Pin to the full SHA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(gcp-cvm): migrate to cryptpilot 0.8.0 + #128/#130; simplify build (drop fix-B & keep-generic workarounds)

Realizes what #130 was for: let cryptpilot-convert do the ESP-grub sync itself instead of our scripts.
Validated end-to-end on GCP TDX hardware (boot → /data auto-provision → docker/sysbox → tapp-server).

Toolchain (ci/setup-toolchain.sh, reproducible): install released cryptpilot 0.8.0 (crypt/verity/
fde-host + dracut) and overlay convert (#130) + fde-host (#128) built from the pinned fork commit
0gfoundation/cryptpilot@57d7512 (feat/0g-gcp-cvm = 0.8.0 + #128 + #130). In-image runtime = released
0.8.0 cryptpilot-fde-guest deb (fetch-inputs).

Script simplification:
- prepare-gcp-tapp.sh: DROP fix-B (convert #130 syncs the ESP grub.cfg + modules itself); add
  CRYPTPILOT_CONVERT override.
- build-gcp-tapp.sh: FDE_PACKAGE -> 0.8.0 guest deb; config.toml gains max_log_files=7 (bounds
  /var/log/tapp on the RAM rootfs — issue #23).
- ci/gen-reference-values.sh: use /usr/bin/cryptpilot-fde-host (from setup-toolchain).
- ci/build-cryptpilot-fde.sh: removed (subsumed by setup-toolchain).
- workflow: add "Set up 0.8.0 toolchain" step; ref-value step uses the installed fde-host.
- add publish-gcp-image.sh (Stage C) to this branch.

TODO (deferred): drop the leftover -generic kernel for a gcp-only image; wire tapp-server from dev
in the real pipeline; re-verify prod (HARDEN=1) variant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ci): dual trigger — release auto-builds default owner; dispatch for custom owner

- on: release (published) -> build the DEFAULT deployment (vars.DEFAULT_OWNER_ADDRESS), both env,
  publish + register. workflow_dispatch -> custom per-owner (owner input; blank = default).
- resolve version/owner/publish for both triggers; matrix env defaults to both on release.
- header comment updated to the 0.8.0 toolchain (ci/setup-toolchain.sh).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(ci): rename DEFAULT_OWNER_ADDRESS var -> OWNER_ADDRESS

Consistent naming with the other vars.* config (no DEFAULT_ prefix); it still
serves as the default/fallback owner for release builds and blank dispatch input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* harden(ci): keep GCP SA credential off the persistent runner

Point gcloud/gsutil at a job-scoped CLOUDSDK_CONFIG under RUNNER_TEMP, write the
key there only long enough to activate, delete it immediately, and revoke +
wipe the config dir in cleanup. Nothing lands in the runner's ~/.config/gcloud.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): don't use runner.temp in job-level env (startup failure)

runner context isn't available in jobs.<id>.env — GitHub rejected the workflow
file. Set CLOUDSDK_CONFIG in the auth step via $RUNNER_TEMP and export it into
$GITHUB_ENV for later steps; guard cleanup when it's unset (publish=false).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): install grpcurl in setup-toolchain (register-shared-as needs it) (#37)

The reference-value registration step (verifier/register-shared-as.sh) calls grpcurl
for SetAttestationPolicy, but grpcurl isn't in the al8 repos and wasn't provisioned,
so publish=true runs failed at 'grpcurl: command not found'. Install the release
binary to /usr/local/bin if missing.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(verifier): add cloud dimension to reference values + AS policy (#39)

Each cloud builds its own image (GCP uses linux-image-gcp/gVNIC/GCP boot fixes; Ali
uses its own kernel/drivers), so boot-chain measurements genuinely differ per cloud.
Without a cloud dimension, clouds collide on the shared repo path and shared AS policy id.

- path:   verifier/reference-values/<cloud>/<version>/<env>/<owner>.json
- policy: 0g-tapp-<cloud>-<version>-<env>-<owner>
- gen-reference-values.sh / register-shared-as.sh / open-refvalues-pr.sh take <cloud> first
- build-cvm.yml sets CLOUD=gcp (this kit is GCP-specific; an Ali pipeline sets ali)
- README + register usage updated; also fixed stale build-cryptpilot-fde.sh reference

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): reference-values PR write perms + release auto-trigger (RELEASE_PAT) (#40)

* fix(ci): grant contents/pull-requests write for the reference-values PR

The Open-PR step pushes a refvalues/* branch and opens a PR into dev via GITHUB_TOKEN,
but the default token is read-only -> 'Permission denied to github-actions[bot]' 403 on
git push. Add a workflow permissions block granting contents:write + pull-requests:write.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): fix release auto-trigger + refvalues PR via RELEASE_PAT

- build.yml: create the GitHub Release with RELEASE_PAT (falls back to GITHUB_TOKEN).
  Releases created by GITHUB_TOKEN do NOT fire on:release in other workflows (anti-recursion),
  so build-cvm's release auto-trigger never ran. A PAT-created release fires it.
- build-cvm.yml: Open-PR step uses RELEASE_PAT for 'gh pr create' (covers orgs that disable
  Actions-created PRs); git push still uses GITHUB_TOKEN via the permissions block.

Requires repo secret RELEASE_PAT (fine-grained PAT: Contents+Pull-requests write on 0g-tapp).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(main): CLI material compile fix + CI build speed (cherry-picks from dev) (#41)

* fix(cli): update GetSecretResourceRequest for new material field

PR #35 added the material field to GetSecretResourceRequest and updated
the server, but tapp-cli's initializer was left behind, breaking
cargo build --workspace (E0063) — this is what failed the release
pipeline on v0.2.0-test. Expose it as an optional --material flag
(default empty, unchanged behavior).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(ci): cache toolchain layers, use official rustup source, drop -j1

Release run 29242499831 took 2h23m: rustup via the USTC mirror crawled
for 88 min on the US-based runner, yum via Aliyun OSS took 11 min, and
CARGO_BUILD_JOBS=1 made the compile itself 13 min.

- Split the Dockerfile into a toolchain stage (yum + rustup, no sources)
  and a builder stage; cache the toolchain stage in the GHA cache so it
  only rebuilds when the Dockerfile changes.
- Default rustup to static.rust-lang.org; domestic mirrors stay available
  via --build-arg for builds from inside China. Mirror choice does not
  affect the installed bits.
- Drop CARGO_BUILD_JOBS=1: job parallelism only affects scheduling, not
  codegen. Verified empirically — clean -j1 and -j4 builds with identical
  paths produce sha256-identical tapp-server and tapp-cli.

Expected: ~5-8 min warm cache, ~15 min cold, vs 27 min baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): include owner in the published GCP image name/family (avoid collision) (#44)

The GCP image name was og-tdx[-dev]-<version> with no owner, so two owners building the same
version×env would collide on the image name (publish refuses an existing name). owner is a measured
dimension, so bake it into the name + family: og-tdx[-dev]-<version>-<owner(lowercased)>.
Add a name length/pattern guard to publish-gcp-image.sh (name now carries the ~42-char owner; GCP
image names are max 63 chars / [a-z][-a-z0-9]*).

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Multi-cloud CVM build kit (GCP + Alibaba Cloud) (#46)

* feat(cvm): parameterize build/prepare by CLOUD (gcp|ali); fix branch to ali for testing

Integrate (not fork) Ali support by branching the few cloud-specific points on $CLOUD:
- prepare-*.sh: gcp swaps in linux-image-gcp + fix A + verifies GCP ESP; ali keeps the base's
  generic kernel (Ali ECS boots generic via virtio) — skips kernel swap/fix A/ESP verify.
  convert / DNS / nbd are shared. GCP behavior unchanged (CLOUD defaults to gcp).
- build-*.sh: export CLOUD to prepare (stage B); stage C picks publish-<cloud>-image.sh.
- build-cvm.yml: CLOUD temporarily FIXED to 'ali' on this branch to test the Ali build path
  (per '固定走 aliyun 流程'); becomes an input/matrix before merging to main.

Shared (unchanged, cloud-agnostic): tapp-server/config.toml/docker/sysbox//data/DNS/netplan +
ci/gen-reference-values, ci/open-refvalues-pr, ci/setup-toolchain, register-shared-as.

TODO: publish-ali-image.sh (OSS + aliyun ecs ImportImage); ali dev-variant guest agent
(google-guest-agent is gcp-only); validate on a real Ali TDX instance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cvm): ali dev variant relies on cloud-init for SSH/network (skip google-guest-agent)

HARDEN=0 dev keeps cloud-init (only HARDEN=1 purges it); on Alibaba Cloud cloud-init injects the
instance SSH key + configures networking from the Ali metadata service, so an ali dev image is
SSH-able without the GCP-only google-guest-agent. GCP dev unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cvm): ali uses UKI/systemd-boot (--uki) + pins cloud-init datasource to AliYun

Per the Alibaba confidential-disk guide, Ali's cryptpilot flow is UKI/systemd-boot, not grub:
- prepare (ali): keep generic kernel; install dracut/dracut-core/systemd-boot-efi (UKI prereqs);
  cryptpilot-convert gets --uki for ali (gcp stays grub + #130 ESP sync). No GCP ESP handling on ali.
- build (ali dev): pin cloud-init datasource_list: [ AliYun ] so it reliably reads the Ali metadata
  service and injects the instance SSH key + network (Alibaba recommends pinning over ds-identify).

GCP path unchanged (CLOUD=gcp branches untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cvm): gpg --dearmor with --batch --no-tty (fails in tty-less env)

The SGX/docker repo-key setup piped into 'gpg --dearmor' without --batch/--no-tty; in a fully
detached (no controlling tty) environment gpg errors 'cannot open /dev/tty', aborting the guest
provisioning. --batch --no-tty is the correct non-interactive form (identical dearmor output).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cvm): pre-install dracut-network for ali (cryptpilot-fde-guest dep)

cryptpilot-fde-guest depends on dracut-network; pre-install it in the ali kernel step so the guest
deb install during convert doesn't hit a missing-dep dpkg error (convert auto-resolves, but cleaner).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): gen-reference-values captures the tool's actual components (grub 5 / UKI 1)

The reference-value component set is a property of the boot FORMAT, not the cloud: grub emits
shim/grub/kernel/initrd/kernel_cmdline (5); UKI emits a single measurement.uki. The script hardcoded
the 5 grub keys and failed on a UKI image. Make it format/cloud-agnostic: keep whatever non-empty
measurement.*.SHA-384 keys show-reference-value emits (require >=1). Verified on a real UKI image
(-> measurement.uki.SHA-384). cloud stays only the output-path dimension.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cvm): add [server] unix_socket_path to config.toml + RuntimeDirectory=tapp

tapp-server also serves gRPC on a Unix domain socket (/run/tapp/tapp.sock) that app containers
bind-mount. Add the [server] unix_socket_path config, and RuntimeDirectory=tapp on the service so
/run/tapp exists (0755, cleaned on stop) for tapp-server to bind it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(cvm): make BOOT_FORMAT (grub|uki) an independent build dimension

Decouple boot format from cloud: CLOUD picks kernel/guest (gcp kernel+fixA vs generic), BOOT_FORMAT
picks the boot format (grub vs uki -> --uki + UKI prereqs + ESP-verify gating). Default by cloud
(gcp->grub, ali->uki) but overridable (e.g. a uki image on gcp). A CVM is built as exactly one
CLOUD × one BOOT_FORMAT = one measurement chain (grub 5 components / uki 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(cvm): rename gcp-cvm -> cvm; build-gcp-tapp.sh -> build-tapp.sh; prepare-gcp-tapp.sh -> prepare-tapp.sh

The kit is now multi-cloud (gcp + ali), so the gcp-specific names are misleading. Directory and the
two shared-core scripts renamed; publish-gcp-image.sh keeps its name (GCP-specific; ali gets
publish-ali-image.sh). All references updated (build-cvm.yml paths, script internals, docs). No
behavior change — pure rename + path updates. cryptpilot-gcp-boot-fix.md keeps its name (GCP grub doc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ci): cloud + boot_format as workflow dimensions (un-fix ali)

- add workflow_dispatch inputs: cloud (gcp|ali, default gcp), boot_format (auto|grub|uki, default auto).
- CLOUD = inputs.cloud || gcp; BOOT_FORMAT = auto -> '' (build-tapp derives per cloud). release -> gcp.
- concurrency group keyed by cloud too.
- gate 'Authenticate to GCP' + 'Stage C publish' on CLOUD==gcp (ali image publish via OSS+ImportImage
  is TODO); reference-values compute/register + Open-PR stay cloud-agnostic (run for ali too).

Replaces the branch's temporary 'CLOUD: ali' pin — cloud is now a real dispatch dimension.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cvm): add publish-ali-image.sh (OSS + aliyun ecs ImportImage)

Ali Stage C per the confidential-disk flow: ossutil cp the qcow2 to OSS -> ImportImage
(Architecture=x86_64, BootMode=UEFI, Format=QCOW2) -> enable NVMe (ModifyImageAttribute
Features.NvmeSupport=supported, set after import is kicked off) -> wait until Available.
The 4 easy-to-get-wrong params (64-bit / UEFI / QCOW2 / NVMe-after-import) are pinned.
Requires aliyun + ossutil authenticated (ALIYUN_REGION + creds). DRAFT — not yet run end-to-end
(needs Ali OSS/creds); build-tapp.sh Stage C already dispatches to it for CLOUD=ali.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cvm): NVMe via --Features.NvmeSupport supported --force (CLI schema gap)

The aliyun CLI's ModifyImageAttribute schema doesn't expose the Features.NvmeSupport sub-field as a
flag (rejects both --Features.NvmeSupport and JSON --Features 'flat format required'); --force skips
client validation and sends the flat param the server wants. Validated end-to-end: og-tdx-ali-dev-v0-1-0
imported Available with BootMode=UEFI, Arch=x86_64, NvmeSupport=supported.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ci): wire ali publish into the workflow (EcsRamRole, no secret)

The al8 build runner is an Ali ECS instance, so add:
- 'Authenticate to Ali' (CLOUD==ali): aliyun configure --mode EcsRamRole (ossutil already uses the
  instance role) — no AK/SK secret needed.
- 'Stage C (ali)' (CLOUD==ali): ./publish-ali-image.sh (OSS + ImportImage), image name carries owner.
- ali config as vars: ALIYUN_REGION (ap-southeast-1), ALIYUN_ECS_ROLE (0g-confidential-disk-oss),
  OSS_BUCKET (0g-confidential-disk).
gcp Stage C unchanged (owner-in-name handled by PR #44). Both clouds now publish from one workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(cvm): rewrite README for multi-cloud (gcp+ali) + dimension/platform tables

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(cvm): fix .gitignore paths after gcp-cvm->cvm rename; untrack fde debs

The rename left .gitignore pointing at gcp-cvm/*, so the two binary FDE .debs
under cvm/ got tracked. Retarget the patterns to cvm/ and drop the debs from
git (they stay on disk; placed locally per README prerequisites).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: drop build-cvm on:release auto-trigger (dispatch-only) (#47)

The release auto-trigger only ever built one slice (cloud=gcp + default owner)
of the now multi-cloud/multi-owner matrix, and was the sole reason build.yml
needed a repo-scoped RELEASE_PAT (GITHUB_TOKEN-created releases don't fire
'on: release'). Remove it: workflow_dispatch is the single entry point, and
build.yml's release step goes back to the default GITHUB_TOKEN.

The reference-values Open-PR step keeps its RELEASE_PAT || GITHUB_TOKEN fallback
(separate use; falls back cleanly if the secret is removed).

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* verifier: support UKI reference values in registration + policy (#48)

The gcp+uki validation run failed at 'register on AS' with 'measurement.shim
empty': register-shared-as.sh hard-required the 5 grub components, and
policy.rego only verified the grub chain. UKI images carry a single
measurement.uki instead.

- policy.rego: add ref_uki + an all_bsa_digests helper + a second boot_chain_ok
  branch that matches the (specific, known) UKI digest against any boot-services
  application event. One branch per format; only the format whose ref set is
  populated fires, so an image verifies as exactly one (fail-closed otherwise).
- register-shared-as.sh: inject a literal set per known rule (empty set() when the
  key is absent) instead of erroring on missing grub keys; require >=1 value overall.

Validated with opa eval: grub positive=3 (no regression), uki positive=3,
digest-mismatch/cross-format=33 (fail-closed both ways).

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* cvm: default boot_format to grub for any cloud (uki opt-in) (#49)

Drop the per-cloud auto derivation (gcp->grub, ali->uki). boot_format is an
independent axis; 'auto' now resolves to grub regardless of cloud, and uki is an
explicit opt-in (BOOT_FORMAT=uki / boot_format=uki), for any cloud. Updates
build-tapp.sh, prepare-tapp.sh, the build-cvm.yml input description, and README.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: drop redundant boot_format 'auto' option, default grub directly (#50)

'auto' resolved to grub anyway (per #49), so it was identical to the 'grub'
option. Make the input options [grub, uki] with default grub, and simplify the
BOOT_FORMAT env to inputs.boot_format directly.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* cvm: add opt-in OVERWRITE for publish (delete+recreate existing image) (#51)

Version images are immutable by default — a published image may back running
confidential instances, and silently replacing it changes measurements and breaks
their attestation. But re-running the same version during testing hit a hard
'already exists' failure and required a manual delete.

Add OVERWRITE (env / dispatch input 'overwrite', default off): when the target
image name already exists, delete-then-recreate instead of failing. Wired into both
publish-gcp-image.sh (gcloud images delete) and publish-ali-image.sh (DeleteImage by
resolved ImageId), and the build-cvm.yml Stage C steps.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* cvm: make boot_format a first-class identifier dimension (name/refval/policy) (#52)

grub and uki produce different images with different measurement shapes (grub 5
components / uki 1), but boot_format wasn't in the image name, reference-value
path, branch, or AS policy id — so a gcp+uki build clobbered the gcp+grub one
(same name/path/PR #42/policy).

Add boot_format everywhere cloud is, and drop the redundant 0x owner prefix
globally so the longer names still fit GCP's 63-char cap:
- image name:  <imgbase>-<boot_format>-<version>-<owner40hex>   (og-tdx-dev-grub-v0-2-0-<40hex> = 63)
- refvalue:    verifier/reference-values/<cloud>/<boot_format>/<version>/<env>/<owner>.json
- policy id:   0g-tapp-<cloud>-<boot_format>-<version>-<env>-<owner>
- branch:      refvalues/<cloud>-<boot_format>-<version>-<env>-<owner>
- concurrency group also keyed by boot_format

owner is normalized identically in all scripts: strip 0x + lowercase. Updates
gen-reference-values.sh, open-refvalues-pr.sh, register-shared-as.sh, build-cvm.yml,
and both READMEs.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* cvm: add DISK_SIZE + image_name (custom output name) parameters (#54)

DISK_SIZE (build-tapp.sh env / build-cvm dispatch input, default 20G): grow the
working image up front (qemu-img resize + growpart /dev/sda 1 + resize2fs) before
Stage A/convert, so the produced verity-rootfs image can be larger than the cached
20G base. Only grows (>= current is a no-op; shrinking a populated fs is unsafe).

image_name (build-cvm dispatch input, default blank): override the published image
name; blank keeps the auto <imgbase>-<boot_format>-<version>-<owner>. Applies to both
gcp and ali Stage C (use a single env, not both, to avoid a name collision).

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: drop the advisory boot smoke test step from build-cvm (#55)

On this builder (no /dev/kvm) it runs under TCG, so the 600s window is truncated
before multi-user (false 'BOOT NOT CONFIRMED' on good images) and it was advisory
(continue-on-error) so it never gated — ~10min/build of noise. Keep
cvm/test/boot-smoke-test.sh for local / KVM runs; revisit as a hard gate once a
KVM-capable runner exists. Real boot + attestation validation is on TDX hardware.

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: move image_name input to the top of the build-cvm dispatch form (#56)

Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: runtime owner claim — unclaimed boot + measured ClaimOwner RPC

- config: owner_address now optional; initial_whitelist removed (use the
  measured add-to-whitelist RPC after claiming)
- server boots UNCLAIMED without an owner; first valid signer of ClaimOwner
  becomes owner, extended as a claim_owner runtime measurement event
- claimed owner persisted under /run (tmpfs) so a process restart within the
  same boot cannot reopen the claim; config/persisted mismatch refuses to start
- legacy baked-in owner still works and is now also measured at startup
- cli: claim-owner command with end-to-end verification; examples/claim_owner.sh

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* build/ci/docs: drop the per-owner image dimension (ownerless claim-at-runtime images)

- build-tapp.sh: OWNER_ADDRESS now optional (legacy); default builds an
  ownerless image whose owner is claimed at runtime as a measured event
- build-cvm.yml: remove the owner input/env and the owner tag from image
  names/family/concurrency; reference values register under the fixed 'any'
  owner slot (refvalues scripts unchanged)
- docs: README claim-owner section, cvm/README dimensions, reference-values
  README, EVIDENCE_AND_AS_VERIFICATION claim_owner reconciliation rule
- version: tapp-server 0.3.0 (new RPC), tapp-cli 0.3.0 (new command)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: ClaimConfig — extend claim to include chain+kbs runtime config

Renames ClaimOwner → ClaimConfig and extends it to carry the full runtime
configuration: chain_rpc_url, chain_contract_address, kbs_node_urls.

- proto: ClaimConfigRequest has chain/kbs fields; both proto copies synced
- server: measurement event is now claim_config and includes all fields;
  kms_client wrapped in Arc<RwLock> so dynamic-mode kbs_node_urls initialise
  the KMS client at claim time (no [kbs] section in config.toml needed)
- establish_owner_at_startup: passes chain/kbs from config.toml into the
  claim_config measurement so pre-baked and dynamic modes produce the same
  event shape — verifiers see the complete runtime config in both modes
- cli: claim-config command with --chain-rpc-url / --chain-contract / --kbs-urls
- examples/claim_owner.sh: updated for ClaimConfig RPC
- e2e: 14/14 with chain+kbs fields in the test claim

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* build: explicit canonical/custom build mode selection

Replaces the implicit OWNER_ADDRESS-present-or-not heuristic with an
explicit BUILD_MODE parameter (canonical | custom).

  canonical (default): owner-agnostic image — no owner baked in, one
    reference set for all owners (refvals under 'any' slot), image name
    has no owner tag. OWNER_ADDRESS is cleared even if accidentally set.

  custom: owner baked into config.toml — per-owner initrd measurement,
    per-owner refval set and image name. Requires OWNER_ADDRESS.

build-tapp.sh: BUILD_MODE env var; custom mode enforces OWNER_ADDRESS.
build-cvm.yml: build_mode input (choice); owner input (custom only);
  names step resolves img_suffix + owner_slot per mode; both CI paths
  (canonical and custom) fully supported.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* build: canonical refvals at <env>.json (no owner subdir/placeholder)

Drop the 'any' placeholder — canonical mode uses a clean path without an
owner dimension:
  canonical: verifier/reference-values/<cloud>/<boot_format>/<version>/<env>.json
             policy id: 0g-tapp-<cloud>-<boot_format>-<version>-<env>
  custom:    verifier/reference-values/<cloud>/<boot_format>/<version>/<env>/<owner>.json
             policy id: 0g-tapp-<cloud>-<boot_format>-<version>-<env>-<owner>

gen-reference-values.sh / register-shared-as.sh / open-refvalues-pr.sh:
  owner arg is now optional; omit for canonical, pass 0x address for custom.
  register-shared-as.sh auto-detects owner vs AS-endpoint for arg 5.

build-cvm.yml: owner_slot replaced by owner_tag (empty = canonical); scripts
  called with bare $OWNER_TAG so empty expands to nothing (not an empty string arg).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: require tapp-server >= v0.3.0 for canonical build mode

ClaimConfig RPC was introduced in 0.3.0; a canonical image built from
an older binary boots unclaimed with no way to claim it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: fail-fast input validation + fix custom-mode owner passing

- validate step moved to the very first step (before toolchain/download):
  canonical requires >= v0.3.0; custom requires an explicit well-formed
  0x owner (vars fallback removed — a silently-defaulted owner baked into
  a measured image is how mistakes ship); image_name+env=both rejected
  (matrix jobs would clobber each other); release binary existence probed
- fix: refvalue steps passed the 0x-stripped owner_tag, but
  register-shared-as.sh detects owner-vs-endpoint by the 0x prefix —
  custom mode would have treated the owner as the AS endpoint. Now the
  raw 0x address is passed everywhere (scripts strip it themselves) and
  the detection regex also accepts unprefixed 40-hex
- OWNER env is empty unless build_mode=custom (canonical steps can never
  see an owner); BUILD_MODE forwarded to build-tapp.sh explicitly
- docs: drop remaining 'any'-slot references; canonical/custom identifier
  tables; claim_config event shape in EVIDENCE_AND_AS_VERIFICATION.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* build: KBS_URLS optional in canonical mode

canonical images can receive kbs_node_urls at runtime via ClaimConfig;
custom mode still requires KBS_URLS to be baked in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: fix KBS_URLS passing (inline quoted assignment) + normalize literal []

- $KBS_ARG expanded from a variable is not parsed as an env assignment by
  the shell — with vars.TAPP_KBS_URLS='[]' the word KBS_URLS=[] became the
  command name ('command not found'). Keep the assignment inline and quoted.
- build-tapp.sh: treat a literal [] as empty (would interpolate into
  node_urls = [[]], broken TOML).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci: fix env passing to build-tapp.sh — no $VAR in assignment-prefix list

bash decides what is an assignment prefix BEFORE expansion: a variable
($EXTRA/$KBS_ARG, even when empty) in the prefix list turns every later
VAR=val word into the command name — the actual cause of both
'KBS_URLS=[]: command not found' failures.

BUILD_MODE/KBS_URLS are already inherited from the job env; custom mode
exports OWNER_ADDRESS explicitly. Verified locally with a simulated step
for both modes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* skill: update 0g-tapp-cli v1.4.0 — claim-config, unclaimed state, new policy id format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* cvm: RuntimeDirectoryPreserve=yes — keep claimed_owner across process restarts

systemd removes RuntimeDirectory on stop by default; restart = stop+start,
so /run/tapp/claimed_owner was wiped on every systemctl restart, allowing
re-claim. Preserve=yes keeps the dir across restarts but still clears it on
VM reboot (tmpfs) — matching the RTMR lifetime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: get-tapp-info shows runtime chain + kbs from ClaimConfig

- proto: add ChainConfigInfo message and chain field to TappConfigInfo
- server: ClaimedRuntimeConfig stores chain/kbs set at claim time;
  get_tapp_info reads runtime values, falls back to static config
- cli: display Chain and KBS sections in get-tapp-info output
- e2e: 14/14 still passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Wei <wei@0g.ai>

* chore(verifier): reference values for gcp/grub/v0.2.0/dev/ea695c312ce119de347425b29aff85371c9d1837 (#57)

Co-authored-by: cvm-ci <cvm-ci@0g.ai>

* feat(verify-app): claim_config owner reconcile + no-policy boot-chain output

Squash of the feat/runtime-owner-claim commits that missed the #58 merge:

- verify-app chain mode reconciles the claim_config event owner vs the
  on-chain app owner → owner✓/✗/? in the reconcile line
- without --policy-ids, both modes print boot-chain component digests in
  reference-value JSON (same selection rules as policy.rego: shim/grub/uki
  from EV_EFI_BOOT_SERVICES_APPLICATION device paths, kernel/initrd/cmdline
  from EV_IPL strings) — directly diffable against the refvalue json
- fix: claim-config always overwrites kms_client when kbs_node_urls given
  (empty [kbs] node_urls=[] stub no longer blocks dynamic KMS init)
- get-tapp-info shows runtime chain+kbs set via ClaimConfig
- docs/skill: two-axis verify model (chain=dynamic refs, policy=static refs)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(verifier): reference values gcp uki v0.3.0 dev (#65)

Co-authored-by: cvm-ci <cvm-ci@0g.ai>

* fix(cvm): ali image poll — DescribeImages defaults to Available-only

A just-imported image (Creating/Waiting) is invisible to the default
DescribeImages filter, so the first poll read the empty result as 'Gone'
and failed the publish instantly. Query all lifecycle states explicitly;
treat not-yet-listed as Pending (keep polling), fail only on
CreateFailed/UnAvailable. Same fix for the OVERWRITE pre-check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cvm): ali images need HWE kernel — TDX RTMR extend requires >=6.16

The noble GA generic kernel (6.8) lacks the TDX runtime-measurement
interface (sysfs measurements/rtmrN:sha384 + EXTEND_RTMR ioctl, mainlined
in 6.16), so every extend_runtime_measurement on the Ali image failed
(claim_config/start_app not measured). Install linux-image-generic-hwe-24.04
(currently 6.17, same generation as the gcp kernel), point the /boot
symlinks at it, and purge the 6.8 kernel (no alternate boot path, smaller
image).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cvm): ali kernel — pick newest generic WITH modules-extra, not the HWE meta

The HWE meta raced ahead of the archive: it pulled linux-image-7.0.0-28-generic
whose linux-modules-extra (needed by cryptpilot-convert for zram) is not
published, failing the build. Select the newest versioned generic kernel that
has its modules-extra available (currently the 6.17 line), install both
explicitly, and require >=6.16 (TDX RTMR extend).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(verifier): reference values ali uki v0.3.0 dev (#68)

Co-authored-by: cvm-ci <cvm-ci@0g.ai>

* fix(server): measure_only resolved ${VAR} image refs literally — empty hashes on-chain

compose_service_images parsed the RAW compose YAML, so interpolated refs
(image: ${REGISTRY_PREFIX:-…}/foo) stayed literal, docker inspect failed,
and the empty digest was SILENTLY registered on-chain by
start-app --register-onchain (update-onchain later 'fixed' it because the
real start path reads digests from running containers, where compose did
the interpolation).

- resolve service→image via `docker compose config --format json` in the
  staged dir (full interpolation incl. the staged .env)
- fail the measure task loudly if any pulled service still has no digest
  (never register an empty image hash again)

Verified live on 127.0.0.1:50099: image ${TEST_IMG:-nginx:alpine} →
sha256:4a73073b…; volumes hash unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(tapp-server): bump version to 0.3.1

PATCH release: measure_only ${VAR} image-ref interpolation fix (empty
imageHashes registered on-chain by start-app --register-onchain).
tapp-cli unchanged (stays 0.3.0). Skill troubleshooting updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Revert "chore(tapp-server): bump version to 0.3.1"

This reverts commit e85419fabf977d65c0ebf5d936d2471033bd5e2c.

* chore(tapp-server): bump version to 0.3.1

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: keep tapp-server at 0.3.0 (fold measure fix into the 0.3.0 release)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Wei <wei@0g.ai>
Co-authored-by: cvm-ci <cvm-ci@0g.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant