-
Notifications
You must be signed in to change notification settings - Fork 4
Description
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>]
- Read token from stdin (bare string or JSON)
- If bare string: wrap in
{"accessToken": "<token>", "expiresAt": <now + 365d in ms>} - If JSON with
accessTokenfield: use as-is (preserveexpiresAtif present, add if missing) - Write to Keychain via
security add-generic-password -s "<agent>-token" -a agent-loop -w '<json>' -U - Print confirmation:
ralph: token stored for agent <agent> (expires <date>)
ralph check-token [--agent <name>]
- Read from Keychain via
security find-generic-password -s "<agent>-token" -w - Parse JSON, check
expiresAtagainst current time - If valid: print
ralph: token valid for agent <agent> (expires <date>, <N> days remaining), exit 0 - If expired: print
ralph: token expired for agent <agent> (expired <date>), exit 1 - If missing: print
ralph: no token found for agent <agent> — run: claude setup-token | ralph store-token, exit 1
ralph get-token [--agent <name>]
- Read from Keychain, parse JSON
- Print bare
accessTokento stdout (for piping to proxy) - 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— addstore_token(),check_token(),get_token()functions and CLI routing
Implement:
- Add
store_token(agent)function: read stdin, detect format (bare vs JSON), wrap if needed, compute expiry, write to Keychain - Add
check_token(agent)function: read Keychain, parse JSON, compare expiresAt to now, print status, return exit code - Add
get_token(agent)function: read Keychain, parse JSON, check expiry, print accessToken to stdout - Add
--agentflag to argument parser (default:claude) - Route
store-token,check-token,get-tokensubcommands inmain() - Helper:
keychain_service_name(agent)→"<agent>-token" - Helper:
read_token_from_keychain(agent)→ parsed JSON dict - Helper:
write_token_to_keychain(agent, json_str)→ writes viasecurity
Test:
tests/test_ralph.py(extend existing):store_token: bare string input wraps correctly, JSON input preserved, expiresAt computedcheck_token: valid token returns 0, expired returns 1, missing returns 1get_token: prints accessToken to stdout, errors on expired/missing- Keychain calls are mocked (
subprocess.runforsecuritycommands) - 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— updateUSAGE_TEXTand arg parsing
Implement:
- Add
store-token,check-token,get-tokento usage text - Add
--agentflag documentation - Handle subcommands before the existing
--issue/--pollflag parsing
Test:
ralph store-token --helpshows usageralph check-token --helpshows usageralph --agent codex check-tokenuses 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:
- Run
pytest tests/test_ralph.py -v - Run full test suite:
pytest tests/ -v
Verify: All checks pass clean.
Step 4: Create commit
Implement:
- 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