-
Notifications
You must be signed in to change notification settings - Fork 3
Add cloud session bootstrap for eval runbook inputs #1193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
charlesyhuang
wants to merge
2
commits into
staging
Choose a base branch
from
claude/cloud-task-dependencies-g4puha
base: staging
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| #!/bin/bash | ||
| # SessionStart bootstrap for Claude Code on the web. | ||
| # | ||
| # Cloud sessions get a fresh container with the repo cloned and nothing else: | ||
| # no virtualenv, no agent CLIs, no ~/.env, no shell profile. This script | ||
| # reconstructs the pieces the eval runbooks assume, so a session can run | ||
| # `oddish` without a manual setup round-trip first. | ||
| # | ||
| # It never invents credentials. Secrets come from the environment's variable | ||
| # config (claude.ai/code -> Environments); this script only bridges them to the | ||
| # places local tooling looks for them, and reports what is missing. | ||
| # | ||
| # See docs/cloud-session-setup.md. | ||
| set -uo pipefail | ||
|
|
||
| # Local machines already have a venv, a shell profile, and the agent CLIs on | ||
| # PATH. Only the disposable remote container needs rebuilding. | ||
| if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| REPO_ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" | ||
| ENV_HOME="${HOME:-/root}" | ||
|
|
||
| # Credentials bridged into ~/.env for scripts that `source` it. AWS is | ||
| # deliberately absent: the log-bucket creds are short-lived STS tokens with | ||
| # their own refresh cycle (runbook section 6), not session-long config. | ||
| BRIDGED_VARS=( | ||
| ODDISH_API_KEY | ||
| ODDISH_API_URL | ||
| XAI_API_KEY | ||
| XAI_API_KEYS | ||
| ANTHROPIC_API_KEY | ||
| OPENAI_API_KEY | ||
| META_API_KEY | ||
| ) | ||
|
|
||
| status_lines=() | ||
| note() { status_lines+=("$1"); } | ||
|
|
||
| # --- oddish CLI ------------------------------------------------------------ | ||
| # Replaces the local ~/oddish/oddish/.venv that runbooks reference by path. | ||
| # --extra server matches the documented setup (AGENTS.md, "Local Development"); | ||
| # without it the test suite cannot import sqlalchemy. | ||
| venv_bin="$REPO_ROOT/oddish/.venv/bin" | ||
| if command -v uv >/dev/null 2>&1; then | ||
| sync_log="$(cd "$REPO_ROOT/oddish" && uv sync --frozen --extra server 2>&1)" || \ | ||
| sync_log="$(cd "$REPO_ROOT/oddish" && uv sync --extra server 2>&1)" | ||
| if [ -x "$venv_bin/oddish" ]; then | ||
| note "oddish CLI ok $venv_bin/oddish" | ||
| else | ||
| note "oddish CLI FAILED uv sync did not produce the venv" | ||
| printf '%s\n' "$sync_log" | tail -20 >&2 | ||
| fi | ||
| else | ||
| note "oddish CLI FAILED uv not on PATH" | ||
| fi | ||
|
|
||
| # --- grok CLI -------------------------------------------------------------- | ||
| # Reported, not installed: pulling and running a remote installer at session | ||
| # start is a network dependency the session cannot audit. Install it in-session | ||
| # when a task needs it -- docs/cloud-session-setup.md has the command. | ||
| if [ -x "$ENV_HOME/.grok/bin/grok" ]; then | ||
| note "grok CLI ok $ENV_HOME/.grok/bin/grok" | ||
| else | ||
| note "grok CLI absent see docs/cloud-session-setup.md to install" | ||
| fi | ||
|
|
||
| # --- ~/.env ---------------------------------------------------------------- | ||
| # Bridges configured secrets to the file local scripts source. Rewrites only | ||
| # its own marker block so anything hand-added during the session survives. | ||
| env_file="$ENV_HOME/.env" | ||
| begin="# >>> oddish cloud bootstrap >>>" | ||
| end="# <<< oddish cloud bootstrap <<<" | ||
| bridged=() | ||
| block="$begin"$'\n'"# Generated at session start from the environment's variable config." | ||
| for var in "${BRIDGED_VARS[@]}"; do | ||
| if [ -n "${!var:-}" ]; then | ||
| block+=$'\n'"export $var=${!var@Q}" | ||
| bridged+=("$var") | ||
| fi | ||
| done | ||
| block+=$'\n'"$end" | ||
|
|
||
| if [ -f "$env_file" ] && grep -qF "$begin" "$env_file" 2>/dev/null; then | ||
| kept="$(awk -v b="$begin" -v e="$end" \ | ||
| 'index($0,b){s=1} !s{print} index($0,e){s=0}' "$env_file")" | ||
| else | ||
| kept="$(cat "$env_file" 2>/dev/null)" | ||
| fi | ||
| umask 077 | ||
| { [ -n "$kept" ] && printf '%s\n' "$kept"; printf '%s\n' "$block"; } > "$env_file" | ||
| chmod 600 "$env_file" | ||
|
|
||
| if [ ${#bridged[@]} -gt 0 ]; then | ||
| note "~/.env ok ${#bridged[@]} var(s): ${bridged[*]}" | ||
| else | ||
| note "~/.env empty no known credentials set on this environment" | ||
| fi | ||
|
|
||
| # --- session env ----------------------------------------------------------- | ||
| # One export, venv first: ~/.local/bin carries its own pytest/ruff/black/mypy, | ||
| # which would shadow the venv's if it were prepended afterwards. | ||
| # | ||
| # Written at most once. SessionStart also fires on resume, clear, and compact, | ||
| # and CLAUDE_ENV_FILE persists across those firings, so an unguarded append | ||
| # would stack a duplicate prefix onto PATH every time the session compacted. | ||
| if [ -n "${CLAUDE_ENV_FILE:-}" ]; then | ||
| path_prefix="" | ||
| [ -d "$venv_bin" ] && path_prefix="$venv_bin:" | ||
| path_line="export PATH=\"$path_prefix$ENV_HOME/.grok/bin:$ENV_HOME/.local/bin:\$PATH\"" | ||
| if ! grep -qxF "$path_line" "$CLAUDE_ENV_FILE" 2>/dev/null; then | ||
| printf '%s\n' "$path_line" >> "$CLAUDE_ENV_FILE" | ||
| fi | ||
| fi | ||
|
|
||
| # --- report ---------------------------------------------------------------- | ||
| # SessionStart stdout joins the session context, so the agent starts knowing | ||
| # what it has. Names and states only -- never values. | ||
| echo "Cloud bootstrap (docs/cloud-session-setup.md):" | ||
| printf ' %s\n' "${status_lines[@]}" | ||
|
|
||
| missing=() | ||
| for var in ODDISH_API_KEY XAI_API_KEY; do | ||
| [ -z "${!var:-}" ] && missing+=("$var") | ||
| done | ||
| if [ ${#missing[@]} -gt 0 ]; then | ||
| echo " missing secrets: ${missing[*]}" | ||
| echo " -> set them on this environment at claude.ai/code (Environments ->" | ||
| echo " this environment -> environment variables), then start a new session." | ||
| fi | ||
|
|
||
| # Always succeed: a partial bootstrap should degrade the session, not block it. | ||
| exit 0 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| # Cloud session setup (Claude Code on the web) | ||
|
|
||
| How to give a cloud Claude Code session the things the eval runbooks assume. | ||
| The short version: **secrets come from the environment's variable config, code | ||
| comes from attached repos, and everything else is rebuilt by the SessionStart | ||
| hook.** Nothing is copied from a laptop. | ||
|
|
||
| ## What a fresh cloud container actually has | ||
|
|
||
| A cloud session runs in an ephemeral container: this repo is cloned fresh, the | ||
| container is reclaimed when the session ends, and anything not committed is | ||
| lost. It starts with no virtualenv, no `~/.env`, no shell profile, no agent | ||
| CLIs, and no credentials beyond what the environment injects. | ||
|
|
||
| That is why runbook steps written against a laptop fail verbatim. Most of them | ||
| do not need porting — they need translating: | ||
|
|
||
| | Runbook input (laptop) | Cloud equivalent | | ||
| | ----------------------------------------- | ----------------------------------------------------------------------- | | ||
| | `~/oddish/oddish/.venv/bin/oddish` | `<repo>/oddish/.venv/bin/oddish` — built by the hook, and on `PATH` | | ||
| | `~/.grok/bin/grok` | installed in-session on demand (see below) | | ||
| | `XAI_API_KEY` via `~/.env` | set on the environment; the hook writes it back into `~/.env` | | ||
| | `~/.oddish` credentials | **does not exist** — see below | | ||
| | `~/cyberpipeline/*.sh` and similar | must live in a git repo and be attached to the session | | ||
|
|
||
| ### There is no `~/.oddish` credentials file | ||
|
|
||
| The CLI reads credentials from the environment only — `ODDISH_API_KEY` and | ||
| `ODDISH_API_URL`, resolved in `oddish/src/oddish/cli/config.py` | ||
| (`get_api_key`, `get_api_url`). There is no credentials file, no `oddish login`, | ||
| and no dotfile to copy. Setting the two environment variables is the whole of | ||
| CLI auth, locally and in the cloud alike. | ||
|
|
||
| ## 1. Secrets: set them on the environment | ||
|
|
||
| Environment variables are configured per environment at | ||
| [claude.ai/code](https://claude.ai/code) → Environments → *(your environment)* → | ||
| environment variables. They are injected into every session that environment | ||
| starts, so this is a one-time setup rather than a per-session step. | ||
|
|
||
| Set what the work needs: | ||
|
|
||
| | Variable | Needed for | | ||
| | ------------------- | ------------------------------------------------------------ | | ||
| | `ODDISH_API_KEY` | every `oddish` command that talks to the hosted API | | ||
| | `ODDISH_API_URL` | only to target a non-default API (preview, self-hosted) | | ||
| | `XAI_API_KEY` | the grok CLI, and any script that authenticates to xAI | | ||
| | `ANTHROPIC_API_KEY` | CUA verifiers on the open-internet tasks | | ||
| | `OPENAI_API_KEY` / `META_API_KEY` | vendor routes that read them | | ||
|
|
||
| Two things these variables are *not* for: | ||
|
|
||
| - **Trial credentials.** A vendor key used by an agent inside a trial must be a | ||
| **Modal secret** on the worker function, not a session variable — see the | ||
| runbook's prereqs. A key set here is available to the session driving the | ||
| eval, not to the sandboxes running it. | ||
| - **The log-bucket export.** Those AWS credentials are short-lived STS tokens | ||
| (~1h) with their own refresh cycle, so the hook deliberately leaves `AWS_*` | ||
| alone. Export them per-session as the runbook describes. | ||
|
|
||
| ## 2. Code: attach the repo it lives in | ||
|
|
||
| A cloud session can only see repositories attached to it. Scripts that live | ||
| only on a laptop — a `~/cyberpipeline` working directory, `author_prompt.sh`, | ||
| `ship.sh` — are unreachable no matter how the environment is configured. Push | ||
| them to a repo first; then a session can be given access to it, and can clone it | ||
| alongside this one. | ||
|
|
||
| The eval runbook's background-agent prompt already assumes this: it asks for | ||
| access to `abundant-ai/oddish`, `abundant-ai/harbor`, and | ||
| `abundant-ai/swe-marathon`. Add whichever repo holds the pipeline scripts to | ||
| that list. | ||
|
|
||
| ## 3. Everything else: the SessionStart hook | ||
|
|
||
| `.claude/hooks/session-start.sh` runs when a cloud session starts and: | ||
|
|
||
| - runs `uv sync --frozen --extra server` in `oddish/`, producing | ||
| `oddish/.venv/bin/oddish` (the `--extra server` matches AGENTS.md; without it | ||
| the test suite cannot import `sqlalchemy`) | ||
| - puts that venv, `~/.grok/bin`, and `~/.local/bin` on `PATH`, venv first — | ||
| `~/.local/bin` ships its own `pytest`/`ruff`/`black`/`mypy`, which would | ||
| otherwise shadow the project's | ||
| - writes the configured credentials into `~/.env` for scripts that `source` it, | ||
| in a marked block so anything else in the file survives | ||
| - prints a status line per input, and names any missing secret | ||
|
|
||
| It is remote-only (`CLAUDE_CODE_REMOTE`), idempotent, and always exits 0 — a | ||
| partial bootstrap degrades a session rather than blocking it. It never invents | ||
| a credential: if a variable is not set on the environment, the hook says so. | ||
|
|
||
| Idempotency matters here because `SessionStart` fires on `resume`, `clear`, and | ||
| `compact` as well as on startup, and a long eval session compacts repeatedly. | ||
| Each firing rewrites only its own `~/.env` block and writes the `PATH` line at | ||
| most once, so nothing accumulates. The `uv sync` does re-run every time; that is | ||
| deliberate and costs about a second once the cache is warm, and it repairs a | ||
| venv that has been broken or partially installed mid-session. | ||
|
|
||
| To enable it, register it in `.claude/settings.json`: | ||
|
|
||
| ```json | ||
| { | ||
| "hooks": { | ||
| "SessionStart": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The hook takes effect for sessions started after that lands on the default | ||
| branch. | ||
|
|
||
| ### Installing the grok CLI | ||
|
|
||
| The hook reports the grok CLI but does not install it: it is a network install | ||
| most sessions never need. When a task does need it: | ||
|
|
||
| ```bash | ||
| curl -fsSL https://x.ai/cli/install.sh -o /tmp/grok-install.sh | ||
| bash /tmp/grok-install.sh | ||
| export PATH="$HOME/.grok/bin:$PATH" | ||
| ``` | ||
|
|
||
| This is the same installer `OddishGrokBuildAgent.install()` runs inside Harbor | ||
| sandboxes (`oddish/src/oddish/workers/agents/grok_build.py`). Note the | ||
| distinction: that in-sandbox install is what trials use, and it happens whether | ||
| or not the CLI is present in the session. You only need it in the session | ||
| itself if *you* are driving grok directly. | ||
|
|
||
| ## Verifying | ||
|
|
||
| The hook's own output is the check — it prints one line per input at session | ||
| start. To re-run it by hand: | ||
|
|
||
| ```bash | ||
| CLAUDE_CODE_REMOTE=true .claude/hooks/session-start.sh | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.