Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
430 changes: 277 additions & 153 deletions docs/developer-guide/harness-adapters.rst

Large diffs are not rendered by default.

81 changes: 57 additions & 24 deletions reef/harness/adapters/claude/descriptor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ env:
# only with nothing else on the command line, stops at "unknown option" after
# the --settings below. reef-claude update is reef's own install of the
# served release. Claude Code copies settings.env over its environment, so
# the quirks refuse a tree whose settings.env sets either switch; a project
# the checks below refuse a tree whose settings.env sets either switch; a project
# .claude/settings.json of the person's own can still set them.
client_env:
DISABLE_AUTOUPDATER: "1"
Expand Down Expand Up @@ -85,29 +85,62 @@ files:
# Covers a run where the person passes their own --settings, which
# replaces the one in client_args.
disableDeepLinkRegistration: "disable"
# Keys the merged config must keep at the defaults' value, and settings
# keys no tree may set at all: the shared render refuses a composition
# that moves them, before any quirks step runs. The locked settings
# choose the model, a credential helper or a login method, which Reef's
# model binding owns.
locked:
- includeCoAuthoredBy
- disableDeepLinkRegistration
- advisorModel
- apiKeyHelper
- availableModels
- awsAuthRefresh
- awsCredentialExport
- enforceAvailableModels
- fallbackModel
- forceLoginGatewayUrl
- forceLoginMethod
- gcpAuthRefresh
- model
- modelOverrides
- modelPicker
- proxyAuthHelper
- switchModelsOnFlag
checks:
- key: [includeCoAuthoredBy, disableDeepLinkRegistration]
match: IsDefault()
# Settings that choose the model, a credential helper or a login
# method, which Reef's model binding owns.
- key: [
advisorModel, apiKeyHelper, availableModels, awsAuthRefresh, awsCredentialExport,
enforceAvailableModels, fallbackModel, forceLoginGatewayUrl, forceLoginMethod, gcpAuthRefresh,
model, modelOverrides, modelPicker, proxyAuthHelper, switchModelsOnFlag,
]
match: IsAbsent()
reason: Reef's model binding chooses the model, the credential and the login method
# Claude Code copies settings.env over its environment. It reads the
Comment thread
yanxz marked this conversation as resolved.
# updater switches as on only for 1, true, yes or on, so a tree sets
# neither, under any name Windows matches without case.
- key: env
match: Each(Key(Not(Uppercase(IsIn(["DISABLE_AUTOUPDATER", "DISABLE_UPDATES"])))))
reason: the episode env and reef-claude set it so Claude Code keeps the pinned version
- key: [env.DISABLE_TELEMETRY, env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC]
match: Not(Strip(Lowercase(IsIn(["0", "false", "off", "no", ""]))))
reason: benchmark episodes keep telemetry and non-essential traffic off
# The env names Claude Code 2.1.257 reads to choose the endpoint, the
# provider, the credential or the model of a model call: every
# ANTHROPIC_ name, the cloud providers and their credentials, the
# provider switches, proxies, endpoints, sockets, credential helpers
# and files, and model names. The binding writes the first three, and
# the render refuses them from a tree without the binding's token.
- key: env
match: |-
Each(Key(AnyOf(
IsIn(["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "ANTHROPIC_MODEL"]),
Not(Uppercase(AnyOf(
StartsWith("ANTHROPIC_"), StartsWith("AWS_"), StartsWith("AZURE_"), StartsWith("GOOGLE_"),
StartsWith("GCLOUD_"), StartsWith("GCE_"), StartsWith("CLOUDSDK_"), StartsWith("CLOUD_ML_"),
StartsWith("VERTEX_"),
StartsWith("CLAUDE_CODE_USE_ANTHROPIC_"), StartsWith("CLAUDE_CODE_USE_BEDROCK"),
StartsWith("CLAUDE_CODE_USE_FOUNDRY"), StartsWith("CLAUDE_CODE_USE_GATEWAY"),
StartsWith("CLAUDE_CODE_USE_MANTLE"), StartsWith("CLAUDE_CODE_USE_VERTEX"),
StartsWith("CLAUDE_CODE_PROVIDER_"), AllOf(StartsWith("CLAUDE_CODE_SKIP_"), EndsWith("_AUTH")),
EndsWith("_MODEL"), HasSubstr("_MODEL_CATALOG"), HasSubstr("_MODEL_OPTION"), HasSubstr("_MODEL_FORCE"),
HasSubstr("PROXY"), HasSubstr("BASE_URL"), HasSubstr("ENDPOINT"), HasSubstr("SOCKET"),
HasSubstr("API_KEY"), HasSubstr("AUTH_"), HasSubstr("CREDS"), HasSubstr("CREDENTIAL"),
HasSubstr("HELPER"), HasSubstr("FILE_DESCRIPTOR")
)))
)))
reason: Reef's model binding chooses the endpoint, the credential and the model
# Claude Code merges the JSON object in CLAUDE_CODE_EXTRA_BODY into
# every request body, over the bound model, so the object names no
# model (model, or OpenRouter's fallback models).
- key: env
match: |-
Each(AnyOf(
Key(Not(Uppercase(Eq("CLAUDE_CODE_EXTRA_BODY")))),
Value(ParseJson(AllOf(IsObject(), Not(Contains(Key(IsIn(["model", "models"])))))))
))
reason: Reef's model binding chooses the model, so CLAUDE_CODE_EXTRA_BODY must be a JSON object without model or models
rules: "claude/CLAUDE.md"
agent_command: "claude/commands/{name}.md"
skill: "claude/skills/{name}/SKILL.md"
Expand Down
110 changes: 5 additions & 105 deletions reef/harness/adapters/claude/quirks.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,19 @@
"""Claude Code adapter quirks: boot artifacts and enforced config invariants.
"""Claude Code adapter quirks: boot artifacts and the model a command or skill frontmatter names.

Claude Code's boot writes local state beside the rendered config under
CLAUDE_CONFIG_DIR: a top-level ``.claude.json`` state file, a ``statsig/``
feature-gate cache, per-session ``todos/`` lists, and ``shell-snapshots/``.
The descriptor whitelists those so the episode inverse tolerates them and
reports anything else as residue.

``process_config`` enforces what the declaration cannot state. Claude Code
copies ``settings.env`` over its own environment. The episode env turns
telemetry and non-essential traffic off, so ``settings.env`` must not set
either to ``0``, ``false``, ``off``, ``no`` or an empty value. The episode
env and ``reef-claude`` set ``DISABLE_AUTOUPDATER`` and ``DISABLE_UPDATES``
so that Claude Code's updater and its own ``update`` and ``install`` keep
the pinned version; Claude Code reads them as on only for ``1``, ``true``,
``yes`` or ``on``, so ``settings.env`` must not set either one at all, under
any name Windows matches without case. The keys a settings.json must keep,
and the settings keys that choose the model, a credential helper or a login
method, are declared in the descriptor and refused by the shared render
before this step runs.

It also keeps every model call on Reef's model binding. The binding writes
``ANTHROPIC_BASE_URL``, ``ANTHROPIC_AUTH_TOKEN`` and ``ANTHROPIC_MODEL`` into
``settings.env``, renders after the tree and wins every name it writes. Its
token is a credential, which a tree cannot hold because admission refuses an
inline credential, so the three names pass only beside the binding's token.
Any other env name Claude Code reads to choose the endpoint (another
provider such as Bedrock or Vertex, a proxy), the credential or the model,
and a ``model`` in the frontmatter of a command or a skill are the
tree choosing where calls go, and are refused. Claude Code merges the JSON
object in ``CLAUDE_CODE_EXTRA_BODY`` into every request body, over the bound
model, so that object may not name a model (``model``, or OpenRouter's
fallback ``models``).

These checks cover the config the tree renders. Reef's proxy forwards the
``model`` and ``models`` a request sends as they are, so a request that other
code builds with the rendered key, such as a tool the model runs, can still
name another model.
Every model call stays on Reef's model binding, and a ``model`` in the
Comment thread
yanxz marked this conversation as resolved.
frontmatter of a command or a skill is the tree choosing where calls go.
``check_model_route`` refuses it, reading the frontmatter as Claude Code
reads it, which no config check can.
"""

from __future__ import annotations

import json
import re
from collections.abc import Mapping
from typing import Any
Expand All @@ -50,31 +23,6 @@
from reef.harness.adapters.descriptor import AdapterRenderer
from reef.harness.tree.render import RenderError

_CONFIG_PATH = "claude/settings.json"

# The env switches the descriptor relies on to keep episodes hermetic. A
# rendered settings.env that sets any of these to a falsey value would undo
# the descriptor's own env and let the binary phone home mid-campaign.
_HERMETIC_ENV = ("DISABLE_TELEMETRY", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
_FALSEY = {"0", "false", "off", "no", ""}

#: The env names Reef's model binding writes.
BINDING_ENV = ("ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "ANTHROPIC_MODEL")
#: The env names Claude Code 2.1.257 reads to choose the endpoint, the provider, the credential or the model of a
#: model call: every ANTHROPIC_ name, the cloud providers and their credentials, the provider switches, proxies,
#: endpoints, sockets, credential helpers and files, and model names. Matched without case, as Windows reads env.
MODEL_ROUTE_ENV = re.compile(
r"^(?:ANTHROPIC_|AWS_|AZURE_|GOOGLE_|GCLOUD_|GCE_|CLOUDSDK_|CLOUD_ML_|VERTEX_)"
r"|^CLAUDE_CODE_(?:USE_(?:ANTHROPIC_|BEDROCK|FOUNDRY|GATEWAY|MANTLE|VERTEX)|SKIP_\w*_AUTH$|PROVIDER_)"
r"|_MODEL$|_MODEL_(?:CATALOG|OPTION|FORCE)"
r"|PROXY|BASE_URL|ENDPOINT|SOCKET|API_KEY|AUTH_|CREDS|CREDENTIAL|HELPER|FILE_DESCRIPTOR",
re.IGNORECASE,
)
#: The env name whose JSON object Claude Code merges into every request body, after the bound model.
EXTRA_BODY_ENV = "CLAUDE_CODE_EXTRA_BODY"
#: Request body fields that name the model: ``model`` replaces the bound one, and OpenRouter reads ``models`` as
#: fallback models.
BODY_MODEL_KEYS = ("model", "models")
#: Claude Code's frontmatter block: after a byte order mark, an opening --- line, then the text up to the next ---.
FRONTMATTER_BLOCK = re.compile(r"---\s*\n([\s\S]*?)---\s*\n?")
#: A plain ``key: value`` line and the value characters Claude Code quotes when its YAML reader refuses the block.
Expand Down Expand Up @@ -139,59 +87,11 @@ def frontmatter_chooses_model(text: str) -> bool:
return "model" in block or "\\" in block


def extra_body_names_model(value: object) -> bool:
"""Whether a ``CLAUDE_CODE_EXTRA_BODY`` value can name a model; a value render cannot read as an object counts.

Claude Code strips a byte order mark and reads the value with JSON.parse.
"""
if not isinstance(value, str):
return True
try:
body = json.loads(value.removeprefix("\ufeff"))
except (ValueError, RecursionError):
return True
return not isinstance(body, dict) or any(key in body for key in BODY_MODEL_KEYS)


class ClaudeAdapterRenderer(AdapterRenderer):
@staticmethod
def process_config(path: str, config: dict[str, Any]) -> dict[str, Any]:
env = config.get("env")
if isinstance(env, dict):
# The episode env and reef-claude set the updater switches, and a tree value other than 1, true, yes or
# on would turn the updater back on. Windows matches env names in any case.
for key in env:
if key.upper() in ("DISABLE_AUTOUPDATER", "DISABLE_UPDATES"):
raise RenderError(
f"claude composition must not set {key} in settings.env: the episode env and reef-claude "
"set it so Claude Code keeps the pinned version"
)
for key in _HERMETIC_ENV:
if key in env and str(env[key]).strip().lower() in _FALSEY:
raise RenderError(f"claude composition must not re-enable {key} for benchmark episodes")
return config

@staticmethod
def check_model_route(
configs: Mapping[str, Mapping[str, Any]], skills: Mapping[str, str], commands: Mapping[str, str]
) -> None:
config = configs[_CONFIG_PATH]
env = config.get("env")
if isinstance(env, dict):
for name in env:
# The render refuses the binding's names from a tree without the binding's token.
if name in BINDING_ENV:
continue
if MODEL_ROUTE_ENV.search(name):
raise RenderError(
f"claude composition must not set env {name}: Reef's model binding chooses the endpoint, "
"the credential and the model"
)
if name.upper() == EXTRA_BODY_ENV and extra_body_names_model(env[name]):
raise RenderError(
f"claude composition env {name} must be a JSON object without model or models: "
"Reef's model binding chooses the model"
)
for path, text in {**skills, **commands}.items():
if frontmatter_chooses_model(text):
raise RenderError(
Expand Down
61 changes: 61 additions & 0 deletions reef/harness/adapters/codex/descriptor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,69 @@ files:
trace_exporter: none
sandbox_workspace_write:
network_access: false
# web_search may take any value Codex reads, because the episode argv
# pins it disabled and only a person's reef-codex session reads the
# tree's value.
checks:
# The episode argv pins never, and a reef-codex session keeps Codex's
# on-request default, so the person approves each command that leaves
# the sandbox.
- key: approval_policy
match: IsAbsent()
reason: episodes pin never, and a reef-codex session keeps on-request
# The config.toml keys a tree may set, and the keys Reef's model
# binding writes, which the render refuses from a tree without it.
# The entire file is treated as a dict.
- key: "."
match: |-
Each(Key(IsIn([
"analytics", "check_for_update_on_startup", "features", "feedback", "model_auto_compact_token_limit",
"model_auto_compact_token_limit_scope", "model_context_window", "model_reasoning_effort",
"model_reasoning_summary", "model_verbosity", "otel", "sandbox_workspace_write",
"tool_output_token_limit", "web_search",
"model", "model_provider", "model_providers"
])))
reason: the other codex config keys are not admitted for benchmark episodes
- key: web_search
match: IsIn(["disabled", "cached", "indexed", "live"])
- key: [
check_for_update_on_startup, analytics.enabled, feedback.enabled, otel,
sandbox_workspace_write.network_access,
features.apps, features.hooks, features.plugins, features.shell_snapshot,
features.skill_mcp_dependency_install,
]
match: IsDefault()
- key: features
match: |-
Each(Key(IsIn([
"apps", "enable_request_compression", "hooks", "plugins", "shell_snapshot",
"skill_mcp_dependency_install"
])))
- key: sandbox_workspace_write
match: Each(Key(IsIn(["network_access", "writable_roots"])))
- key: sandbox_workspace_write.writable_roots
match: AnyOf(IsAbsent(), Eq([]))
# The provider Reef's model binding writes, with only its keys.
- key: model_provider
match: AnyOf(IsAbsent(), Eq("reef"))
reason: Reef's model binding chooses the model provider
- key: model_providers
match: Each(Key(Eq("reef")))
reason: Reef's model binding chooses the model provider
- key: model_providers.*
match: |-
Each(Key(IsIn(["base_url", "experimental_bearer_token", "name", "supports_websockets", "wire_api"])))
reason: Reef's model binding chooses the model provider
models:
path: "codex/models.json"
# Model metadata by model name, which the quirks module turns into
# Codex's model catalog.
checks:
# The entire file is treated as a dict.
- key: "."
match: Each(Key(Eq("models")))
- key: models
match: AnyOf(IsAbsent(), IsObject())
rules: "codex/AGENTS.md"
# Codex 0.153.4 loads no custom prompts (its TUI refuses /name), so an
# agent_command renders as a skill the person types as $name. A skill and
Expand Down
Loading
Loading