Skip to content

fix(sdk): fail fast with a clear message when the CodeAF harness has no model key - #1130

Merged
AbirAbbas merged 6 commits into
mainfrom
fix/sdk-codeaf-missing-key
Oct 26, 2026
Merged

AbirAbbas merged 6 commits into
mainfrom
fix/sdk-codeaf-missing-key

Conversation

@AbirAbbas

Copy link
Copy Markdown
Contributor

Summary

CodeAF became the default harness provider in v0.1.143 (#1128). When an agent has no model key, the SDK still downloads and launches CodeAF. CodeAF then exits 1, and the call comes back as a generic codeaf exit code 1 (stop=): … result. Every one of those launches is also counted by CodeAF's own usage telemetry as a failed session. That has been inflating the error rate in CodeAF's session outcomes since the default changed.

Now, before it resolves, downloads or launches CodeAF, each SDK checks whether CodeAF could find a model key. If it could not, the call fails right away with a message the user can act on, and CodeAF is never started:

Harness provider 'codeaf' needs a model key and none is configured. Set OPENROUTER_API_KEY (https://openrouter.ai/keys), or choose another harness provider with the provider option or AGENTFIELD_HARNESS_PROVIDER.
  • Python / TypeScript: raises HarnessProviderNotConfigured, a subclass of HarnessProviderUnavailable, so existing except / instanceof handlers still catch it. missing_auth_env / missingAuthEnv is ["OPENROUTER_API_KEY"].
  • Go: returns the existing *ProviderUnavailableError with NotConfigured: true, so errors.As still matches. Existing messages are unchanged.
  • Warnings: the first refusal in a process logs a warning. An Agent built with a harness config that resolves to CodeAF also warns once at construction. Construction never fails because of it.
  • The check mirrors CodeAF's own key rules, applied to the environment the CodeAF process would actually get, including per-call env overrides and the SDK's default CODEAF_HOME. CodeAF is treated as having a key when any of these is true:
    • OPENROUTER_API_KEY is set;
    • OPENAI_API_KEY starts with sk-or-;
    • OPENAI_API_KEY is set and a custom CODEAF_BASE_URL points away from OpenRouter (for example a local Ollama);
    • the CodeAF profile has a saved api_key or model_sources.
  • When unsure, the check lets the call through and CodeAF decides. That covers:
    • an unreadable or malformed profile;
    • credentials.json or codex.json present (connected providers and sign-ins);
    • a profile location that can't be pinned down.
  • Unchanged: legacy AFORGE_BIN pins, and every call that does have a key.

Behaviour change: a keyless CodeAF call used to return an is_error result; it now raises or returns the unavailable-provider error. That is the same way a missing CodeAF binary already behaves.

Validation contract

Behaviour
K1 No usable key → the not-configured error, before any resolve, download or launch, also with an explicit binary path
K2 The first refusal per process logs the warning once
K3 With a key, the command, child environment and result are unchanged
K4 The check follows CodeAF's key rules against the effective child environment, and allows the call when unsure
K5 Legacy aforge pins are untouched
K6 An Agent whose harness config resolves to CodeAF warns once at construction and never fails
K7 Python, Go and TypeScript share one message, one warning and one case table (sdk/codeaf-auth-contract.json, 42 cases)

Test plan

  • Gates (CI's commands, Node 20):
    • Python: ruff check . (0.15.22), mypy, ./scripts/run_pytest.sh (2864 passed on the first four commits; the five CodeAF test files re-run after the last one, 229 passed)
    • Go: go mod tidy, go build, go vet, go test ./...
    • TypeScript: npm ci, npm run lint, npm test (1332 passed), plus the harness doctor test
  • Hand test of each SDK's public API with the real CodeAF v0.8.2 binary. A wrapper recorded every CodeAF launch, and a local listener captured CodeAF's telemetry.
Calls with no key Call with a per-call key
main, Python returned codeaf exit code 1 (stop=) and launched CodeAF each time → 2 stop_reason: error sessions ok
main, Go the same: 2 launches, 2 error sessions ok
branch, Python HarnessProviderNotConfigured with the message above, 0 launches, 0 telemetry, one warning (plus one at Agent construction) ok, 1 launch, done
branch, Go ProviderUnavailableError (errors.As true, MissingAuthEnv=[OPENROUTER_API_KEY]), 0 launches, one warning (plus one at agent.New) ok, done
branch, TypeScript HarnessProviderNotConfigured (instanceof HarnessProviderUnavailable true), 0 launches, one warning (plus one at construction) ok, done
  • Zero-config Python run with no CODEAF_BIN. Keyless calls refused without downloading anything. The keyed call then downloaded the managed CodeAF and ran.
  • With OPENROUTER_API_KEY in the process environment: no warnings, and every call runs as before.

🤖 Generated with Claude Code

AbirAbbas and others added 5 commits October 26, 2026 17:03
…del key

CodeAF is the default harness provider, and with no model key it used to be
downloaded and launched anyway, exit 1, and come back as a generic
"codeaf exit code 1 (stop=)" result. Each of those launches was also counted
by CodeAF's own usage telemetry as a failed session.

The provider now checks, before resolving, downloading or launching CodeAF,
whether CodeAF could find a key in the child's effective environment
(OPENROUTER_API_KEY, an OpenRouter-compatible or custom-endpoint
OPENAI_API_KEY, or a saved profile key or model source; unreadable profiles
and connected-provider files are allowed). With none it raises
HarnessProviderNotConfigured, a HarnessProviderUnavailable subclass, naming
OPENROUTER_API_KEY and how to pick another provider, and logs one warning per
process. An Agent built with a harness config that resolves to CodeAF warns
once at construction. Legacy aforge pins are unchanged.

sdk/codeaf-auth-contract.json is the case table the Python, Go and
TypeScript suites share.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Go provider runs the same credential check as the Python SDK before
resolving, downloading or launching CodeAF. With no usable key Execute
returns a ProviderUnavailableError with NotConfigured set, which prints the
shared refusal message and still matches errors.As on the existing type;
MissingAuthEnv lists OPENROUTER_API_KEY. The first refusal logs one warning,
and agent.New warns once when its HarnessConfig resolves to CodeAF with no
key visible.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o model key

The TypeScript provider runs the shared credential check before resolving,
downloading or launching CodeAF and throws HarnessProviderNotConfigured, an
exported HarnessProviderUnavailable subclass with the shared message and
missingAuthEnv ["OPENROUTER_API_KEY"]. The first refusal warns once, and an
Agent whose harnessConfig resolves to CodeAF warns once at construction.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The key check guessed ~/.codeaf when every CODEAF_*/AFORGE_* home and profile
variable was empty, but CodeAF itself may then use HOME from the child's own
environment, a legacy ~/.aforge or a relative directory, and it reads a
whitespace-only spelling differently from an unset one. In those cases the
SDK now lets the call through and CodeAF decides, instead of refusing a setup
that could work. Python also stops accepting NaN/Infinity in config.json, so a
malformed profile is allowed the same way in all three SDKs, and the
Agent-construction warning can no longer raise.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AbirAbbas
AbirAbbas requested a review from a team as a code owner October 26, 2026 21:10
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Performance

SDK Memory Δ Latency Δ Tests Status
Python 9.1 KB - 0.24 µs -31% ✓ ✓
Go 225 B -20% 0.62 µs -38% ✓ ✓
TS 448 B +28% 2.19 µs +9% ✓ ⚠

⚠ Regression detected:

  • TypeScript memory: 350 B → 448 B (+28%)

…ield skill

The embedded skill copy under control-plane/internal/skillkit/skill_data must
stay byte-identical to skills/ (scripts/sync-embedded-skills.sh --check), and
only the embedded copy had the new paragraph.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 88.20% 87.40% ↑ +0.80 pp 🟡
sdk-go 93.70% 92.00% ↑ +1.70 pp 🟢
sdk-python 94.72% 93.73% ↑ +0.99 pp 🟢
sdk-typescript 92.50% 90.42% ↑ +2.08 pp 🟢
web-ui 84.90% 84.79% ↑ +0.11 pp 🟡
aggregate 86.12% 85.75% ↑ +0.37 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

Copy link
Copy Markdown
Contributor

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 0 — ➖ no changes
sdk-go 90 100.00% ✅
sdk-python 0 — ➖ no changes
sdk-typescript 49 100.00% ✅
web-ui 0 — ➖ no changes

✅ Patch gate passed

Every surface whose lines were touched by this PR has patch coverage at or above the threshold.

@AbirAbbas
AbirAbbas merged commit 27bc3a1 into main Oct 26, 2026
43 checks passed
@AbirAbbas
AbirAbbas deleted the fix/sdk-codeaf-missing-key branch October 26, 2026 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant