Skip to main content

athena-claude-coder

The in-guest Agent Protocol server that drives the Claude Agent SDK for Athena coding tasks (ADR 0006).

The Athena deep agent delegates repository work to a claude-coder async subagent (deepagents AsyncSubAgent). Its calls travel through the agora coding-worker proxy (/api/coding-worker/{asset_id}/…), which authorizes every request, wakes the computer, ensures this server is running, mints a router grant, and forwards to loopback port 46100 inside the tenant's Talos v2 computer. Here, one ClaudeSDKClient per thread works in the checkout at /workspace/template and answers with a compact structured result.

Route Who calls it What it does
GET /ok the ensure command, the proxy unauthenticated liveness {ok, version}; adds busy, active_thread_id, owner, owner_since, owner_thread_id only when the guest token header matches (never 401)
POST /threads threads.create create a task thread (the proxy supplies the id)
GET /threads/{thread_id} threads.get the Thread shape; values.messages[-1].content is the result JSON
GET /threads/{thread_id}/state threads.get_state {values, next, metadata} (parity)
POST /threads/{thread_id}/runs runs.create start a Claude turn; multitask_strategy=interrupt supersedes a live one
GET /threads/{thread_id}/runs/{run_id} runs.get the Run shape; error carries the reason on error/interrupted
POST /threads/{thread_id}/runs/{run_id}/cancel runs.cancel interrupt + bounded drain → 204; idempotent
PUT /internal/credential the proxy rotate the task owner key on disk → 204
POST /internal/handoff/request the athena-claude launcher interrupt + drain the live run, lock the session for a human → {thread_id, claude_session_id, cwd, owner}
POST /internal/handoff/return the launcher on exit release the lock, record the session id the human ended on → {thread_id, owner}
GET /threads/{thread_id}/transcript?after&limit the proxy (task card) Claude Code's own session transcript for the thread, projected and redacted → {thread_id, claude_session_id, total, entries}

Nothing else exists: no LangGraph in the guest, no streaming endpoints, no OpenAPI docs.

Install

From a checkout — what the environment bake does today:

uv tool install --python 3.12 /workspace/template/python-sdk/athena-claude-coder
athena-claude-coder --version
athena-claude-coder serve
install -m 0755 /workspace/template/python-sdk/athena-claude-coder/scripts/athena-claude /opt/athena/bin/athena-claude

From PyPI — only after the first human-triggered publish. Nothing publishes automatically: a human dispatches .github/workflows/publish-athena-claude-coder.yml from staging with confirm=publish and the version that equals __version__ in src/athena_claude_coder/__init__.py (the upload job runs in the pypi GitHub environment, which holds the token and its approval/branch rules); until that has happened for a version, athena-claude-coder==<version> does not resolve.

uv tool install --python 3.12 athena-claude-coder==<version>
athena-claude-coder --version    # must print that version; the proxy refuses guests below its minimum

The wheel carries the server only. The athena-claude human launcher (scripts/athena-claude) is not in it — install it from the checkout (the line above) or from the bake runbook's inline copy.

claude-agent-sdk==0.2.151 is pinned exactly and bundles the Claude Code CLI.

Configuration

Everything comes from the environment; the proxy's ensure command exports it before pm2 starts the server.

Variable Default Meaning
ATHENA_CODING_WORKER_PORT 46100 loopback port (never public; the router grant targets it)
ATHENA_CODING_WORKER_STATE_DIR /workspace/.claude-state SQLite, run logs, credential, settings.json; also CLAUDE_CONFIG_DIR
ATHENA_CODING_WORKER_REPO_DIR /workspace/template Claude's cwd
ATHENA_CODING_WORKER_DRAIN_SECONDS 30 how long an interrupt waits for the turn's terminal message before killing the task
ATHENA_CODING_WORKER_MAX_RUN_HOURS 6 runs older than this are interrupted by the sweep
ATHENA_CODING_WORKER_TOKEN shared secret; required for every route except GET /ok

The gate

This server is not the security boundary — the microVM, the agora proxy (per-request get_asset_with_permission_check + ABAC EDIT) and the gateway budget are. Two things here are still load-bearing:

  • Guest token. Every route except GET /ok requires X-Athena-Coding-Worker-Token to equal ATHENA_CODING_WORKER_TOKEN (constant-time compare) → otherwise 401. If the variable is unset the server fails closed: those routes answer 503 {"detail": "guest token not configured"}. GET /ok never 401s, but without a matching token it says only {ok, version}; the proxy's ensure command proves the running server adopted the current token with GET /threads/00000000-0000-4000-8000-000000000000404 (wrong token 401, unset 503).
  • can_use_tool. Fail-closed defence in depth for Claude's tool calls: read-only tools pass, writes are confined to the checkout, the state dir and /tmp, destructive shell verbs are never allowed, and git push, gh pr …, publishing and force operations need an explicit AUTHORIZED: <action> line in the task text. The credential files under the state dir are off-limits to every tool (and the search tools may not be pointed at the state dir root); shell references to them or to the athena-claude-key helper are refused.
  • One live task per computer. Every task shares one checkout and one credential file, so the envelope's max_concurrent_tasks is clamped to 1 until worktree-per-task lands (plan P2.4); a second thread gets 409 computer busy. The slot is reserved before anything is written, so a create that loses a race leaves no transcript entry, run row or credential.

Credentials

The run's config.configurable.athena envelope — injected by the proxy, never trusted from anywhere else — carries the task owner key. The server writes it to <state_dir>/credential (mode 0600, atomic temp+rename, raw key only) and <state_dir>/credential.json (expires_at, budget_user_id, anthropic_base_url). Claude reads the key through the apiKeyHelper (/opt/athena/bin/athena-claude-key, installed by the bake) named in the settings.json that serve writes; the server process never passes it as an environment variable.

serve also removes ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN and CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR from its own environment before any client is built: ClaudeAgentOptions.env is merged over os.environ and cannot unset a key, and the computer's own gateway key is in the inherited environment.

Runs, results, durability

  • One consumer. Only the run task iterates client.receive_response(). Interrupts (cancel, multitask_strategy=interrupt, the sweep, shutdown) set a flag and call client.interrupt(); the same loop drains the interrupted turn's ResultMessage and records interrupted. The drain is bounded by ATHENA_CODING_WORKER_DRAIN_SECONDS; past it the task is cancelled and the SDK closes the subprocess.
  • Result JSON. The last AI message is the compact result (status ∈ completed|blocked|failed, summary, branch, commit_sha, changed_files, tests, artifacts, blockers, questions, plus claude_session_id, cost_usd, num_turns, terminal_reason, run_id). Budget exhaustion is status: blocked with blockers: ["budget exhausted"]. A run is success whenever Claude produced a terminal result — including error results — because the parent only reads the message on success; a run is error only when the SDK or process failed.
  • Same thread = same Claude session. The session id from the init message is persisted immediately; every later run on the thread resumes it.
  • Suspend-safe. State is SQLite plus files on the persisted tree. At boot every pending/running row becomes interrupted (error: restart) with its session id intact; the sweep interrupts runs older than ATHENA_CODING_WORKER_MAX_RUN_HOURS.

Human handoff (P2.1)

A human in the computer terminal (Olympus terminal tab or ssh) can take a task's Claude session over and give it back, on the same session: both sides share CLAUDE_CONFIG_DIR and the checkout, so the next SDK run resumes exactly where the human stopped.

State (sessions.owner) Entered by What the SDK may do
sdk a run starting — (it is the run)
human POST /internal/handoff/request: the live run is interrupted with reason handoff, drained through the single stream consumer, and recorded as a success whose last AI message is {"status": "blocked", "summary": "Handed off to a human in the computer terminal", "blockers": ["human_active"], "claude_session_id", "run_id"}success so check_async_task reads it runs.create/reserve409 human holds the session, nothing written
none POST /internal/handoff/return (records the session id the human ended on), a run finishing, or a restart clearing a dead sdk holder start a run; it passes resume=<claude_session_id>

Both routes are idempotent and need the guest token. owner_since is the moment the lock was taken (null for none) and doubles as the lock's lease: the request returns it (with acquired: true the first time, false while a human already holds the session), the return must present it, and a return with a stale lease — or one arriving while the SDK owns the thread — is a no-op. A human lock also occupies the computer's one task slot, so other threads are refused with 409 computer busy while it is held. The request carries the launcher's launcher_pid; a lock whose launcher process is gone (closed terminal, suspended computer) is released at startup, before every reservation and by the sweep. takeover: true re-mints the lease for a second terminal; the earlier launcher's return then does nothing. The authenticated GET /ok reports owner and owner_since for the live thread, else the human-held or most recent session, and names that thread in owner_thread_id (null when no session exists) — once a handoff has drained the run and active_thread_id is gone, this is what ties a human lock to its task.

scripts/athena-claude is the launcher the bake installs at /opt/athena/bin/athena-claude: it reads the guest token from the running server's pm2 environment (PM2_HOME=/workspace/.pm2-athena-coder pm2 jlist, never printed), requests the handoff, runs claude --resume <session> (or a fresh --session-id it chooses) with the SDK's exact environment and cwd, and on exit — any exit code, via trap, retried while the worker is unreachable — posts the return with the lease and the session id it ended on. It refuses to start a second writer when another terminal holds the session (ATHENA_CLAUDE_TAKEOVER=1 takes it over). ATHENA_CODING_WORKER_THREAD_ID picks a thread explicitly; the default is the live one, else the most recent session.

Concurrency

The run envelope's max_concurrent_tasks (the workspace policy) sets the number of task slots, capped at HARD_MAX_CONCURRENT_TASKS = 4. With one slot (the default) a task runs in the checkout itself (ATHENA_CODING_WORKER_REPO_DIR), exactly as before. With more, every task thread gets its own git worktree:

  • git worktree add /workspace/worktrees/<thread12> -b athena/<thread12> from the checkout's current HEAD on the thread's first run (thread12 = first 12 characters of the thread id); the path is persisted as the thread's cwd, so every later run — and a human handoff — lands in the same worktree. An existing worktree or branch (crash between create and persist, or a garbage-collected worktree) is adopted / re-attached, never recreated.
  • A thread occupies one slot while it has a live run, a reservation or a human holder; the (N+1)th concurrent thread is refused with 409 computer busy, a follow-up run on a thread that holds a slot reuses it. Slot indices are stable while a thread holds its slot; GET /ok reports slots: {used, total}, active_thread_ids (oldest first) and keeps active_thread_id (the oldest live one); busy means every slot is taken.
  • Each task's system prompt names its dev-server port, 46200 + slot index, and warns that other tasks may be using the neighbouring ports.
  • Each task reads its own credential: runs.create writes the envelope's owner key to <state_dir>/credentials/<thread_id> as well as to the shared credential file, and the run's --settings file (settings-<thread_id>.json) names an apiKeyHelper that cats that copy, so concurrent tasks never bill each other's key. The shared file and the baked helper stay for the human launcher; PUT /internal/credential may carry thread_id to rotate a task's copy too.
  • A human handoff takes a slot exactly like a run (refused with 409 computer busy beyond the policy) and a never-run thread handed to a human gets its own worktree under a multi-slot policy.
  • Threads that already work in the checkout (first run under one slot, or any 0.2.0 row) keep it for transcript continuity; when the policy grows, at most one such thread may be active at a time — a second is refused with 409 computer busy rather than allowed to share the tree. Two thread ids that share their first 12 characters get distinct worktrees (the second takes a longer name).
  • GC runs on the sweep cadence and removes a worktree only when its thread is terminal (no live run, no reservation, owner none), the worktree is clean (git status --porcelain empty and no commit ahead of the checkout's HEAD) and its session has been idle for more than 24 h; eligibility is re-checked under the runner lock right before the removal, so a thread claimed meanwhile keeps its worktree. A dirty or ahead worktree is kept and logged once. The athena/<thread12> branch is never deleted — it is the work product — and the checkout itself is never touched.

Transcript route

GET /threads/{thread_id}/transcript?after=<int>&limit=<int> (guest token required) reads Claude Code's own session transcript for the thread — the JSONL under CLAUDE_CONFIG_DIR/projects/<project-slug>/<claude_session_id>.jsonl, where the project slug follows the thread's cwd (its worktree under a multi-slot policy). Agora reads a task's turns through this route instead of the guest pushing them into the Athena session: the session bridge has no credential a guest could hold. The read is never taken under the runner lock and runs in a worker thread.

  • 404 thread not found for an unknown thread; 404 no transcript for this thread yet while the thread has no claude_session_id or the file does not exist yet.
  • One entry per content block of every user/assistant message: {seq, role: "user"|"assistant", kind: "text"|"tool_use"|"tool_result"|"thinking", text, tool_name?, ts?}. seq is a stable 0-based index over the whole file, text is clipped to 4000 characters (a tool_use renders compactly — the Bash command, the Read/Edit/Write path, the Grep pattern — and a tool_result carries the result text and the name of the tool it answers), ts is the entry timestamp. Summary, system and meta lines are skipped.
  • The projection follows the live conversation the way the pinned SDK's session reader finds it: the most recent turn no later record descends from, and its parentUuid ancestors. Turns of an abandoned branch (a rewind in the terminal during a handoff) and subagent isSidechain records are left out. Unlike the SDK, a compact_boundary is crossed through its logicalParentUuid, so the turns before an auto-compaction stay in the card and the isCompactSummary message appears in their flow — following the SDK here would make total collapse at every compaction. seq is therefore stable while the conversation only grows; should the live leaf move to another branch, earlier seqs can shift and total can shrink — a consumer that sees total below its cursor re-reads from after=0.
  • Two passes over the file: one for the uuid/parentUuid graph (a few fields per record), one that projects only live records and redacts and clips only the requested window — memory is the graph plus the page, not the transcript.
  • after returns entries with seq >= after (negative → 0); limit is clamped to 1..500, default 200; total always counts the whole file.
  • Credential shapes are redacted before anything leaves the guest (redact.py, mirroring the dcode mirror's list): sk-… keys, Bearer and Basic authorization, x-api-key headers, environment-style assignments whose name says credential (…_API_KEY=, …_ACCESS_KEY=, …_SECRET=, …_TOKEN=, …_PASSWORD= — the guest's own ATHENA_*/ANTHROPIC_* variables, AWS_SESSION_TOKEN, GITHUB_TOKEN, …), GitHub/AWS/Slack/Stripe shapes, PEM blocks and URL userinfo.

Tests

cd python-sdk/athena-claude-coder
pip install -e ".[dev]"
python -m pytest -v --tb=short && ruff check . && ruff format --check .

The suite never spawns a Claude process: a fake ClaudeSDKClient drives the runner, and the protocol contract is exercised through the real langgraph_sdk client on an ASGI transport. Tests marked post_merge need a real Claude and are skipped unless ATHENA_CODING_WORKER_POST_MERGE=1.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

athena_claude_coder-0.4.1.tar.gz (88.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

athena_claude_coder-0.4.1-py3-none-any.whl (52.7 kB view details)

Uploaded Python 3

File details

Details for the file athena_claude_coder-0.4.1.tar.gz.

File metadata

  • Download URL: athena_claude_coder-0.4.1.tar.gz
  • Upload date:
  • Size: 88.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for athena_claude_coder-0.4.1.tar.gz
Algorithm Hash digest
SHA256 7f17fa0a5f71bf2097f3ed345e197e27783c9c1a09c7e0478d8d78d7b2b99ff3
MD5 2e549c79bce715909fe1f216125ce086
BLAKE2b-256 73a0535fccffec9bf0c3db20677638a33564bcc7ee0b9b9bef2c10b083e187a2

See more details on using hashes here.

File details

Details for the file athena_claude_coder-0.4.1-py3-none-any.whl.

File metadata

File hashes

Hashes for athena_claude_coder-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4cd935b01d23bd7b2d03d376ee11e3e2eb62a654b9ee943f6a59fe6b482b77ad
MD5 4a39251fbe6da828f50eafda5836d7ad
BLAKE2b-256 10bb233f33081e9ace5cc98365452beb2f4d1d33e4a65e0a9bb61b64dee2340d

See more details on using hashes here.

Release history Release notifications | RSS feed

0.4.2

2 files

This release

0.4.1 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page