A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
72
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 263 lines 14 kB view raw view rendered
1# CLAUDE.md 2 3This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 5## Project Overview 6 7ATCR (ATProto Container Registry) is an OCI-compliant container registry that uses the AT Protocol for manifest storage and S3 for blob storage. Manifests are stored in users' Personal Data Servers (PDS) while layers are stored in S3. 8 9## Go Workspace 10 11The project uses a Go workspace (`go.work`) with two modules: 12- `atcr.io` — Main module (appview, hold, credential-helper, oauth-helper) 13- `atcr.io/scanner` — Scanner module (separate to isolate heavy Syft/Grype dependencies) 14 15## Build Commands 16 17Always build into the `bin/` directory (`-o bin/...`), not the project root. 18 19```bash 20# Build main binaries 21go build -o bin/atcr-appview ./cmd/appview 22go build -o bin/atcr-hold ./cmd/hold 23go build -o bin/docker-credential-atcr ./cmd/credential-helper 24go build -o bin/oauth-helper ./cmd/oauth-helper 25 26# Build scanner (separate module) 27cd scanner && go build -o ../bin/atcr-scanner ./cmd/scanner && cd .. 28 29# Build hold with billing support (optional build tag) 30go build -tags billing -o bin/atcr-hold ./cmd/hold 31 32# Tests 33go test ./... # all tests 34go test ./pkg/atproto/... # specific package 35go test -run TestManifestStore ./pkg/atproto/... # specific test 36go test -race ./... # race detector 37 38# Docker 39docker build -f Dockerfile.appview -t atcr.io/appview:latest . 40docker build -f Dockerfile.hold -t atcr.io/hold:latest . 41docker build -f Dockerfile.scanner -t atcr.io/scanner:latest . 42docker-compose up -d 43 44# Generate & run with config 45./bin/atcr-appview config init config-appview.yaml 46./bin/atcr-hold config init config-hold.yaml 47./bin/atcr-appview serve --config config-appview.yaml 48./bin/atcr-hold serve --config config-hold.yaml 49 50# Scanner (env vars only, no YAML) 51SCANNER_HOLD_URL=ws://localhost:8080 SCANNER_SHARED_SECRET=secret ./bin/atcr-scanner serve 52 53# Usage report 54go run ./cmd/usage-report --hold https://hold01.atcr.io 55go run ./cmd/usage-report --hold https://hold01.atcr.io --from-manifests 56 57# Utilities 58go run ./cmd/db-migrate --help # SQLite → libsql migration 59go run ./cmd/record-query --help # Query ATProto relay by collection 60go run ./cmd/s3-test # S3 connectivity test 61go run ./cmd/healthcheck <url> # HTTP health check (for Docker) 62``` 63 64## Architecture Overview 65 66ATCR uses **distribution/distribution** as a library, extending it via middleware to route content to different backends: 67 68- **Manifests** → ATProto PDS (small JSON, stored as `io.atcr.manifest` records) 69- **Blobs/Layers** → S3 via hold service (presigned URLs for direct client-to-S3 transfers) 70- **Authentication** → ATProto OAuth with DPoP + Docker credential helpers 71 72### Four Components 73 741. **AppView** (`cmd/appview`) — OCI Distribution API server. Resolves identities, routes manifests to PDS, routes blobs to hold service, validates OAuth, issues registry JWTs. Includes web UI for browsing. 752. **Hold Service** (`cmd/hold`) — BYOS blob storage. Embedded PDS with captain/crew/stats/scan records (all ATProto records in CAR store), S3-compatible storage, presigned URLs. Supports did:web (default) or did:plc identity with auto-recovery. Optional subsystems: admin UI, quotas, billing (Stripe), GC, scan dispatch, Bluesky status posts. 763. **Scanner** (`scanner/cmd/scanner`) — Vulnerability scanning. Connects to hold via WebSocket, generates SBOMs (Syft), scans vulnerabilities (Grype). Priority queue with tier-based scheduling. 774. **Credential Helper** (`cmd/credential-helper`) — Docker credential helper implementing ATProto OAuth flow, exchanges OAuth token for registry JWT. 78 79### Request Flow Summary 80 81**Push:** Client pushes to `atcr.io/<identity>/<image>:<tag>`. Registry middleware resolves identity → DID → PDS, discovers hold DID (from sailor profile `defaultHold` → legacy `io.atcr.hold` records → AppView default). Blobs go to hold via XRPC multipart upload (presigned S3 URLs). Manifests stored in user's PDS as `io.atcr.manifest` records with `holdDid` reference. 82 83**Pull:** AppView fetches manifest from user's PDS. The manifest's `holdDid` field tells where blobs were stored. Blobs fetched from that hold via presigned download URLs. Pull always uses the historical hold from the manifest, even if the user changed their default since pushing. 84 85**Hold discovery priority** (in `findHoldDID()`, `pkg/appview/middleware/registry.go`): 861. Sailor profile's `defaultHold` (user preference) 872. User's `io.atcr.hold` records (legacy) 883. AppView's `default_hold_did` (fallback) 89 90### Name Resolution 91 92Pattern: `atcr.io/<identity>/<image>:<tag>` where identity is a handle or DID. 93 94Resolution in `pkg/atproto/resolver.go`: Handle → DID (DNS/HTTPS) → PDS endpoint (DID document). 95 96### Nautical Terminology 97 98- **Sailors** = registry users, **Captains** = hold owners, **Crew** = hold members 99- **Holds** = storage endpoints (BYOS), **Quartermaster/Bosun/Deckhand** = crew tiers 100 101### Hold Embedded PDS Records 102 103The hold's embedded PDS stores all operational data as ATProto records in a CAR store (not SQLite). SQLite holds only the records index and events. 104 105| Collection | Cardinality | Description | 106|---|---|---| 107| `io.atcr.hold.captain` | Singleton | Hold identity, owner DID, settings | 108| `io.atcr.hold.crew` | Per-member | Crew membership + permissions | 109| `io.atcr.hold.layer` | Per-layer | Layer metadata (digest, size, media type) | 110| `io.atcr.hold.stats` | Per-repo | Push/pull counts per owner+repository | 111| `io.atcr.hold.scan` | Per-scan | Vulnerability scan results | 112| `io.atcr.hold.image.config` | Per-manifest | OCI image config (history, env, entrypoint, labels) | 113| `app.bsky.feed.post` | Status posts | Online/offline status, push notifications | 114| `sh.tangled.actor.profile` | Singleton | Hold profile (name, description, avatar) | 115 116## Authentication 117 118Three token types flow through the system: 119 120| Token | Issued By | Used For | Lifetime | 121|-------|-----------|----------|----------| 122| OAuth (access+refresh) | User's PDS | AppView → PDS communication | ~2h / ~90d | 123| Registry JWT | AppView | Docker client → AppView | 5 min | 124| Service Token | User's PDS | AppView → Hold service | 60s (cached 50s) | 125 126``` 127Docker Client ──Registry JWT──→ AppView ──OAuth──→ User's PDS ──Service Token──→ Hold 128``` 129 130The credential helper never manages OAuth tokens directly — AppView owns the OAuth session and issues registry JWTs. See `docs/OAUTH.md` for full OAuth/DPoP implementation details. 131 132## Hold Authorization 133 134- **Public hold**: Anonymous reads allowed. Writes require captain or crew with `blob:write`. 135- **Private hold**: Reads require crew with `blob:read` or `blob:write`. Writes require `blob:write`. 136- `blob:write` implicitly grants `blob:read`. 137- Captain has all permissions implicitly. 138- See `docs/BYOS.md` for full authorization model and permission matrix. 139 140## Key File Locations 141 142| Responsibility | Files | 143|---|---| 144| ATProto records & collections | `pkg/atproto/lexicon.go` | 145| DID/handle resolution | `pkg/atproto/resolver.go` | 146| PDS client (XRPC) | `pkg/atproto/client.go` | 147| Manifest ↔ ATProto storage | `pkg/atproto/manifest_store.go` | 148| Sailor profiles | `pkg/atproto/profile.go` | 149| Registry middleware (identity resolution, hold discovery) | `pkg/appview/middleware/registry.go` | 150| Auth middleware (JWT validation) | `pkg/appview/middleware/auth.go` | 151| Content routing (manifests vs blobs) | `pkg/appview/storage/routing_repository.go` | 152| Blob proxy to hold (presigned URLs) | `pkg/appview/storage/proxy_blob_store.go` | 153| Request context struct | `pkg/appview/storage/context.go` | 154| Database queries | `pkg/appview/db/queries.go` | 155| Database schema | `pkg/appview/db/schema.sql` | 156| OAuth client & session refresher | `pkg/auth/oauth/client.go` | 157| OAuth P-256 key management | `pkg/auth/oauth/keys.go` | 158| Hold PDS endpoints & auth | `pkg/hold/pds/xrpc.go`, `pkg/hold/pds/auth.go` | 159| Hold DID management (did:web, did:plc, PLC recovery) | `pkg/hold/pds/did.go` | 160| Hold captain records | `pkg/hold/pds/captain.go` | 161| Hold crew management | `pkg/hold/pds/crew.go` | 162| Hold push/pull stats (ATProto records in CAR store) | `pkg/hold/pds/stats.go` | 163| Hold layer records | `pkg/hold/pds/layer.go` | 164| Hold scan records & scanner integration | `pkg/hold/pds/scan.go`, `pkg/hold/pds/scan_broadcaster.go` | 165| Hold Bluesky status posts | `pkg/hold/pds/status.go` | 166| Hold OCI upload endpoints | `pkg/hold/oci/xrpc.go` | 167| Hold config | `pkg/hold/config.go` | 168| AppView config | `pkg/appview/config.go` | 169| Config marshaling (commented YAML) | `pkg/config/marshal.go` | 170| Scanner config (env-only) | `scanner/internal/config/config.go` | 171 172## Configuration 173 174ATCR uses **Viper** for config. YAML primary, env vars override. Generate defaults with `config init`. 175 176**Env var convention:** Prefix + YAML path with `_` separators: 177- AppView: `ATCR_` (e.g., `ATCR_SERVER_DEFAULT_HOLD_DID`) 178- Hold: `HOLD_` (e.g., `HOLD_SERVER_PUBLIC_URL`) 179- S3: standard AWS names (`AWS_ACCESS_KEY_ID`, `S3_BUCKET`, `S3_ENDPOINT`) 180- Scanner: `SCANNER_` prefix (env-only, no Viper) 181 182See `config-appview.example.yaml` and `config-hold.example.yaml` for all options. Config structs use `comment` struct tags for auto-generating commented YAML via `MarshalCommentedYAML()` in `pkg/config/marshal.go`. 183 184## Development Gotchas 185 186- **Do NOT run `npm run css:build` or `npm run js:build` manually** — Air handles these on file change 187- **Do NOT edit `icons.svg` directly** — SVG icon sprite sheets (`pkg/appview/public/icons.svg`, `pkg/hold/admin/public/icons.svg`) are auto-generated from template icon references during build. Just reference icons by name in templates and the build will include them. 188- **RoutingRepository is created fresh on EVERY request** (no caching). Previous caching caused stale OAuth sessions and "invalid refresh token" errors. The OAuth refresher caches efficiently already (in-memory + DB). 189- **Storage driver import**: `_ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws"` — blank import required 190- **Hold DID lookups use database** (`manifests` table), not in-memory cache — persistent across restarts 191- **Context keys** (`auth.method`, `puller.did`) exist because `Repository()` receives `context.Context` from the distribution library interface — context values are the only way to pass data from HTTP middleware into the distribution middleware layer. Both are copied into `RegistryContext` inside `Repository()`. 192- **OAuth key types**: AppView uses P-256 (ES256) for OAuth, not K-256 like PDS keys 193- **Confidential vs public clients**: Production uses P-256 key at `/var/lib/atcr/oauth/client.key` (auto-generated); localhost is always public client 194- **Hold stats are ATProto records in CAR store** — `io.atcr.hold.stats` records are stored via `repomgr.PutRecord()`, not in SQLite. Lost if CAR store is lost without backup. 195- **PLC auto-update on boot** — When using did:plc, `LoadOrCreateDID()` calls `EnsurePLCCurrent()` every startup. If local signing key or URL doesn't match plc.directory, it auto-updates (requires rotation key on disk). 196- **Hold CAR store is the source of truth** — Captain, crew, layer, stats, scan records, Bluesky posts, profiles are all ATProto records in the CAR store. SQLite holds only the records index and events. 197 198## Common Tasks 199 200**Adding a new ATProto record type:** 2011. Define schema in `pkg/atproto/lexicon.go` 2022. Add collection constant (e.g., `MyCollection = "io.atcr.my-type"`) 2033. Add constructor function (e.g., `NewMyRecord()`) 2044. Update client methods if needed 205 206**Modifying storage routing:** 2071. Edit `pkg/appview/storage/routing_repository.go` 2082. Update `Blobs()` or `Manifests()` method 2093. Context passed via `RegistryContext` struct (`pkg/appview/storage/context.go`) 210 211**Changing name resolution:** 2121. Modify `pkg/atproto/resolver.go` for DID/handle resolution 2132. Update `pkg/appview/middleware/registry.go` if changing routing 2143. `findHoldDID()` checks: sailor profile → `io.atcr.hold` records (legacy) → default hold DID 215 216**Working with OAuth client:** 217- Self-contained: pass `baseURL`, handles client ID/redirect URI/scopes 218- Standard callback path: `/auth/oauth/callback` (all ATCR components) 219- See `pkg/auth/oauth/client.go` for `NewClientApp()`, refresher setup 220 221**Adding BYOS support for a user:** 2221. User configures hold YAML (storage credentials, public URL, owner DID) 2232. User runs hold service — creates captain + crew records in embedded PDS 2243. User sets sailor profile `defaultHold` to their hold's DID 2254. AppView automatically routes blobs to user's storage — no AppView changes needed 226 227**Working with the database:** 228- **Base schema**: `pkg/appview/db/schema.sql` — source of truth for fresh installs 229- **Migrations**: `pkg/appview/db/migrations/*.yaml` — only for ALTER/UPDATE/DELETE on existing DBs 230- **Adding new tables**: Add to `schema.sql` only (no migration needed) 231- **Altering tables**: Create migration AND update `schema.sql` to keep them in sync 232 233**Hold DID recovery/migration (did:plc):** 2341. Back up `rotation.key` and DID string (from `did.txt` or plc.directory) 2352. Set `database.did_method: plc` and `database.did: "did:plc:..."` in config 2363. Provide `rotation_key` (multibase K-256 private key) — signing key auto-generates if missing 2374. On boot: `LoadOrCreateDID()` adopts the DID, `EnsurePLCCurrent()` auto-updates PLC directory if keys/URL changed 2385. Without rotation key: hold boots but logs warning about PLC mismatch 239 240**Adding web UI features:** 241- Add handler in `pkg/appview/handlers/` 242- Register route in `pkg/appview/routes/routes.go` 243- Create template in `pkg/appview/templates/pages/` 244 245## Testing Strategy 246 247- Mock ATProto client for manifest operations 248- Mock S3 driver for blob operations 249- Test name resolution independently 250- Integration tests require real PDS + S3 251 252## Documentation References 253 254- **BYOS Architecture**: `docs/BYOS.md` 255- **OAuth Implementation**: `docs/OAUTH.md` 256- **Hold Service**: `docs/hold.md` 257- **AppView**: `docs/appview.md` 258- **Hold XRPC Endpoints**: `docs/HOLD_XRPC_ENDPOINTS.md` 259- **Development Guide**: `docs/DEVELOPMENT.md` 260- **Billing/Quotas**: `docs/BILLING.md`, `docs/QUOTAS.md` 261- **Scanning**: `docs/SBOM_SCANNING.md` 262- **ATProto Spec**: https://atproto.com/specs/oauth 263- **OCI Distribution Spec**: https://github.com/opencontainers/distribution-spec