fix(config): honor endpoint: alias in redteam/behavior override blocks - #257
fix(config): honor endpoint: alias in redteam/behavior override blocks#257nikhilpatidar wants to merge 2 commits into
Conversation
The shared target: block in nuguard.yaml accepts both endpoint: and
target_endpoint: as aliases for the same field (nuguard/config.py:199-203).
The nuguard.yaml.example file documents endpoint: as the canonical form
(line 47).
The redteam: override block (lines 233-234) only accepts target_endpoint:,
and the behavior: override block (lines 401-404) likewise only accepts
target_endpoint:. The endpoint: alias is silently ignored in both blocks,
so a user who writes:
target:
endpoint: /api/chat
redteam:
endpoint: /api/redteam
sees the shared endpoint win instead of their override. The same applies
to behavior.endpoint.
Fix: accept endpoint: as an alias in the redteam override block (elif
branch next to target_endpoint), and pre-merge behavior.endpoint into
behavior.target_endpoint before the shared-as-base merge so the override
takes effect.
Tests: 2 new regression tests in tests/test_config.py pin the contract
for both redteam and behavior endpoint aliases.
KanishkThamman
left a comment
There was a problem hiding this comment.
Correct fix for the alias asymmetry, well tested. One nit inline on precedence consistency.
| flat["target_url"] = redteam["target"] | ||
| if "target_endpoint" in redteam: | ||
| flat["target_endpoint"] = redteam["target_endpoint"] | ||
| elif "endpoint" in redteam: |
There was a problem hiding this comment.
Nit: precedence is inverted vs. the shared target: block, which prefers endpoint over target_endpoint (if endpoint / elif target_endpoint). Here target_endpoint wins over endpoint. Only matters if both keys are set in the same block, but worth making consistent.
There was a problem hiding this comment.
Fixed in d0a94d5. Flipped the precedence in both the redteam and behavior override blocks so endpoint (canonical) wins over target_endpoint when both keys are set in the same block, matching the shared target: block on lines 199-203.
Added 4 new tests to tests/test_config.py::TestSharedTargetBlock:
- test_redteam_endpoint_wins_over_target_endpoint_when_both_set
- test_behavior_endpoint_wins_over_target_endpoint_when_both_set
- test_redteam_target_endpoint_used_when_endpoint_absent (alias resolution still works)
- test_behavior_target_endpoint_used_when_endpoint_absent (alias resolution still works)
Negative-tested: with the precedence reverted, the alias value wins instead of the canonical on both blocks.
Reviewer nit on PR NuGuardAI#257: the redteam and behavior override blocks used inverted precedence for the endpoint / target_endpoint alias pair vs. the shared target: block. The shared block prefers endpoint (canonical) and falls back to target_endpoint; the override blocks did the opposite, so a user who set both keys saw the long-form value win instead of the canonical form. Fix: - Redteam block: flip the if/elif so endpoint is checked first (matching lines 199-203). - Behavior block: change the alias rename to also overwrite target_endpoint when endpoint is set, so endpoint wins when both keys are present in the same block. Tests: - test_redteam_endpoint_wins_over_target_endpoint_when_both_set - test_behavior_endpoint_wins_over_target_endpoint_when_both_set - test_redteam_target_endpoint_used_when_endpoint_absent (alias still works when only the long form is set) - test_behavior_target_endpoint_used_when_endpoint_absent (same for behavior) Negative-tested: with the precedence reverted, '-alias' wins instead of the expected '-canonical' on both blocks.
PR Type
Fixes #256
What
endpoint:as an alias in theredteam:override block innuguard/config.py_flatten_yaml, mirroring the sharedtarget:block's alias handling.endpoint:as an alias in thebehavior:override block by pre-mergingbehavior.endpoint:intobehavior.target_endpointbefore the shared-as-base merge.tests/test_config.py(test_redteam_endpoint_alias_overrides_shared_endpoint,test_behavior_endpoint_alias_overrides_shared_endpoint) pinning the contract for both aliases.Why
The shared
target:block innuguard.yamlaccepts bothendpoint:andtarget_endpoint:as aliases (nuguard/config.py:199-203).nuguard.yaml.exampleline 47 documentsendpoint:as the canonical name. The override blocks (redteam:andbehavior:) only acceptedtarget_endpoint:, so users who wroteredteam.endpoint:orbehavior.endpoint:saw their override silently dropped and the shared value win instead. This is an asymmetric alias handling bug — the override blocks must mirror what the shared block accepts.Root Cause
nuguard/config.py:233-234checked onlytarget_endpoint; theendpointalias was not consulted.nuguard/config.py:397-431injects sharedtarget_endpointinto_shared_for_behaviorand then mergesb = {**_shared_for_behavior, **b}. A user-writtenbehavior.endpoint:key was never renamed totarget_endpoint:before the merge, so it never overrode the shared value.How
python -c "..."against the existing_flatten_yaml— confirmedflat['target_endpoint']resolved to the shared/api/chatvalue whenredteam.endpoint: /api/redteamwas set.test_redteam_endpoint_overrides_shared_endpointtest intests/test_config.py:335— it exercises only the long-formtarget_endpoint:path; the alias path was never covered.elif "endpoint" in redteam:branch next to the existingtarget_endpointcheck in the redteam block, plus abehavior.endpoint→behavior.target_endpointrename before the shared-as-base merge.TestSharedTargetBlockclass so they sit alongside the existingtest_redteam_endpoint_overrides_shared_endpointtest that pins the long-form contract.Test Steps
redteam.endpoint: /api/redteamagainsttarget.endpoint: /api/chatresolves to/api/chat(wrong)./api/redteam(correct).uv run pytest tests/test_config.py -v— 36 tests pass (34 existing + 2 new).uv run pytest tests/test_config.py tests/cli/ tests/sbom/ -q— 296 passed, 1 skipped.uv run ruff check nuguard/config.py tests/test_config.py— clean.uv run mypy nuguard/config.py— clean.Checks
make testpasses for the changed files (uv run pytest tests/test_config.py tests/cli/ tests/sbom/ -q— 296 passed, 1 skipped)make lintpasses (ruff check+mypyonnuguard/config.py)make fmtapplied, no diffOther Notes
origin/main(f5b131bb),bug/*→mainperCONTRIBUTING.md.TestSharedTargetBlockclass.target_endpoint:path is unchanged; the alias path now works.