Skip to content

Coerce AgentPolicyConfig fields to string (fixes #169) - #170

Open
Amresh-01 wants to merge 27 commits into
robocurve:mainfrom
Amresh-01:fix/agent-config-coercion
Open

Coerce AgentPolicyConfig fields to string (fixes #169)#170
Amresh-01 wants to merge 27 commits into
robocurve:mainfrom
Amresh-01:fix/agent-config-coercion

Conversation

@Amresh-01

@Amresh-01 Amresh-01 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #169

Description

This PR fixes #169 by strictly rejecting non-string parameters (model, base_url, api_key_env, effort, speed) in the LLMAgentPolicy constructor and raising a guided ConfigError.

This resolves the issue where unquoted CLI options parsed as booleans or integers (e.g. -P api_key_env=false or -P model=42) either recorded non-string values verbatim in the evaluation log or caused downstream tracebacks. Users are now guided to quote these values.

Changes

  1. Parameter Validation in LLMAgentPolicy:
    • Replaced parameter coercion logic with a validation loop that raises a guided ConfigError when non-strings are passed to model, base_url, api_key_env, effort, or speed.
  2. Robust Rerun-SDK Install Check:
    • Updated _RERUN_INSTALLED in test_rerun_sink.py to attempt to import rerun instead of find_spec, preventing false positives in environments with DLL load policies.
  3. Changelog:
    • Updated CHANGELOG.md entry under "Fixed" to reflect the strict validation change, wrapped at 80 columns.
  4. Tests:
    • Updated test_non_string_param_coercion to run parameterized checks for both truthy and falsy non-string parameters across all five fields, asserting that the proper guided ConfigError is raised.
    • Updated parameterization for test_falsy_api_key_env_does_not_send_the_openrouter_key_to_a_gateway in test_anthropic.py to only pass None and "" since non-strings are now caught and rejected at construction.

Verification

  • Synced the workspace package version locally.
  • Ran uv run ruff check plugins/inspect-robots-agent (all passed).
  • Ran uv run python -m pytest plugins/inspect-robots-agent/tests (396/396 passed).
  • Ran uv run python -m pytest tests/test_rerun_sink.py (55/55 passed).

@Amresh-01
Amresh-01 force-pushed the fix/agent-config-coercion branch 5 times, most recently from f97aa40 to d8029a4 Compare July 25, 2026 16:13

@jeqcho jeqcho left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The fix is right, and I checked it end to end rather than by eye. On main, -P api_key_env=0 records 0 in the log and -P model=42 dies with AttributeError: 'int' object has no attribute 'partition' at _llm.py:111. On this branch every non-None case records as str and None passes through untouched. Your new test fails on main with that same AttributeError, so it's a real regression test and not just a shape assertion.

The str(x) if x else "" looks like a slip but it's load-bearing, and I'd put a line of comment on it. Coercing False to "False" would flip the truthiness that not api_key_env and api_key_env or _OPENROUTER_KEY depend on, and the lookup would go hunting for an env var literally named False. Falsy to "" keeps the OpenRouter guard and the ANTHROPIC_API_KEY fallback working, which I confirmed. Without a comment someone will simplify it to str(x) later and quietly break that.

Two things before this can go in.

The description doesn't match the diff. It says you corrected a build_toolset call signature inside bind() and added a missing state_labels() helper to Toolset in _tools.py. Neither is in the diff, and both already exist on main, from #110 and #134. The test counts don't line up either: the agent plugin suite is 264 tests, not 35, and there's no 398-test workspace run. The three files you actually touched are fine, so I think the body got pasted in from somewhere else. Please rewrite it to describe the real change, since it lands in merge history.

CHANGELOG entry under Unreleased, per CONTRIBUTING.md:131. This changes what the public LLMAgentPolicy constructor writes into the eval log, so it counts as user-visible.

Minor: test_policy_e2e.py:1010 sets env={"False": "test-key"}, which is never looked up, since False coerces to "" rather than "False". It reads as though the opposite were true. A plain dummy env would be clearer.

One more: the tests/test_rerun_sink.py change is unrelated to #169. It is correct, and I ran that file both with and without rerun installed, so I don't mind keeping it here. Just say so in the description.

@Amresh-01
Amresh-01 requested a review from jeqcho July 26, 2026 08:16
@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho, Please review this PR

@jeqcho

jeqcho commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

CHANGELOG entry looks right, and good call spelling out the falsy-to-"" reasoning in it.

Still need the description sorted before I merge. The build_toolset signature and state_labels() bullets, and the 35/398 test counts, none of that is in your diff and both of those helpers already exist on main. Once the body describes the four files you actually changed, this is good to go.

@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho,
The description now reflects the actual changes in the PR, including the LLMAgentPolicy coercion, falsy-to-"" behavior, regression test, CHANGELOG entry, and the unrelated rerun import robustness improvement.

Thanks for catching this. The remaining CI checks are now running.

@jeqcho

jeqcho commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Heads up: main's history was rewritten today to purge 3 GB of accidentally committed eval logs (#212), and this branch's history includes the old rewritten commits (it looks like main was merged into the branch at some point). Rebasing it is not mechanical because the original commits conflict with the current AgentPolicyConfig code, so those conflicts are yours to resolve. Suggested path:

git fetch upstream            # robocurve/inspect-robots
git rebase --onto upstream/main <your first commit>^ fix/agent-config-coercion
# resolve conflicts in policy.py and test_policy_e2e.py, then
git push --force-with-lease

Happy to review as soon as it is rebased.

@jeqcho jeqcho left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for taking this on — the coercion lands in the right place (before all the validation in LLMAgentPolicy.__init__, so validators see the coerced values), it covers all five unvalidated str | None params including speed, CI is green across every platform, and the new test passes locally for me too. A few things need attention before this is mergeable, though.

1. The falsy → "" special case is inconsistent with its own rationale (plugins/inspect-robots-agent/src/inspect_robots_agent/policy.py:258-271)

The comment justifies str(x) if x else "" by saying api_key_env=False coerced to "False" would look up an env var literally named False and bypass the key fallbacks. But the same logic applies to truthy values, and those are not protected: -P api_key_env=true still coerces to "True" and produces a lookup for an env var literally named True (verified — policy.config.api_key_env == "True"). So the special case only fixes half of the problem it describes, while introducing new inconsistencies:

  • -P model=0"" → falls into the "no model configured" unset path, while -P model=1"1". Same kind of typo, opposite outcomes, and the user's value is silently discarded in the falsy case.
  • -P effort=false now raises ConfigError: effort must be one of [...], got '' — the user typed false but the error shows '', which makes the message harder to act on than got 'False' would be.

I'd suggest one of two directions:

  • Uniform str(x) with no falsy special case, letting the existing validation and provider resolution fail loudly with the actual value in the message; or
  • Reject non-strings with a guided ConfigError, which is what this same constructor already does for exactly this failure mode elsewhere — see prior_learnings (policy.py:277-283, whose message even explains "the -P parser coerces unquoted values; pass -P 'prior_learnings="..."'") and the explicit isinstance(..., bool) guards on max_output_tokens and image_horizon. Issue #169 offered "narrowing at the constructor" or widening the annotation; the fail-fast spelling matches the repo's established style (cf. the #154 changelog entry) and I'd lean toward it — either way the truthy/falsy asymmetry should go.

2. The PR description is stale after the rebase

The body describes a build_toolset signature fix, a state_labels() helper in _tools.py, and timeline-transcript lint cleanups — none of these are in the current diff; they already exist on main. Please update the description to match what the PR actually changes now (coercion + test, rerun import check, CHANGELOG), and refresh the verification numbers if they were from the old branch state.

3. tests/test_rerun_sink.py change is unrelated to #169

The find_spec → try/import rerun switch for _RERUN_INSTALLED (tests/test_rerun_sink.py:34-39) is a reasonable robustness improvement on its own, but it has nothing to do with config coercion — it would be cleaner as its own small PR with its motivation stated there. If it stays, one note: pytest.importorskip("rerun") at the old line 1035 already skips on any import failure, so replacing it (tests/test_rerun_sink.py:1039-1042) isn't strictly needed — only the module-level flag change is load-bearing. Keeping the flag as the single source of truth is defensible, so this part is your call, but please say so explicitly in the description.

4. CHANGELOG (CHANGELOG.md:208)

  • The entry is one long unwrapped line; the surrounding entries wrap at roughly 80 columns — please match that.
  • The sentence "Falsy values are coerced to "" to preserve API key and OpenRouter fallback logic" documents the behavior questioned in item 1; it will need rewording once that's settled.

5. Test coverage (plugins/inspect-robots-agent/tests/test_policy_e2e.py:2524-2538)

test_non_string_param_coercion is a good direct check. Once item 1 is resolved, please also cover the currently-untested asymmetry cases: a truthy non-string api_key_env (e.g. True) and a falsy non-string model (e.g. 0), asserting whichever behavior you land on.

Happy to take another look after these — the core of the change is close, it's mainly the falsy special case and the housekeeping around the bundling that need tightening. Thanks again for the contribution! 🙏

@Amresh-01
Amresh-01 force-pushed the fix/agent-config-coercion branch from 5ee4371 to 67726a1 Compare July 30, 2026 02:04
@Amresh-01
Amresh-01 requested a review from jeqcho July 30, 2026 02:21
@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho,

I've addressed the latest review comments:

  • Switched to strict ConfigError validation for non-string config options.
  • Updated the tests to cover the requested cases.
  • Cleaned up the PR description and related changes.
  • Fixed lint issues and all CI checks are now passing.

Whenever you have time, I'd really appreciate another review. Thanks again for all the detailed feedback!

@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho please review this PR

@jeqcho jeqcho left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the quick turnaround on this, @Amresh-01 — this round lands it. Going with strict ConfigError rejection instead of coercion resolved the truthy/falsy asymmetry cleanly, the error message matches the prior_learnings house style, and the new test covers all five fields on both sides of truthiness, including the api_key_env=True and model=0 cases from the last review. The CHANGELOG entry is reworded and wrapped, the description now matches the diff, and the rerun check has its own explanation. I also appreciate you handling the post-#212 rebase — CI is green across the board.

Two tiny non-blocking nits, no need for another round unless you feel like it:

  1. The test name test_non_string_param_coercion (and the PR title) still say "coerce", but the behavior is now rejection — test_non_string_params_rejected would read truer.
  2. The comment above test_falsy_api_key_env_does_not_send_the_openrouter_key_to_a_gateway (test_anthropic.py:1103-1106) still mentions 'false'/'0' being treated as unset; those now raise at construction, so the comment can shrink to the '' case.

Thanks again for sticking with this through the review rounds and the history rewrite — much appreciated! I'll retitle at squash time so the merge commit reflects the rejection behavior. Merging!

@jeqcho

jeqcho commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Quick follow-up, @Amresh-01 — sorry to bounce this once more. After I updated the branch against the latest main, the plugin · agent job went red: main recently gained an _Unset sentinel default for effort in policy.py, and the new strict validation loop now rejects it on every construction:

inspect_robots.errors.ConfigError: effort must be a string, got <inspect_robots_agent.policy._Unset object ...>

The fix should be small: in the validation loop, skip values that are the unset sentinel (they mean "not provided", not "wrong type") — e.g. guard with if value is _UNSET: continue (match whatever the sentinel instance is named on current main) before the isinstance(value, str) check, and add one test case asserting a config with everything left unset still constructs. The approval stands once CI is green again — ping me and I'll merge right away. Thanks for your patience! 🙏

@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho,

I've updated the branch with the fixes:

  • Added a guard if val is _UNSET: continue in the validation loop to skip the _UNSET sentinel values during configuration construction.
  • Added a new unit test test_policy_config_defaults() to assert that default configuration parameters (with unset options) construct successfully.
  • Renamed the test test_non_string_param_coercion to test_non_string_params_rejected to match the actual behavior.
    Cleaned up the comment above test_falsy_api_key_env_does_not_send_the_openrouter_key_to_a_gateway in test_anthropic.py to only refer to the empty string '' case now that non-string options are strictly rejected

@Amresh-01
Amresh-01 requested a review from jeqcho August 2, 2026 19:32
@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho please review this PR

@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho please review this PR

@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho Please Review this PR

@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho Please Review this PR

@jeqcho jeqcho left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @Amresh-01 — this lands the right design, and the execution is close. The core fix is exactly what #169 asked for: rejecting at the constructor (rather than coercing) matches the repo's existing pattern (the prior_learnings check a few lines below uses the same guided ConfigError with the identical fix: the -P parser coerces unquoted values phrasing), excluding effort is the correct call since _validated_effort already rejects bools/bad values with its own guided error, and the test_anthropic.py narrowing plus removal of the # type: ignore[arg-type] is a nice touch. CI is green across the board. A few things before merge:

1. Please drop the unrelated tests/test_rerun_sink.py change (_RERUN_INSTALLED via try: import rerun instead of find_spec, and replacing pytest.importorskip). It has nothing to do with #169, it makes every collection of that file eagerly import the rerun SDK where find_spec did not, and importorskip is the idiomatic pytest mechanism it replaces. If DLL-load false positives are a real problem in some environments, that's worth its own issue + PR where it can be discussed on its merits — this repo's convention is small, focused changes.

2. Dead branch in the new validation loop (plugins/inspect-robots-agent/src/inspect_robots_agent/policy.py, the new isinstance(val, _Unset): continue): none of the four checked params (model, base_url, api_key_env, speed) uses the _UNSET sentinel — they all default to None — so this branch is unreachable. Please remove it; it implies a sentinel contract these fields don't have.

3. test_policy_config_defaults (plugins/inspect-robots-agent/tests/test_policy_e2e.py): this asserts a private attribute (policy._pre_check) and is unrelated to the fix. Please remove it (or move it to a PR where it has a purpose).

4. Title and description are stale. The title says "Coerce AgentPolicyConfig fields to string" but the implementation (correctly) rejects instead of coercing — please retitle, since the title becomes the merge commit. The body also says five fields including effort are validated (it's four, with effort intentionally delegated to _validated_effort) and names the test test_non_string_param_coercion (it's test_non_string_params_rejected).

Minor, non-blocking: the new test uses a hand-rolled for loop where the rest of the file uses @pytest.mark.parametrize; parametrize would give per-case test IDs on failure.

The behavioral change itself checks out end to end: -P model=42 previously fell into the model or environ.get(ENV_MODEL) falsy-fallback trap and -P api_key_env=false was recorded verbatim in the eval log against a str | None annotation — both now fail fast with a guided message, consistent with the #168 convention that every construction check raises ConfigError. The CHANGELOG entry is correctly placed under Unreleased → Fixed. Happy to approve once the scope is trimmed and the title/body match the code.

@Amresh-01
Amresh-01 force-pushed the fix/agent-config-coercion branch from a4a2471 to b256e5e Compare August 9, 2026 05:39
@Amresh-01
Amresh-01 force-pushed the fix/agent-config-coercion branch from b256e5e to 75a451c Compare August 9, 2026 05:42
@Amresh-01
Amresh-01 requested a review from jeqcho August 9, 2026 05:46
@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho Have a look on this PR

@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho Have a look on this PR

@Amresh-01

Copy link
Copy Markdown
Contributor Author

Hi @jeqcho Please Review this PR

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.

AgentPolicyConfig records non-string -P values verbatim in the eval log

2 participants