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.

Credential Helper Rewrite#

Context#

The current credential helper (cmd/credential-helper/main.go, ~1070 lines) is a monolithic single-file binary with a manual switch dispatch. It has no help text, hangs silently when run without stdin, embeds interactive device auth inside the Docker protocol get command (blocking pushes for up to 2 minutes while polling), and only supports one account per registry. Users want multi-account support (e.g., evan.jarrett.net and michelle.jarrett.net on the same atcr.io) and multi-registry support (e.g., atcr.io + buoy.cr).

Approach#

Rewrite using Cobra (already a project dependency) for the CLI framework and charmbracelet/huh for interactive prompts (select menus, confirmations, spinners). Separate Docker protocol commands (machine-readable, hidden) from user-facing commands (interactive, discoverable). Model after gh auth UX patterns.

Smart account auto-detection: The get command inspects the parent process command line (/proc/<ppid>/cmdline on Linux, ps on macOS) to determine which image Docker is pushing/pulling. Since ATCR URLs are host/<identity>/repo:tag, we can extract the identity and auto-select the matching account — no prompts, no manual switching needed in the common case.

Command Tree#

docker-credential-atcr
  ├── get              (Docker protocol — stdin/stdout, hidden, smart account detection)
  ├── store            (Docker protocol — stdin, hidden)
  ├── erase            (Docker protocol — stdin, hidden)
  ├── list             (Docker protocol extension, hidden)
  ├── login            (Interactive device flow with huh prompts)
  ├── logout           (Remove account credentials)
  ├── status           (Show all accounts with active indicators)
  ├── switch           (Switch active account — auto-toggle for 2, select for 3+)
  ├── configure-docker (Auto-edit ~/.docker/config.json credHelpers)
  ├── update           (Self-update, existing logic preserved)
  └── version          (Built-in via cobra)

Smart Account Resolution (get command)#

The get command resolves which account to use with this priority chain — fully non-interactive:

1. Parse parent process cmdline → extract identity from image ref
   docker push atcr.io/evan.jarrett.net/test:latest
   → parent cmdline contains "evan.jarrett.net" → use that account

2. Fall back to active account (set by `switch` command)

3. Fall back to sole account (if only one exists for this registry)

4. Error with helpful message:
   "Multiple accounts for atcr.io. Run: docker-credential-atcr switch"

Parent process detection (in helpers.go):

  • Linux: read /proc/<ppid>/cmdline (null-separated args)
  • macOS: ps -o args= -p <ppid>
  • Windows: best-effort via wmic or skip (fall to active account)
  • Parse image ref: find the arg matching <registry-host>/<identity>/..., extract <identity>
  • Graceful failure: if parent isn't Docker, cmdline unreadable, or image ref not parseable → fall through to active account

File Structure#

cmd/credential-helper/
  main.go              — Cobra root command, version vars, subcommand registration
  config.go            — Config types, load/save/migrate, getConfigPath
  device_auth.go       — authorizeDevice(), validateCredentials() HTTP logic
  protocol.go          — Docker protocol: get, store, erase, list (all hidden)
  cmd_login.go         — login command (huh prompts + device flow)
  cmd_logout.go        — logout command (huh confirm)
  cmd_status.go        — status display
  cmd_switch.go        — switch command (huh select)
  cmd_configure.go     — configure-docker (edit ~/.docker/config.json)
  cmd_update.go        — update command (moved from existing code)
  helpers.go           — openBrowser, buildAppViewURL, isInsecureRegistry, parentCmdline, etc.

Config Format (~/.atcr/device.json)#

{
  "version": 2,
  "registries": {
    "https://atcr.io": {
      "active": "evan.jarrett.net",
      "accounts": {
        "evan.jarrett.net": {
          "handle": "evan.jarrett.net",
          "did": "did:plc:abc123",
          "device_secret": "atcr_device_..."
        },
        "michelle.jarrett.net": {
          "handle": "michelle.jarrett.net",
          "did": "did:plc:def456",
          "device_secret": "atcr_device_..."
        }
      }
    },
    "https://buoy.cr": {
      "active": "evan.jarrett.net",
      "accounts": { ... }
    }
  }
}

Migration: loadConfig() auto-detects and migrates from old formats:

  • Legacy single-device {handle, device_secret, appview_url} → v2
  • Current multi-registry {credentials: {url: {...}}} → v2
  • Writes back migrated config on first load

Key Behavioral Changes#

Command Current New
get Opens browser, polls 2min if no creds Smart detection → active account → error
get (multi-account) N/A (single account only) Auto-detects identity from parent cmdline
get (no stdin) Hangs forever Detects terminal, prints help, exits 1
get (OAuth expired) Auto-opens browser, polls Prints login URL, exits 1
store No-op Stores if secret is device secret (atcr_device_*)
erase Removes all creds for host Removes active account only
No args Prints bare usage Prints full cobra help with all commands

Dependencies#

  • github.com/spf13/cobra — already in go.mod
  • github.com/charmbracelet/huh — new (pure Go, CGO_ENABLED=0 safe)

No changes to .goreleaser.yaml needed.

Implementation Order#

Phase 1: Foundation#

  1. helpers.go — move utility functions verbatim + add getParentCmdline() and detectIdentityFromParent(registryHost)
  2. config.go — new config types + migration from old formats
  3. main.go — Cobra root command, register all subcommands

Phase 2: Docker Protocol (must work for existing users)#

  1. device_auth.go — extract authorizeDevice() + validateCredentials()
  2. protocol.goget/store/erase/list using new config with smart account resolution

Phase 3: User Commands#

  1. cmd_login.go — interactive device flow with huh spinner
  2. cmd_status.go — display all registries/accounts
  3. cmd_switch.go — huh select for account switching
  4. cmd_logout.go — huh confirm for removal
  5. cmd_configure.go — Docker config.json manipulation
  6. cmd_update.go — move existing update logic

Phase 4: Polish#

  1. Add huh to go.mod
  2. Delete old main.go contents (replaced by new files)

What to Keep vs Rewrite#

Keep (move to new files): openBrowser(), buildAppViewURL(), isInsecureRegistry(), getDockerInsecureRegistries(), readDockerDaemonConfig(), stripPort(), isTerminal(), authorizeDevice() HTTP logic, validateCredentials(), all update/version check functions.

Rewrite: main(), handleGet() (split into non-interactive get with smart detection + interactive login), handleStore() (implement actual storage), handleErase() (multi-account aware), config types and loading.

New: list, login, logout, status, switch, configure-docker commands. Config migration. Parent process identity detection. huh integration.

Verification#

  1. Build: go build -o bin/docker-credential-atcr ./cmd/credential-helper
  2. Help works: bin/docker-credential-atcr --help shows all user commands
  3. Protocol works: echo "atcr.io" | bin/docker-credential-atcr get returns credentials or helpful error
  4. No hang: bin/docker-credential-atcr get (no stdin pipe) detects terminal, prints help, exits
  5. Smart detection: docker push atcr.io/evan.jarrett.net/test:latest auto-selects evan.jarrett.net
  6. Login flow: bin/docker-credential-atcr login triggers device auth with huh prompts
  7. Status: bin/docker-credential-atcr status shows configured accounts
  8. Config migration: Place old-format ~/.atcr/device.json, run any command, verify auto-migration
  9. GoReleaser: CGO_ENABLED=0 go build ./cmd/credential-helper succeeds