Skip to content

Conversation

@hiroTamada
Copy link
Contributor

@hiroTamada hiroTamada commented Jan 26, 2026

Summary

Implements a two-tier caching system for builds that separates global (shared) cache from tenant-specific cache, with fine-grained JWT token permissions for secure access control.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                     Global Cache                            │
│                cache/global/{key}                           │
│         (populated by admin builds only)                    │
└─────────────────────────────────────────────────────────────┘
                           │
              ┌────────────┼────────────┐
              │ pull       │ pull       │ pull
              ▼            ▼            ▼
       ┌──────────┐  ┌──────────┐  ┌──────────┐
       │ Tenant A │  │ Tenant B │  │ Tenant C │
       │  Cache   │  │  Cache   │  │  Cache   │
       └──────────┘  └──────────┘  └──────────┘

Changes

  • Token Structure: New RepoPermission struct with per-repository scope (push/pull)
  • Build Request: New is_admin_build and global_cache_key fields
  • Manager: Generates tokens with fine-grained repo access based on build type
  • Builder Agent: Configures BuildKit with appropriate --import-cache and --export-cache flags
  • Registry Middleware: Validates per-repo scopes from JWT tokens
  • API Layer: Parses new multipart form fields

Build Flow

Build Type Import From Export To
Admin (is_admin_build=true) cache/global/{key} cache/global/{key}
Regular tenant cache/global/{key} + cache/{tenant} cache/{tenant}

Usage Examples

# Regular tenant build - imports from global "node" cache, exports to tenant cache
curl -X POST -F "source=@source.tar.gz" \
  -F "global_cache_key=node" \
  -F "cache_scope=my-tenant" \
  http://localhost:8083/builds

# Admin build - populates global "ubuntu" cache
curl -X POST -F "source=@source.tar.gz" \
  -F "global_cache_key=ubuntu" \
  -F "is_admin_build=true" \
  http://localhost:8083/builds

Test Plan

  • Unit tests for token generation with per-repo permissions
  • Unit tests for IsRepositoryAllowed, GetRepoScope, IsPushAllowedForRepo, IsPullAllowedForRepo
  • E2E test: Regular tenant build imports from global cache, exports to tenant cache
  • E2E test: Admin build exports to global cache
  • E2E test: Subsequent builds hit global cache (CACHED steps)
  • Backward compatibility with legacy token format

Note

Introduces a two-tier BuildKit cache (global + tenant) and fine-grained registry JWT scopes, with API and builder integration.

  • Builder agent: adds is_admin_build and global_cache_key to config; configures BuildKit to import from cache/global/{key} and tenant cache/{scope} (regular builds), and exports cache to cache/global/{key} for admin builds or cache/{scope} for regular builds (lib/builds/builder_agent/main.go).
  • Token model: new per-repo permissions via RepoPermission and GenerateToken; extended claims and helpers (IsRepositoryAllowed, GetRepoScope, per-repo push/pull checks). Legacy token format retained (lib/builds/registry_token.go, tests added in lib/builds/registry_token_test.go).
  • Manager: generates tokens with repo-specific scopes; writes IsAdminBuild/GlobalCacheKey into builder config; refresh path updated similarly (lib/builds/manager.go).
  • API: parses is_admin_build and global_cache_key multipart fields and passes them to domain request (cmd/api/api/builds.go).
  • OpenAPI: documents new multipart fields and cache behavior (openapi.yaml).

Written by Cursor Bugbot for commit 044a6ba. This will update automatically on new commits. Configure here.

Adds a two-tier caching system for builds that separates global (shared)
cache from tenant-specific cache, with fine-grained JWT token permissions.

Architecture:
- Global cache (cache/global/{runtime}): Shared read-only cache populated
  by admin builds, available to all tenants
- Tenant cache (cache/{tenant}): Tenant-specific cache with push access

Token changes:
- New RepoPermission struct with per-repository scope (push/pull)
- GenerateToken() creates tokens with fine-grained repo access
- Backward compatible with legacy token format

Build flow:
- Regular builds: import from global + tenant cache, export to tenant only
- Admin builds: import from global cache, export to global cache

API changes:
- New is_admin_build field (operator-only, enables global cache push)
- New global_cache_runtime field (e.g., "node", "python", "go")
@github-actions
Copy link

github-actions bot commented Jan 26, 2026

✱ Stainless preview builds

This PR will update the hypeman SDKs with the following commit message.

feat(builds): implement two-tier build cache with per-repo token scopes
⚠️ hypeman-typescript studio · code

There was a regression in your SDK.
generate ⚠️build ✅lint ✅test ✅

npm install https://pkg.stainless.com/s/hypeman-typescript/53c9b248c245c1cb5f1e0dae46a1448de430c283/dist.tar.gz
⚠️ hypeman-go studio · code

There was a regression in your SDK.
generate ⚠️lint ✅test ✅

go get github.com/stainless-sdks/hypeman-go@0e29d03d94cf50a0d0e83c323f7ed9f2e15f3e61
hypeman-cli studio

Code was not generated because there was a fatal error.


This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-01-27 20:18:38 UTC

The "runtime" term was too specific. The global cache can be used for
any shared cache category like base OS images, browser VMs, or runtimes.

Renamed across all files:
- GlobalCacheRuntime -> GlobalCacheKey
- global_cache_runtime -> global_cache_key
@hiroTamada hiroTamada requested review from rgarcia and sjmiller609 and removed request for rgarcia January 26, 2026 22:38
Message: "failed to read is_admin_build field",
}, nil
}
isAdminBuild = string(data) == "true" || string(data) == "1"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing authorization check for admin build privilege

High Severity

The is_admin_build parameter is accepted from any authenticated user without authorization verification. The OpenAPI description claims this is "operator-only," but no enforcement exists. Any authenticated user can set is_admin_build=true to gain push access to the global cache (cache/global/{key}), potentially poisoning shared cache content that all tenant builds consume.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but the endpoiont itself is protected by api key. I think this is fine for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the point tho

The previous regex `^/v2/([^/]+(?:/[^/]+)?)/` only captured up to 2
path segments, but global cache repos have 3 segments (cache/global/{key}).

Changed to `^/v2/(.+?)/(manifests|blobs)/` which uses non-greedy matching
to correctly extract the repo path before /manifests/ or /blobs/.

Fixes: cache/global/{key} paths were being extracted as cache/global,
causing auth failures since tokens grant access to the full 3-segment path.
@hiroTamada hiroTamada requested a review from rgarcia January 26, 2026 23:07
Comment on lines +17 to +18
// Scope is the access scope for this repo: "pull" for read-only, "push" for read+write
Scope string `json:"scope"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we make scope like this

  "scope": "admin:*",

to mean it can push to any

and like this for org only

  "scope": "org:*",

// GenerateToken creates a short-lived token with per-repository access permissions.
// This supports the two-tier cache model where different repos can have different scopes.
// For example: pull on cache/global/*, push on cache/{tenant}
func (g *RegistryTokenGenerator) GenerateToken(buildID string, repoAccess []RepoPermission, ttl time.Duration) (string, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice to generate admin push token from "gen-jwt" script so when users run "hypeman-token" on the server where it's installed, they are getting an admin token that can push (would help me on something I'm working with)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, there are some follow ups on kernel repo side on the token control. I will keep it as a follow up for now

Comment on lines 20 to 23
// registryPathPattern matches /v2/{repository}/(manifests|blobs)/... paths
// Supports 1-3 segment repos: builds/id, cache/tenant, cache/global/key
// Uses non-greedy match to capture repo path before /manifests/ or /blobs/
var registryPathPattern = regexp.MustCompile(`^/v2/(.+?)/(manifests|blobs)/`)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just fixed a bug that looks like this, maybe should work like this #71

maybe this regex misses specify by sha for example

Comment on lines +2318 to +2322
is_admin_build:
type: string
description: |
Set to "true" to grant push access to global cache (operator-only).
Admin builds can populate the shared global cache that all tenant builds read from.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we add the admin claim like "scope": "admin:*", then maybe we can just delete this parameter?

Resolved conflicts by accepting main's changes:
- Use docker/distribution router for path extraction
- Simplified token format (removed per-repo scopes)
@hiroTamada hiroTamada merged commit c539d3c into main Jan 27, 2026
3 of 4 checks passed
@hiroTamada hiroTamada deleted the feat/two-tier-build-cache branch January 27, 2026 20:14
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.


token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(g.secret)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Middleware missing RepoAccess field breaks new token format

High Severity

The new GenerateToken function creates JWT tokens that only populate the RepoAccess field, leaving Repositories and Scope empty. However, the registry middleware in lib/middleware/oapi_auth.go has a separate RegistryTokenClaims struct (lines 27-32) that lacks the RepoAccess field. When the middleware's validateRegistryToken function parses new-format tokens, claims.Repositories is empty, causing the check len(claims.Repositories) == 0 at line 250 to fail with "not a registry token". This breaks authentication for all builds using the new token format.

Fix in Cursor Fix in Web

hiroTamada added a commit that referenced this pull request Jan 27, 2026
The two-tier build cache PR (#70) introduced a new token format using
`repo_access` field with per-repository scopes, but the middleware
wasn't updated to parse it.

This caused 401 Unauthorized errors when builder VMs tried to push
images to the registry, as the middleware only checked for the legacy
`repos` field which is empty in new tokens.

Changes:
- Add RepoPermission struct and RepoAccess field to RegistryTokenClaims
- Update validateRegistryToken to check both RepoAccess (new) and
  Repositories (legacy) formats
- Add per-repo scope checking for write operations
- Add comprehensive tests for both token formats

Fixes build failures in production where the new token format was
being used but not recognized by the registry auth middleware.
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.

3 participants