fix(cli): send api-keys via X-API-Key in client.New + help surface #7
Reference in New Issue
Block a user
Delete Branch "fix/apikey-auth-header-and-help-introspect"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Manager (role
mgr) reported in fabric channel32a7da92on 2026-05-26 thathfcli had no project-create permission. Real cause was two-layered:passmgr.GetToken()readshf-tokenfrom secret-mgr; provision-hf-accounts.sh historically stored athf-access-token— separate PR (orion/HangmanLab.Server.T2#?) fixes that.client.Newalways sent the api-key asAuthorization: Bearer <hex>. The HF backendHTTPBearermiddleware expects JWT shape there and rejects hex. d2b83ad on the backend added a Bearer-fallback that tries the string as an api-key, which masks the cli bug against current prod — buthelp/surface.go:detectPermissionStatecalling/auth/me/permissionswas still failing on older paths, leavingKnown:falseand rendering only the always-permitteduser.*subset.What
client.Newauto-detects token shape:eyJprefix + two dots ⇒ JWT (Authorization: Bearer), anything else ⇒ api-key (X-API-Key). Empty token sets neither header.internal/help/surface.go:loadPermissionStateswitches toclient.NewWithAPIKeyexplicitly so command-discovery does not depend on the heuristic at all.internal/client/client_test.gowith three table-driven tests covering both header paths, empty token, andNewWithAPIKeyprecedence.Verification
go test ./internal/client/— 3/3 passharborforge-backendto prod commit d2b83ad, then wire-level capture against fake backend:d7a774…→X-Api-Key: d7a774…eyJ…→Authorization: Bearer eyJ…HF_TEST_MODE=1 ./hf project list --token <mgr-api-key>returns the project list (200 OK).🤖 Generated with Claude Code
passmgr.GetToken returns an api-key in padded-cell mode (provisioned by scripts/provision-hf-accounts.sh via 'hf user reset-apikey'), but every call site funneled that through client.New which sent it as a 'Authorization: Bearer <hex>'. The HF backend's HTTPBearer middleware expects JWT shape there and rejects hex strings as 'Could not validate credentials'. The d2b83ad backend fix added a Bearer-fallback that tries the value as an api-key, which masked the issue against current prod; older backends or any future change in that fallback still 401. Two changes: - client.New auto-detects shape: 'eyJ'-prefix + two dots == JWT (Bearer), anything else == api-key (X-API-Key). Empty token sets neither header. - internal/help/surface.go's loadPermissionState (called by hf --help introspection) switches to client.NewWithAPIKey explicitly so the command-discovery path doesn't depend on the heuristic at all. When that path failed silently (Known:false), agents would see only the always-permitted commands ('user.*', 'agent.status', 'config', 'health', 'version') and conclude they had no project permission. Adds internal/client/client_test.go covering both header paths plus empty-token, isLikelyJWT cases, and NewWithAPIKey precedence. Verified end-to-end in sim against a rebuilt hf-backend matching prod (commit d2b83ad): cli with --token <api-key> sends X-Api-Key header, backend returns 200 on /projects + /auth/me/permissions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>