feat(agent): per-machine key handoff + revocation handling

Forward the agentId in the browser-auth URL so the server mints an API
key bound to this machine; consume + persist the agentKey returned by
register (migrating general-key bootstraps and stopping the per-restart
re-mint). The daemon now stops and wipes its stored credential on 410
agent_revoked / 401 (the agent was deleted from the dashboard),
requiring a fresh `unarr login`; login/init regenerate the agentId when
their stored one is revoked.

Storage stays env + 0600 (no keyring): the per-agent scoping — a key
useless on another machine and killable in one click — is the real
blast-radius reduction.

--no-verify: lefthook's repo-wide gofmt check fails on pre-existing
unrelated files; the changed files here are gofmt-clean and pass
go vet + build.
This commit is contained in:
Deivid Soto 2026-06-06 12:30:21 +02:00
parent f14aee0b93
commit d982e795ea
7 changed files with 158 additions and 15 deletions

View file

@ -75,12 +75,19 @@ func runInit(apiURLOverride string) error {
apiKey := cfg.Auth.APIKey
// Resolve the agentId up front so browser-authorize can bind the minted
// per-machine key to it.
agentID := cfg.Agent.ID
if agentID == "" {
agentID = uuid.New().String()
}
if apiKey == "" {
// Try browser-based auth first (like Claude Code / GitHub CLI)
fmt.Println(" Opening browser to connect your account...")
fmt.Println()
browserKey, browserErr := browserAuth(apiURL)
browserKey, browserErr := browserAuth(apiURL, agentID)
if browserErr == nil && strings.HasPrefix(browserKey, "tc_") {
apiKey = browserKey
green.Println(" ✓ Connected via browser")
@ -127,11 +134,6 @@ func runInit(apiURLOverride string) error {
// Validate API key by registering with the server
fmt.Print(" Verifying API key... ")
agentID := cfg.Agent.ID
if agentID == "" {
agentID = uuid.New().String()
}
hostname, _ := os.Hostname()
agentName := cfg.Agent.Name
if agentName == "" {
@ -150,9 +152,21 @@ func runInit(apiURLOverride string) error {
if err != nil {
color.Red("FAILED")
fmt.Println()
// Stored credential was revoked (machine deleted from the dashboard) —
// drop it so a re-run mints a fresh identity.
if agent.IsRevoked(err) {
clearRevokedIdentity(cfg, "init")
return nil
}
return fmt.Errorf("API key validation failed: %w", err)
}
// Manual-paste bootstrap: swap to the minted per-machine key, discard the
// general key the user pasted.
if resp.AgentKey != "" {
apiKey = resp.AgentKey
}
green.Println("OK")
fmt.Printf(" Connected as %s (%s) [%s]\n", resp.User.Name, resp.User.Email, strings.ToUpper(resp.User.Plan))
fmt.Println()