Skip to content

feat: fetch schemas from /protocol/{version}.tgz bundle#41

Merged
bokelley merged 4 commits intomainfrom
bokelley/issue-40
Apr 18, 2026
Merged

feat: fetch schemas from /protocol/{version}.tgz bundle#41
bokelley merged 4 commits intomainfrom
bokelley/issue-40

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented Apr 17, 2026

Summary

Closes #40. Replaces the per-file GitHub API schema sync in adcp/schemas/download.sh with a single tarball fetch from https://adcontextprotocol.org/protocol/{version}.tgz, SHA-256 verified against the co-located sidecar, with optional Sigstore signature verification on top.

  • ~250 gh api calls → 2 CDN GETs. Schema sync goes from ~30-60s to ~1-2s.
  • No more GH_TOKEN required by CI — the bundle is served publicly. Dropped the env var from the CI step.
  • check-freshness.sh ported to the /protocol/ manifest. For VERSION=latest, compares the pinned bundle's SHA-256 against the upstream sidecar; for pinned releases, diffs the highest released version in the manifest (proper semver sort, not lex).
  • Optional Sigstore verification via feat(security): sign protocol tarballs with Sigstore (cosign keyless) adcp#2273. When .sig/.crt sidecars are present and cosign is on PATH, verify against the upstream release workflow identity (.github/workflows/release.yml) and OIDC issuer. Falls back to checksum-only trust when sidecars are missing (expected for latest.tgz and pre-signing releases). Set ADCP_STRICT_VERIFY=1 to fail closed when sidecars or cosign are unavailable.
  • Added a CI drift check for adcp/types_gen.go — the existing CI only diffed tmproto/types_gen.go, so an upstream schema change that altered adcp/types_gen.go could merge green with stale generated code.
  • VERSION moved from a commit SHA (12c09e0...) to latest — only latest is currently published at the protocol manifest endpoint. Once released versions land we can pin to them.

Safety hardening (from code + security review)

  • VERSION validated against ^(latest|\d+\.\d+\.\d+(-[A-Za-z0-9.]+)?)$ before it's interpolated into URLs, rsync source paths, or the VERSION file.
  • Tarball inspected for /-prefixed or ..-containing entries before extraction.
  • Post-extract symlink rejection (sha256 of the bundle proves it matches upstream but attests nothing about the contents).
  • rsync --delete source path existence check so a missing/renamed bundle layout fails loudly instead of wiping the schema tree.
  • curl --retry 3 --retry-all-errors --max-time 60 on every fetch.
  • Protected-file list consolidated into a single PROTECTED=(...) array at the top of download.sh so the exclude-list maintenance burden is obvious.

Trust model

Current latest pin relies on sha256 + TLS (same-origin — verifies transport, not supply chain). Once the repo pins to a released version (3.1.0+), cosign verification proves the bundle came from the upstream release workflow, defending against host compromise.

Test plan

  • ./download.sh succeeds with VERSION=latest, sha256 matches, scripts survive rsync
  • ./download.sh '../evil' and ./download.sh 'foo; rm -rf /' rejected by validation
  • Unsigned latest.tgz path reports "checksum-only trust" and succeeds
  • ADCP_STRICT_VERIFY=1 ./download.sh fails closed when sidecars absent
  • ./check-freshness.sh reports up-to-date after fresh download
  • go build ./... passes
  • go test ./adcp/... passes
  • CI green on PR

🤖 Generated with Claude Code

bokelley and others added 4 commits April 17, 2026 11:26
Replaces ~250 per-file gh api calls with a single CDN fetch + SHA-256
verify. Drops the GH_TOKEN requirement from CI and cuts schema-sync
wall time by 20-50x. check-freshness.sh now reads the /protocol/
manifest instead of the GitHub API.

Validates VERSION against a semver/latest regex, rejects bundles with
path-traversal entries or symlinks, and adds a CI drift check for
adcp/types_gen.go.

Closes #40

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up adcontextprotocol/adcp#2273 — released tarballs now ship
cosign keyless signatures. When .sig/.crt sidecars are present and
cosign is installed, verify against the upstream release workflow
identity. Falls back to checksum-only trust when sidecars are missing
(latest.tgz, pre-signing releases).

Set ADCP_STRICT_VERIFY=1 to require signatures (fails closed when
sidecars are missing or cosign is unavailable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fully wires the consumer side of adcontextprotocol/adcp#2273:
download.sh now reads signature_verification metadata from the
/protocol/ manifest rather than hardcoding the Sigstore identity,
and cosign is installed in CI. Released versions (non-"latest")
MUST be signed — the script fails closed when sidecars or cosign
are missing. latest.tgz remains unsigned by design and falls back
to checksum-only trust.

Migrates consumers to the new IdentityMatchRequest.identities[]
shape introduced upstream: user_token and uid_type moved into an
IdentityToken array (maxItems 3 to match TMPX budget). Touches
router provider filter, targeting engine primary-token selection,
tmpclient wire construction, reference identity agent, and every
test call site. targeting/valkeystore also picks up the earlier
tmproto.ExposeRequest -> targeting.ExposeRequest move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Hardcode cosign identity regex and OIDC issuer in download.sh
  rather than sourcing from the /protocol/ manifest. Manifest-sourced
  identity is equivalent-trust to origin TLS — an attacker with write
  access to the origin could swap the regex to match any cert they
  control. In-tree pinning requires attacker to also compromise the
  upstream GitHub workflow.
- Validate signature certificate is actually PEM before handing to
  cosign (catches CDN returning 200 with an HTML error body).
- rm partial sidecar files on non-200 response codes.
- Cap IdentityToken.UserToken length at MaxIDLength (256) to bound
  request memory; prior validation applied this to other IDs.
- Document Identities[0] primary-token behavior on EvaluateIdentity.
- identity-agent now calls ValidateIdentityRequest rather than only
  checking len(Identities) == 0, so the full invariant set (max 3,
  uid_type non-empty, country format, package limits) is enforced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 48543a3 into main Apr 18, 2026
5 checks passed
@bokelley bokelley deleted the bokelley/issue-40 branch April 18, 2026 22:15
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.

Use /protocol/{version}.tgz for schema sync instead of per-file GitHub API downloads

1 participant