Skip to content

feat: harness contract and plugin SPI (plan 1 of 3) - #181

Draft
uipreliga wants to merge 12 commits into
mainfrom
feat/harness-contract
Draft

uipreliga wants to merge 12 commits into
mainfrom
feat/harness-contract

Conversation

@uipreliga

@uipreliga uipreliga commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Plan 1 of 3 of the harness target architecture: every agent declares what it honors of the shared agent config, and a task that asks for something the chosen harness cannot do is rejected at resolution, so coder-eval plan fails before any run.

  • HarnessContract per agent class. Each agent declares which of system_prompt, plugins, permission_mode, allowed_tools and disallowed_tools it honors, the permission_mode values it honors, and whether it supports cooperative stop. Registration fails without a contract. The ClassVars supports_cooperative_stop, supports_cost_log_tags and system_prompt_semantics are gone.
  • A closed tool vocabulary. CANONICAL_TOOL_NAMES (14 names, checked against the pinned Claude Code CLI) and a per-harness ToolNameMap that must map every name. Names are checked at resolution, with a did-you-mean hint.
  • Tool restrictions honored natively. Pi: --tools / --no-tools / --exclude-tools. OpenCode: permission rules and an instructions file for the system prompt. Antigravity: SDK tool-call policies. permission_mode: plan means read-only on all three. The Codex tool forward did nothing (enabled_tools is an MCP-server key) and is deleted.
  • Per-kind defaults. An experiment's defaults.agent.by_type.<kind> applies only when the final agent kind matches, below the task. Claude-only defaults in experiments/default.yaml moved there.
  • Resolution-time check. validate_harness_contract raises HarnessContractError, a TaskResolutionError. plan, run and export stop on it. Every "set but NOT enforced" warning is deleted.
  • Plugin surface. coder_eval.spi with SPI_VERSION = 1. cost_log_tags is a base-constructor kwarg on Agent.
  • Guardrails. Lint rule CE068 keeps concrete agent kinds out of orchestration/, streaming/ and timing.py. make parity-table generates the contract and tool tables in docs/agents/HARNESS_PARITY.md, and CE069 fails on drift. tests/test_harness_conformance.py requires one offline probe per enforced cell and per declared permission mode, and checks that a system prompt never rides the user turn.
  • Bugs fixed from review. Pi and OpenCode admitted one turn over max_turns. Antigravity re-pulled the step stream on any RuntimeError, and swallowed a failed teardown silently. The early-stop ceiling could divide by zero.

Breaking changes: YAML that loads today is now rejected

  • A Codex task that sets permission_mode, allowed_tools or disallowed_tools.
  • permission_mode: acceptEdits or default on Pi, OpenCode or Antigravity (they honor plan and bypassPermissions). A task-level acceptEdits run with --type pi is rejected too; put it under by_type.claude-code.
  • A tool name outside the canonical set. Claude Code still accepts mcp__… names and rules such as Bash(git status:*).
  • plugins, system_prompt, permission_mode or a tool list on a type: none task.
  • experiments/default.yaml now applies model, permission_mode and allowed_tools to Claude Code only, so --type pi no longer inherits the Claude model id.
  • An empty allowed_tools: [] restricts nothing on every harness, as on Claude Code.

Before merge

  • Delegate (coder_eval_uipath): its agent fails registration until it declares a contract and accepts cost_log_tags. Land that change, and move permission_mode under by_type.claude-code in the RPA pipeline's shared experiment YAML, together with or before this PR.
  • One live run per harness with allowed_tools: [Read]: Pi and OpenCode (Haiku 4.5 via OpenRouter) and Antigravity (gemini-3.5-flash; it runs Gemini only) each wrote no file and scored 0, while an unrestricted control run on the same model passed on all three. An extra Antigravity run with only disallowed_tools: [Bash] scored 1.0 with no Bash call, so its policies are selective.

Open follow-ups (simulator safeguard, the acceptEdits owner question, deferred lint ideas) are tracked in c/harness-target-architecture-v2.md and .claude/harness-candidates.md.

Test plan

  • make verify: 5989 passed, 2 skipped, 93% coverage; ruff, pyright, custom lint (CE000+) and docs budget clean
  • coder-eval plan over every task: exit 0; plan -e over every shipped experiment: exit 0 except the intended with-plugin rejection on the agentless task
  • mkdocs build --strict; new anchors verified in the built HTML
  • Docker staging and Harbor export round-trip tests: a reloaded non-Claude task passes the contract check
  • Live smoke runs on Pi, OpenCode and Antigravity: restricted runs wrote nothing, unrestricted controls passed (see Before merge)

🤖 Generated with Claude Code

uipreliga and others added 11 commits September 16, 2026 07:50
…date registrations

Replace the supports_cooperative_stop and system_prompt_semantics ClassVars with
one frozen HarnessContract per agent class. AgentRegistry.register now rejects an
agent without a contract, and a config class that is not a forbid-extra
BaseAgentConfig whose type Literal names the kind. The base environment info
records the contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ructor

Agent.__init__ is now concrete and takes cost_log_tags; every in-tree agent
forwards it through super().__init__. The orchestrator forwards the tags on
every LiteLLM route, so the supports_cost_log_tags capability gate is deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and Antigravity

Pi maps allowed_tools / disallowed_tools / permission_mode plan to --tools,
--no-tools and --exclude-tools. OpenCode writes explicit permission rules and an
instructions file for system_prompt into OPENCODE_CONFIG_CONTENT and always
passes --auto. Antigravity builds SDK tool-call policies. Every map is the
inverse of the adapter's telemetry map. The Codex enabled_tools / disabled_tools
forward was a no-op (they are MCP-server keys) and is deleted with its task.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…on-time harness contract check

An experiment's defaults.agent may carry by_type.<kind>, applied below the task
and selected by the final agent kind, so Claude-only defaults move out of the
shared baseline. A gated agent field set on a harness whose contract marks it
unsupported now raises HarnessContractError, which plan, run and export treat as
a hard config error. The sdk_options guard is derived from the registry, a -D
system_prompt_file is inlined after layer 5, and every unenforced-field warning
is deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…map per harness

HarnessContract gains permission_modes, the permission_mode values a harness
honors with their Claude Code meaning. ToolNameMap maps every name in
CANONICAL_TOOL_NAMES to the harness's native tools (an empty tuple for a tool
the harness lacks), is validated at registration, and replaces the silent
.get(name, ()) reads. Pi and OpenCode smoke tasks move to bypassPermissions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mes a harness cannot honor

The resolution-time contract check now also rejects a permission_mode value
outside the harness's permission_modes and a tool-list name outside
CANONICAL_TOOL_NAMES, with a did-you-mean hint. Claude Code keeps its native
mcp__ names and permission rule syntax. Orchestrator setup runs the check for
library and in-container runs, but not for a re-grade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oder_eval.spi surface

CE068 flags a concrete agent config class or an AgentKind member (other than
UNKNOWN) named in orchestration/, streaming/ or timing.py. coder_eval.spi
re-exports the stable plugin surface with SPI_VERSION = 1, and EXTENDING.md
documents the harness contract a plugin agent must declare.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the harness conformance test

make parity-table renders the HarnessContract and ToolNameMap of every in-tree
agent into docs/agents/HARNESS_PARITY.md, and CE069 fails on drift. The
conformance test derives every rejection from the contracts and requires one
offline probe per enforced cell and declared permission mode, including a check
that a system prompt never rides the user turn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Docker staging and the Harbor export write only the agent fields a layer set,
  so a reloaded non-Claude task no longer claims the permission_mode default
  and fails the contract check in the container (round-trip tests added).
- An empty allowed_tools / disallowed_tools restricts nothing and is not a set field.
- OpenCode keeps a host rule for external_directory / doom_loop under an allowlist.
- coder_eval.spi exports CommandTelemetry, TokenUsage, TranscriptMessage,
  ResultSummary and ModelPricing, which a plugin needs to emit events and prices.
- Correct stale notes and docs: the Codex start() warning, the simulator's
  allowed_tools=[] claim, plan meaning, resolution vs load, and Pi's ls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eardown and the early-stop ceiling

- Pi and OpenCode stop at the (N+1)th turn_start / step_start before it is
  counted or emitted, so max_turns=N records N turns (it recorded N+1).
- Antigravity retries a receive_steps() RuntimeError only when no step was
  pulled yet; a later error is a real failure and is no longer re-pulled.
- A failed Antigravity harness teardown is logged instead of swallowed silently.
- The early-stop ceiling fails closed at zero armed weight instead of dividing
  by zero, matching armed_criteria_passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
subagent tool.
"""

TOOL_NAME_ALIASES: Final[dict[str, str]] = {"Task": "Agent"}
… task caps

Co-Authored-By: Claude <noreply@anthropic.com>
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.

2 participants