A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
1# Credential Helper Rewrite
2
3## Context
4
5The 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`).
6
7## Approach
8
9Rewrite 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.
10
11**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.
12
13## Command Tree
14
15```
16docker-credential-atcr
17 ├── get (Docker protocol — stdin/stdout, hidden, smart account detection)
18 ├── store (Docker protocol — stdin, hidden)
19 ├── erase (Docker protocol — stdin, hidden)
20 ├── list (Docker protocol extension, hidden)
21 ├── login (Interactive device flow with huh prompts)
22 ├── logout (Remove account credentials)
23 ├── status (Show all accounts with active indicators)
24 ├── switch (Switch active account — auto-toggle for 2, select for 3+)
25 ├── configure-docker (Auto-edit ~/.docker/config.json credHelpers)
26 ├── update (Self-update, existing logic preserved)
27 └── version (Built-in via cobra)
28```
29
30## Smart Account Resolution (`get` command)
31
32The `get` command resolves which account to use with this priority chain — fully non-interactive:
33
34```
351. Parse parent process cmdline → extract identity from image ref
36 docker push atcr.io/evan.jarrett.net/test:latest
37 → parent cmdline contains "evan.jarrett.net" → use that account
38
392. Fall back to active account (set by `switch` command)
40
413. Fall back to sole account (if only one exists for this registry)
42
434. Error with helpful message:
44 "Multiple accounts for atcr.io. Run: docker-credential-atcr switch"
45```
46
47**Parent process detection** (in `helpers.go`):
48- Linux: read `/proc/<ppid>/cmdline` (null-separated args)
49- macOS: `ps -o args= -p <ppid>`
50- Windows: best-effort via `wmic` or skip (fall to active account)
51- Parse image ref: find the arg matching `<registry-host>/<identity>/...`, extract `<identity>`
52- Graceful failure: if parent isn't Docker, cmdline unreadable, or image ref not parseable → fall through to active account
53
54## File Structure
55
56```
57cmd/credential-helper/
58 main.go — Cobra root command, version vars, subcommand registration
59 config.go — Config types, load/save/migrate, getConfigPath
60 device_auth.go — authorizeDevice(), validateCredentials() HTTP logic
61 protocol.go — Docker protocol: get, store, erase, list (all hidden)
62 cmd_login.go — login command (huh prompts + device flow)
63 cmd_logout.go — logout command (huh confirm)
64 cmd_status.go — status display
65 cmd_switch.go — switch command (huh select)
66 cmd_configure.go — configure-docker (edit ~/.docker/config.json)
67 cmd_update.go — update command (moved from existing code)
68 helpers.go — openBrowser, buildAppViewURL, isInsecureRegistry, parentCmdline, etc.
69```
70
71## Config Format (`~/.atcr/device.json`)
72
73```json
74{
75 "version": 2,
76 "registries": {
77 "https://atcr.io": {
78 "active": "evan.jarrett.net",
79 "accounts": {
80 "evan.jarrett.net": {
81 "handle": "evan.jarrett.net",
82 "did": "did:plc:abc123",
83 "device_secret": "atcr_device_..."
84 },
85 "michelle.jarrett.net": {
86 "handle": "michelle.jarrett.net",
87 "did": "did:plc:def456",
88 "device_secret": "atcr_device_..."
89 }
90 }
91 },
92 "https://buoy.cr": {
93 "active": "evan.jarrett.net",
94 "accounts": { ... }
95 }
96 }
97}
98```
99
100**Migration**: `loadConfig()` auto-detects and migrates from old formats:
101- Legacy single-device `{handle, device_secret, appview_url}` → v2
102- Current multi-registry `{credentials: {url: {...}}}` → v2
103- Writes back migrated config on first load
104
105## Key Behavioral Changes
106
107| Command | Current | New |
108|---------|---------|-----|
109| `get` | Opens browser, polls 2min if no creds | Smart detection → active account → error |
110| `get` (multi-account) | N/A (single account only) | Auto-detects identity from parent cmdline |
111| `get` (no stdin) | Hangs forever | Detects terminal, prints help, exits 1 |
112| `get` (OAuth expired) | Auto-opens browser, polls | Prints login URL, exits 1 |
113| `store` | No-op | Stores if secret is device secret (`atcr_device_*`) |
114| `erase` | Removes all creds for host | Removes active account only |
115| No args | Prints bare usage | Prints full cobra help with all commands |
116
117## Dependencies
118
119- `github.com/spf13/cobra` — already in go.mod
120- `github.com/charmbracelet/huh` — new (pure Go, CGO_ENABLED=0 safe)
121
122No changes to `.goreleaser.yaml` needed.
123
124## Implementation Order
125
126### Phase 1: Foundation
1271. `helpers.go` — move utility functions verbatim + add `getParentCmdline()` and `detectIdentityFromParent(registryHost)`
1282. `config.go` — new config types + migration from old formats
1293. `main.go` — Cobra root command, register all subcommands
130
131### Phase 2: Docker Protocol (must work for existing users)
1324. `device_auth.go` — extract `authorizeDevice()` + `validateCredentials()`
1335. `protocol.go` — `get`/`store`/`erase`/`list` using new config with smart account resolution
134
135### Phase 3: User Commands
1366. `cmd_login.go` — interactive device flow with huh spinner
1377. `cmd_status.go` — display all registries/accounts
1388. `cmd_switch.go` — huh select for account switching
1399. `cmd_logout.go` — huh confirm for removal
14010. `cmd_configure.go` — Docker config.json manipulation
14111. `cmd_update.go` — move existing update logic
142
143### Phase 4: Polish
14412. Add `huh` to go.mod
14513. Delete old `main.go` contents (replaced by new files)
146
147## What to Keep vs Rewrite
148
149**Keep** (move to new files): `openBrowser()`, `buildAppViewURL()`, `isInsecureRegistry()`, `getDockerInsecureRegistries()`, `readDockerDaemonConfig()`, `stripPort()`, `isTerminal()`, `authorizeDevice()` HTTP logic, `validateCredentials()`, all update/version check functions.
150
151**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.
152
153**New**: `list`, `login`, `logout`, `status`, `switch`, `configure-docker` commands. Config migration. Parent process identity detection. huh integration.
154
155## Verification
156
1571. Build: `go build -o bin/docker-credential-atcr ./cmd/credential-helper`
1582. Help works: `bin/docker-credential-atcr --help` shows all user commands
1593. Protocol works: `echo "atcr.io" | bin/docker-credential-atcr get` returns credentials or helpful error
1604. No hang: `bin/docker-credential-atcr get` (no stdin pipe) detects terminal, prints help, exits
1615. Smart detection: `docker push atcr.io/evan.jarrett.net/test:latest` auto-selects `evan.jarrett.net`
1626. Login flow: `bin/docker-credential-atcr login` triggers device auth with huh prompts
1637. Status: `bin/docker-credential-atcr status` shows configured accounts
1648. Config migration: Place old-format `~/.atcr/device.json`, run any command, verify auto-migration
1659. GoReleaser: `CGO_ENABLED=0 go build ./cmd/credential-helper` succeeds