Skip to content

Ralph Token Management #25

@rjernst

Description

@rjernst

branch: ralph-token-management
depends: [24]

Spec: Ralph Token Management

Source issue: #24 (credential injection proxy)

Overview

Token lifecycle management for ralph's credential injection architecture. Provides commands to store a long-lived OAuth token from claude setup-token into a dedicated macOS Keychain entry, validate token expiry, and extract the token for piping to the credential injection proxy.

The token is stored as JSON (with expiresAt metadata) in a Keychain entry separate from Claude Code's normal OAuth credentials, preventing normal Claude sessions from overwriting the long-lived token.

Designed with multi-agent namespacing: each agent type (e.g., claude, codex) gets its own Keychain entry. Default agent is claude.

Architecture

┌─ One-time setup ─────────────────────────────────────┐
│  claude setup-token                                   │
│    ↓ (stdout: bare token string)                      │
│  ralph store-token [--agent claude]                   │
│    ↓ wraps in JSON with expiresAt                     │
│  Keychain: "claude-token" = {                 │
│    "accessToken": "sk-ant-oat01-...",                 │
│    "expiresAt": <now + 365 days (ms)>                 │
│  }                                                    │
└───────────────────────────────────────────────────────┘

┌─ Each ralph run ─────────────────────────────────────┐
│  ralph check-token [--agent claude]                   │
│    ↓ reads Keychain, checks expiresAt                 │
│    ↓ exits 0 if valid, 1 if expired/missing           │
│                                                       │
│  ralph get-token [--agent claude]                     │
│    ↓ reads Keychain, prints accessToken to stdout      │
│    ↓ piped to proxy container stdin                   │
└───────────────────────────────────────────────────────┘

1. Keychain Storage Format

Keychain entry:

  • Service name: <agent>-token (e.g., claude-token)
  • Account: agent-loop
  • Value (JSON):
    {
      "accessToken": "sk-ant-oat01-...",
      "expiresAt": 1805000000000
    }

expiresAt is Unix timestamp in milliseconds. When store-token receives a bare token string (no JSON), it sets expiresAt to now + 365 days. If setup-token ever outputs JSON with its own expiry, store-token should detect and preserve it.

2. Subcommands

These are subcommands of the ralph script (not separate scripts).

ralph store-token [--agent <name>]

  1. Read token from stdin (bare string or JSON)
  2. If bare string: wrap in {"accessToken": "<token>", "expiresAt": <now + 365d in ms>}
  3. If JSON with accessToken field: use as-is (preserve expiresAt if present, add if missing)
  4. Write to Keychain via security add-generic-password -s "<agent>-token" -a agent-loop -w '<json>' -U
  5. Print confirmation: ralph: token stored for agent <agent> (expires <date>)

ralph check-token [--agent <name>]

  1. Read from Keychain via security find-generic-password -s "<agent>-token" -w
  2. Parse JSON, check expiresAt against current time
  3. If valid: print ralph: token valid for agent <agent> (expires <date>, <N> days remaining), exit 0
  4. If expired: print ralph: token expired for agent <agent> (expired <date>), exit 1
  5. If missing: print ralph: no token found for agent <agent> — run: claude setup-token | ralph store-token, exit 1

ralph get-token [--agent <name>]

  1. Read from Keychain, parse JSON
  2. Print bare accessToken to stdout (for piping to proxy)
  3. If missing or expired: print error to stderr, exit 1

Default --agent is claude for all subcommands.


Implementation Plan

Step 1: Add token management functions ✅

Files:

  • scripts/ralph — add store_token(), check_token(), get_token() functions and CLI routing

Implement:

  1. Add store_token(agent) function: read stdin, detect format (bare vs JSON), wrap if needed, compute expiry, write to Keychain
  2. Add check_token(agent) function: read Keychain, parse JSON, compare expiresAt to now, print status, return exit code
  3. Add get_token(agent) function: read Keychain, parse JSON, check expiry, print accessToken to stdout
  4. Add --agent flag to argument parser (default: claude)
  5. Route store-token, check-token, get-token subcommands in main()
  6. Helper: keychain_service_name(agent)"<agent>-token"
  7. Helper: read_token_from_keychain(agent) → parsed JSON dict
  8. Helper: write_token_to_keychain(agent, json_str) → writes via security

Test:

  • tests/test_ralph.py (extend existing):
    • store_token: bare string input wraps correctly, JSON input preserved, expiresAt computed
    • check_token: valid token returns 0, expired returns 1, missing returns 1
    • get_token: prints accessToken to stdout, errors on expired/missing
    • Keychain calls are mocked (subprocess.run for security commands)
    • Keychain service name includes agent: "claude-token"
    • JSON round-trip: store then read back

Verify: Run pytest tests/test_ralph.py -v. Fix any failures.

Review: Check no token values in error messages, correct Keychain flags, -U for upsert.

Address feedback: Fix, re-test.

Step 2: Update usage text and argument parser ✅

Files:

  • scripts/ralph — update USAGE_TEXT and arg parsing

Implement:

  1. Add store-token, check-token, get-token to usage text
  2. Add --agent flag documentation
  3. Handle subcommands before the existing --issue/--poll flag parsing

Test:

  • ralph store-token --help shows usage
  • ralph check-token --help shows usage
  • ralph --agent codex check-token uses correct Keychain entry name

Verify: Run pytest tests/test_ralph.py -v.

Review: Usage text is clear and consistent.

Address feedback: Fix, re-test.

Step 3: Run all checks

Implement:

  1. Run pytest tests/test_ralph.py -v
  2. Run full test suite: pytest tests/ -v

Verify: All checks pass clean.

Step 4: Create commit

Implement:

  1. Stage all changes and create a commit summarizing token management.

Verify: git log -1 shows the commit.


Conventions

  • Language: Python 3 (stdlib only)
  • Tests: pytest with unittest.mock
  • Error messages: Prefix with ralph:
  • Exit codes: 0=success, 1=runtime error, 2=usage error

Metadata

Metadata

Assignees

No one assigned

    Labels

    specRalph spec for automated executionstatus:doneCompleted

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions