Skip to content

fix(config): honor endpoint: alias in redteam/behavior override blocks - #257

Open
nikhilpatidar wants to merge 2 commits into
NuGuardAI:mainfrom
nikhilpatidar:bug/config-endpoint-alias-override
Open

fix(config): honor endpoint: alias in redteam/behavior override blocks#257
nikhilpatidar wants to merge 2 commits into
NuGuardAI:mainfrom
nikhilpatidar:bug/config-endpoint-alias-override

Conversation

@nikhilpatidar

Copy link
Copy Markdown

PR Type

  • Bug fix
  • Feature

Fixes #256

What

  • Accept endpoint: as an alias in the redteam: override block in nuguard/config.py _flatten_yaml, mirroring the shared target: block's alias handling.
  • Accept endpoint: as an alias in the behavior: override block by pre-merging behavior.endpoint: into behavior.target_endpoint before the shared-as-base merge.
  • Added 2 regression tests in 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 in nuguard.yaml accepts both endpoint: and target_endpoint: as aliases (nuguard/config.py:199-203). nuguard.yaml.example line 47 documents endpoint: as the canonical name. The override blocks (redteam: and behavior:) only accepted target_endpoint:, so users who wrote redteam.endpoint: or behavior.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

  • Redteam override block at nuguard/config.py:233-234 checked only target_endpoint; the endpoint alias was not consulted.
  • Behavior override block at nuguard/config.py:397-431 injects shared target_endpoint into _shared_for_behavior and then merges b = {**_shared_for_behavior, **b}. A user-written behavior.endpoint: key was never renamed to target_endpoint: before the merge, so it never overrode the shared value.

How

  • Reproduced the bug with a 5-line python -c "..." against the existing _flatten_yaml — confirmed flat['target_endpoint'] resolved to the shared /api/chat value when redteam.endpoint: /api/redteam was set.
  • Inspected the existing test_redteam_endpoint_overrides_shared_endpoint test in tests/test_config.py:335 — it exercises only the long-form target_endpoint: path; the alias path was never covered.
  • Added the elif "endpoint" in redteam: branch next to the existing target_endpoint check in the redteam block, plus a behavior.endpointbehavior.target_endpoint rename before the shared-as-base merge.
  • Added two regression tests in the existing TestSharedTargetBlock class so they sit alongside the existing test_redteam_endpoint_overrides_shared_endpoint test that pins the long-form contract.

Test Steps

  • Reproduce the original issue: a yaml containing redteam.endpoint: /api/redteam against target.endpoint: /api/chat resolves to /api/chat (wrong).
  • Confirm the issue no longer occurs on this branch: same yaml resolves to /api/redteam (correct).
  • Run uv run pytest tests/test_config.py -v — 36 tests pass (34 existing + 2 new).
  • Run uv run pytest tests/test_config.py tests/cli/ tests/sbom/ -q — 296 passed, 1 skipped.
  • Run uv run ruff check nuguard/config.py tests/test_config.py — clean.
  • Run uv run mypy nuguard/config.py — clean.

Checks

  • make test passes for the changed files (uv run pytest tests/test_config.py tests/cli/ tests/sbom/ -q — 296 passed, 1 skipped)
  • make lint passes (ruff check + mypy on nuguard/config.py)
  • make fmt applied, no diff
  • Added regression tests covering both aliases

Other Notes

  • Branched from origin/main (f5b131bb), bug/*main per CONTRIBUTING.md.
  • No changes to any existing tests; only two new test methods added in the TestSharedTargetBlock class.
  • The fix is purely additive in terms of behaviour — the long-form target_endpoint: path is unchanged; the alias path now works.

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
KanishkThamman previously approved these changes Aug 11, 2026

@KanishkThamman KanishkThamman 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.

Correct fix for the alias asymmetry, well tested. One nit inline on precedence consistency.

Comment thread nuguard/config.py Outdated
flat["target_url"] = redteam["target"]
if "target_endpoint" in redteam:
flat["target_endpoint"] = redteam["target_endpoint"]
elif "endpoint" in redteam:

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.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.
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.

[Bug]: redteam/behavior override blocks ignore endpoint: alias (only target_endpoint: honored)

2 participants