Skip to content

Ralph Credential Injection Proxy #24

@rjernst

Description

@rjernst

branch: ralph-credential-proxy

Spec: Ralph Credential Injection Proxy

Overview

A lightweight HTTP reverse proxy that runs in a Docker container on the host. It receives API requests from Claude Code running inside a Docker sandbox, strips the phantom auth token, injects the real OAuth token (received via stdin at startup), and forwards requests to the Anthropic API.

This is the foundation of ralph's credential injection architecture: the real token never enters the sandbox. Claude sees only a phantom token value, and the proxy swaps it for the real credential before forwarding upstream.

Designed to be agent-agnostic: the proxy doesn't know or care about Claude specifically — it injects a bearer token into requests to a configurable target. Future agents (Codex, etc.) can reuse the same proxy with different tokens/targets.

Architecture

┌─ Docker Sandbox (microVM) ──────────────────────┐
│  CLAUDE_CODE_OAUTH_TOKEN=phantom                 │
│  ANTHROPIC_BASE_URL=http://host.docker.internal:PORT │
│                                                   │
│  Claude sends: Authorization: Bearer phantom      │
└──────────────────┬────────────────────────────────┘
                   │ via Docker's MITM proxy
                   ▼
┌─ Host Docker ─────────────────────────────────────┐
│  agent-loop-proxy container (port mapped)              │
│    Token received via stdin at startup             │
│    Strips Authorization header                     │
│    Injects real token                              │
│    Forwards to TARGET (api.anthropic.com)          │
└───────────────────────────────────────────────────┘

1. Proxy Server

The proxy is a Python 3 stdlib-only HTTP server. It:

  • Reads a single line from stdin on startup (the real token), stores in memory, closes stdin
  • Listens on a configurable port (env var LISTEN_PORT, default 18080)
  • Forwards POST requests to TARGET (env var, default https://api.anthropic.com)
  • Replaces the Authorization header with Bearer <real-token>
  • Copies all other headers (except Host, Content-Length, Transfer-Encoding)
  • Streams the response back to the caller
  • Responds to GET /health with 200 for health checks
  • Logs each proxied request method + path to stderr (no token values in logs)

Must handle:

  • Chunked/streaming responses (SSE from Anthropic API)
  • HTTP errors from upstream (forward status + body back to caller)
  • Large request bodies (prompt content)

Must NOT:

  • Write the token to disk, logs, or environment
  • Include the token in error messages

2. Dockerfile

docker/agent-loop/proxy/Dockerfile
docker/agent-loop/proxy/proxy.py
  • FROM python:3.12-slim (minimal image, no build tools needed)
  • Copy proxy.py
  • EXPOSE 18080
  • CMD ["python3", "/proxy.py"]

3. Integration with ralph

Ralph will start the proxy container with:

echo "$TOKEN" | docker run -i --rm --name agent-loop-proxy-<agent> \
  -p <port>:18080 \
  -e TARGET=https://api.anthropic.com \
  agent-loop-proxy:v1

The <agent> suffix (e.g., claude) supports future multi-agent namespacing.


Implementation Plan

Step 1: Create proxy server [DONE]

Files:

  • docker/agent-loop/proxy/proxy.py — HTTP proxy server

Implement:

  1. Read token from stdin (one line), store in memory, close stdin
  2. HTTP server on LISTEN_PORT (default 18080)
  3. POST handler: read body, build upstream request to TARGET + path, replace Authorization header, forward, stream response back
  4. GET /health handler: return 200 with agent-loop-proxy ok
  5. Logging: method + path to stderr, never log token values

Test:

  • tests/test_ralph_proxy.py:
    • Token read from stdin and stored in memory
    • Authorization header is replaced in forwarded requests
    • Other headers are preserved (except Host, Content-Length, Transfer-Encoding)
    • Health endpoint returns 200
    • Upstream errors are forwarded with correct status code
    • Token never appears in log output
    • Use unittest.mock to mock urllib.request.urlopen for unit tests

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

Review: Check for token leakage in error paths, correct header handling, streaming correctness.

Address feedback: Fix all findings, re-run tests.

Step 2: Create Dockerfile [DONE]

Files:

  • docker/agent-loop/proxy/Dockerfile

Implement:

  1. FROM python:3.12-slim
  2. COPY proxy.py /proxy.py
  3. EXPOSE 18080
  4. CMD ["python3", "/proxy.py"]

Test:

  • Build succeeds: docker build -t agent-loop-proxy:test docker/agent-loop/proxy/
  • Container starts and responds to health check:
    echo "test-token" | docker run -i --rm -p 18081:18080 agent-loop-proxy:test &
    curl -s http://localhost:18081/health
    

Verify: Health check returns agent-loop-proxy ok.

Review: Image size, no unnecessary layers.

Address feedback: Fix, re-test.

Step 3: Run all checks

Implement:

  1. Run pytest tests/test_ralph_proxy.py -v
  2. Run shellcheck and zsh -n on any shell scripts (if added)
  3. Verify Docker build succeeds

Verify: All checks pass clean.

Step 4: Create commit

Implement:

  1. Stage all changes and create a commit summarizing the credential injection proxy.

Verify: git log -1 shows the commit.


Conventions

  • Language: Python 3 (stdlib only, no third-party deps)
  • Tests: pytest with unittest.mock
  • Error messages: Prefix with proxy:
  • Exit codes: 0=success, 1=runtime 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