Skip to content

[fix] Refuse a malformed harness kind with a structured error at commit and invoke (F4) - #6409

Merged
mmabrouk merged 2 commits into
release/v0.114.4from
fix/harness-kind-structured-refusal
Aug 31, 2026
Merged

[fix] Refuse a malformed harness kind with a structured error at commit and invoke (F4)#6409
mmabrouk merged 2 commits into
release/v0.114.4from
fix/harness-kind-structured-refusal

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

The defect

Found live by the gate's new bad-harness cell (H1). A malformed harness.kind (12345, "not_a_real_harness") was ACCEPTED by the commit API with a 200, which persisted an agent config that can never run. Invoking that revision then died on an unhandled HTTP 500 whose body was the bare Python repr: '12345' is not a valid HarnessKind. The caller learned neither which field was wrong nor which values exist, and got no code to branch on.

The trace: HarnessKind.coerce (sdks/python/agenta/sdk/agents/dtos.py) did str(value).lower() and then cls(normalized), so a non-member raised the enum's plain ValueError. Nothing above make_harness (sdks/python/agenta/sdk/agents/adapters/harnesses.py:158) caught it, and handle_invoke_failure maps an unclassified exception to 500 with a stack trace. Nothing at the commit boundary looked at the field at all.

The core invariant did hold throughout: no turn ran, no rows were stored, and the harness was never silently defaulted to Pi. This changes the refusal SHAPE and stops an unrunnable config from being stored.

The fix, at both boundaries

Commitapi/oss/src/core/workflows/service.py. A new _reject_unreadable_harness_kind runs on the CANDIDATE revision inside commit_workflow_revision_checked, which is the one point both commit forms pass through: a delta has already been merged onto the head by then, and a full-data commit is the data as sent. It raises the domain exception InvalidAgentHarnessError, which carries the agent-actionable envelope (code, message, retryable: false, next_step, details with the field, the value, and the allowed harnesses). The route maps it to 422, the same status every other "your change is not committable" answer on that route uses, and the agent's own commit tool returns the identical envelope as an AgentError rather than raising.

The check is deliberately narrow. It reads one field, only when the commit carries it, so a workflow that is not an agent takes exactly its old path.

Invokesdks/python/agenta/sdk/agents/dtos.py. HarnessKind.coerce now raises InvalidHarnessKindError, an ErrorStatus with code 400 and type ...#v0:agent:invalid-harness-kind, whose message names the field, the value with its type, and every harness that exists. AgentTemplate.from_params validates a present kind where the template is read, which is before the handler selects a backend or resolves anything, so a config persisted before this fix now fails closed with a code instead of a 500.

Two deliberate choices worth a reviewer's eye:

  • The error is also a ValueError. The enum has always raised one there and callers (including an existing SDK test) guard on that type, so inheriting both keeps those guards working while the middleware renders the coded status. A dedicated ErrorStatus was chosen over the agent_run_failed code the brief suggested: this is a malformed request, not a run that failed, and it matches the neighbouring AgentTemplateShapeError in the same file, which is also a coded 400. Either satisfies the H1 cell, which accepts any 4xx or coded frame whose text names the harness.
  • I did not wrap the make_harness call site. With coerce typed, the only remaining bare ValueError there is the unreachable branch for an enum member with no adapter class, which is our bug, not the caller's. Relabelling it as a 400 would blame the caller for a server defect, so it stays a 500.

An absent, null, or blank kind still means "use the default" at both boundaries, unchanged, because that is what every config that never set a harness relies on. A legacy pi_agenta revision still reads as plain Pi.

Tests

Four files, at each boundary:

  • sdks/python/oss/tests/pytest/unit/agents/test_dtos_harness_kind_refusal.py (new, 26 cases): coerce refuses 12345, an unknown string, 0, [], {}; the message names the value and every harness; it is still a ValueError; readable kinds and pi_agenta are unchanged; absent/null/blank still default; the invoke remap answers 400 with the field, and the bare ValueError it replaced is pinned as the 500 it used to be.
  • sdks/python/oss/tests/pytest/unit/agents/test_agent_composition_seam.py: a pre-persisted bad config driven through the real handler refuses with the coded error, and no session is created, so no turn can be stored.
  • services/oss/tests/pytest/unit/agent/test_template_shape_validation.py: the same refusal through the deployed agent service handler.
  • api/oss/tests/pytest/unit/workflows/test_commit_harness_validation.py (new, 24 cases) and test_commit_endpoint.py: the values refused, the far larger set of commits left alone (no agent, no harness, no kind, prompt-only), the envelope's contents, the 422 mapping, and the DAO never being awaited, which is the "nothing is persisted" proof.

Results: API unit suite 2872 passed, SDK unit suite 2512 passed, services unit suite 162 passed. ruff format and ruff check clean with ruff@0.15.12, the version CI pins. One pre-existing failure, sdks .../agents/test_streaming.py::test_cli_stream_terminal_only_on_empty_request, fails identically with every change in this branch stashed, so it is not from this work.

Both new suites were verified to fail without the fix: removing the commit-path call makes the "nothing is persisted" case fail, and the SDK cases cannot even import without the new error type.

https://claude.ai/code/session_0165tsjmvf3qvTPFcb9EV44g

A harness.kind the runtime cannot read (a number, an unknown string) was accepted by
the commit API with a 200, which persisted a config that could never run. Invoking it
then died on the enum's bare ValueError as an unhandled HTTP 500 whose body was the
Python repr, naming neither the field nor the values a caller could have sent.

Both boundaries now refuse it with a shape. HarnessKind.coerce raises a coded 400 that
names the field, the value, and every harness that exists, so the invoke path fails
closed with an error a client can act on; it stays a ValueError so existing guards
keep working. The workflow commit path checks the candidate revision and refuses to
store an unrunnable agent config, answering 422 with the agent-actionable envelope on
the route and the same envelope inside the agent's own commit tool.

An absent, null, or blank kind still means "use the default", unchanged.

Claude-Session: https://claude.ai/code/session_0165tsjmvf3qvTPFcb9EV44g
@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview Aug 31, 2026 8:24pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Team

Run ID: 3a84598f-8b1b-437e-be81-c4ac2ef51841

📥 Commits

Reviewing files that changed from the base of the PR and between 7a68c2b and 6902b87.

📒 Files selected for processing (6)
  • api/oss/src/core/workflows/service.py
  • api/oss/tests/pytest/unit/workflows/test_commit_endpoint.py
  • api/oss/tests/pytest/unit/workflows/test_commit_harness_validation.py
  • sdks/python/agenta/sdk/agents/dtos.py
  • sdks/python/oss/tests/pytest/unit/agents/test_agent_composition_seam.py
  • sdks/python/oss/tests/pytest/unit/agents/test_dtos_harness_kind_refusal.py

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Invalid or unreadable agent harness kinds are rejected before configuration commits are saved.
    • Structured validation errors identify the invalid field and allowed values.
    • Invalid harness configurations now return appropriate 400 or 422 responses instead of unclassified server errors.
    • Missing, blank, supported, legacy, and enum-based harness values continue to work correctly.
    • Invalid values are safely represented in error responses, including non-finite numeric values.
  • Tests

    • Added coverage for validation, error responses, supported values, and prevention of invalid configuration persistence.

Walkthrough

The SDK now rejects unreadable harness.kind values with a typed 400 error. Workflow commits reject the same values before persistence. API and platform handlers return structured errors. Tests cover validation, defaults, legacy values, enum inputs, and response serialization.

Changes

Harness kind validation

Layer / File(s) Summary
SDK harness validation
sdks/python/agenta/sdk/agents/dtos.py, sdks/python/agenta/sdk/agents/__init__.py, sdks/python/oss/tests/pytest/unit/agents/*, services/oss/tests/pytest/unit/agent/test_template_shape_validation.py
InvalidHarnessKindError is added as a coded ValueError. HarnessKind.coerce and AgentTemplate.from_params reject unreadable explicit values while preserving defaults, legacy values, and enum inputs. Tests cover parsing, invocation, and error mapping.
Workflow commit validation
api/oss/src/core/workflows/service.py, api/oss/tests/pytest/unit/workflows/test_commit_harness_validation.py
Workflow commits validate parameters.agent.harness.kind on the built candidate before comparison and DAO persistence. Error details use JSON-safe echoed values. Tests cover accepted values, rejected values, serialization, and persistence prevention.
Commit error propagation
api/oss/src/apis/fastapi/workflows/router.py, api/oss/src/core/tools/platform_handlers.py, api/oss/tests/pytest/unit/workflows/test_commit_endpoint.py
The FastAPI router returns HTTP 422 responses. The platform handler returns AgentError failure results. Endpoint tests verify structured error responses for ordinary and non-finite invalid values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🟡 Moderate · up to 7a68c

Malformed nonblank harness values now receive structured errors before persistence or invocation, but whitespace-only values can still be committed and later fail during execution, leaving the workflow unavailable. Merge should wait for this validation contract mismatch to be fixed or explicitly accepted.

Sequence Diagram(s)

sequenceDiagram
  participant AgentTemplate
  participant HarnessKind_coerce
  participant commit_workflow_revision_checked
  participant commit_revision
  participant FastAPI
  AgentTemplate->>HarnessKind_coerce: validate explicit harness.kind
  HarnessKind_coerce-->>AgentTemplate: InvalidHarnessKindError for invalid values
  commit_workflow_revision_checked->>HarnessKind_coerce: validate candidate harness.kind
  HarnessKind_coerce-->>commit_workflow_revision_checked: validation result
  commit_workflow_revision_checked->>commit_revision: persist valid candidate
  commit_workflow_revision_checked-->>FastAPI: InvalidAgentHarnessError
  FastAPI-->>FastAPI: return structured HTTP 422
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 40 functions across 10 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: malformed harness kinds are refused with structured errors at commit and invoke boundaries.
Description check ✅ Passed The description directly explains the malformed harness.kind defect, the commit and invoke fixes, compatibility behavior, and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/harness-kind-structured-refusal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Team

Run ID: 1417e198-e5b5-4c4b-bcfd-250b073202db

📥 Commits

Reviewing files that changed from the base of the PR and between 92aa86e and 7a68c2b.

📒 Files selected for processing (10)
  • api/oss/src/apis/fastapi/workflows/router.py
  • api/oss/src/core/tools/platform_handlers.py
  • api/oss/src/core/workflows/service.py
  • api/oss/tests/pytest/unit/workflows/test_commit_endpoint.py
  • api/oss/tests/pytest/unit/workflows/test_commit_harness_validation.py
  • sdks/python/agenta/sdk/agents/__init__.py
  • sdks/python/agenta/sdk/agents/dtos.py
  • sdks/python/oss/tests/pytest/unit/agents/test_agent_composition_seam.py
  • sdks/python/oss/tests/pytest/unit/agents/test_dtos_harness_kind_refusal.py
  • services/oss/tests/pytest/unit/agent/test_template_shape_validation.py

Included review availability: Your plan provides up to 8 included reviews per hour; 2 remain after this review.

}


class InvalidAgentHarnessError(Exception):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Move InvalidAgentHarnessError to the workflow type module.

InvalidAgentHarnessError is a workflow-domain exception that FastAPI and platform layers import. Define it in api/oss/src/core/workflows/types.py or api/oss/src/core/workflows/dtos.py, then import it into the service and boundaries.

As per coding guidelines, “Define domain exceptions in the core layer (core/{domain}/types.py or core/{domain}/dtos.py).”

Source: Coding guidelines

…kind a 422

Two review findings, both reproduced before fixing.

Parsing a template stringified the caller's value after coercing it, so the SDK's own
HarnessKind member became "harnesskind.claude" (str() on a str Enum gives the member
repr, not the value) and was then refused by make_harness. A member now resolves to its
wire value, while a string still keeps its stored spelling so a legacy pi_agenta
revision normalizes exactly where it always did.

The commit refusal echoed the offending value verbatim, and Python's json parser
accepts the non-standard NaN and Infinity literals in a request body while Starlette
serializes with allow_nan=False. A caller sending one was refused correctly and then
got a 500 raised inside the response, which is the failure this boundary exists to
replace. Non-finite floats are now echoed as text.

Claude-Session: https://claude.ai/code/session_0165tsjmvf3qvTPFcb9EV44g
@mmabrouk

Copy link
Copy Markdown
Member Author

Both P2s reproduced against the source, and both are fixed in 6902b8713c.

1. The coerced member was discarded — real, and worse than the report

Confirmed exactly as described. HarnessKind is a str Enum, so str(HarnessKind.CLAUDE) is 'HarnessKind.CLAUDE', not 'claude'. Measured before the fix:

str(enum)                        -> 'HarnessKind.CLAUDE'
from_params({'kind': ENUM}).harness -> 'harnesskind.claude'
make_harness(that)               -> InvalidHarnessKindError: invalid harness.kind (str) 'harnesskind.claude'

So the SDK's own enum was not accepted by the SDK's own parser. The mangling predates this PR (the old line stringified the same way), but this PR made it worse in one specific sense: it used to die at make_harness as a bare ValueError and a 500, and now it produces a confident, wrong 400 telling the caller that a perfectly valid HarnessKind.CLAUDE is not a harness.

Fixed, but not by using resolved.value unconditionally — that would have broken the legacy case this PR deliberately preserves. coerce maps pi_agenta to PI, so a blanket .value would collapse a stored pi_agenta revision to pi_core at parse time and shift the harness identity a stored revision carries (the thing #6399 and #6364 made load-bearing). The rule is now explicit: a member has no spelling to keep, so it becomes its wire value; a string keeps its stored spelling. The existing pi_agenta pinning test still passes unchanged.

Tests: every HarnessKind member through from_params (parametrized over the enum, so a fourth harness is covered for free), a round-trip asserting coerce(from_params(member)) is member — the hop that actually broke — and an end-to-end run through the real handler in the composition seam asserting the backend receives the config, which a mangled value never reached.

2. Non-finite floats broke the response — real

Confirmed in both halves:

json.loads('{"kind": NaN}')['kind']     -> nan        # the non-standard literal is accepted
JSONResponse({'detail': {'value': nan}}) -> ValueError: Out of range float values are not JSON compliant

And end to end through the real code, before the fix: _reject_unreadable_harness_kind refused NaN correctly, to_detail() echoed it as a raw float, and serializing that envelope raised — turning the intended 422 into the 500 this boundary exists to replace. float('inf') and float('-inf') behave identically.

Fixed with a _json_safe_echo helper on the echo path: a non-finite float is echoed as text ("nan", "inf", "-inf"), everything else is unchanged. The fix sits in to_detail(), so it covers the router's 422 and the agent commit tool's AgentError envelope in one place. The invoke side was checked and needs nothing: handle_invoke_failure serializes only type, code, message, and stacktrace, and the message is already a string.

Tests: NaN/Infinity/-Infinity rows at the validator asserting both the echoed text and that the whole envelope survives JSONResponse, plus NaN/Infinity rows at the router boundary asserting a 422 and a serializable detail. Ordinary values are pinned as echoed unchanged, so the sanitizer cannot quietly stringify everything.

Verification

Both fixes were reverted in place and the suites re-run: 5 failures, exactly the new cases (3 enum/SDK, 2 non-finite router), and nothing else. Restored, all green.

Full suites: API 2880 passed, SDK 2517 passed, services 162 passed. ruff format and ruff check clean under ruff@0.15.12. The one SDK failure, agents/test_streaming.py::test_cli_stream_terminal_only_on_empty_request, is pre-existing and fails identically with every change on this branch stashed.

https://claude.ai/code/session_0165tsjmvf3qvTPFcb9EV44g

@mmabrouk
mmabrouk merged commit 3ece545 into release/v0.114.4 Aug 31, 2026
35 of 37 checks passed
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