Channel overrides: pin per-channel resolution of skill_only_tools, and advise on uncovered sibling channels - #142
Open
eldonm wants to merge 2 commits into
Open
Channel overrides: pin per-channel resolution of skill_only_tools, and advise on uncovered sibling channels#142eldonm wants to merge 2 commits into
eldonm wants to merge 2 commits into
Conversation
added 2 commits
August 5, 2026 16:45
Covers the shapes an operator actually hits and that the existing suite missed: an explicit empty override gates nothing (rather than falling back), an override gates its own channel when the action-level list is empty, alternating channels do not leak state, an unknown channel falls back, the channel-resolved list drives the lean pre-surface pool, and a per-channel deny still beats the gate. The load-bearing one is exact-key matching: channel_overrides is looked up by visitor.channel verbatim, so a 'whatsapp' block does not cover a 'whatsapp_call' voice turn. A mis-keyed override silently no-ops and the action-level list applies, which presents as 'channel overrides are broken' rather than as a typo. Documented in docs/ORCHESTRATOR.md and the config-key reference.
…nnel channel_overrides is resolved by the exact visitor.channel string, so a block written for 'whatsapp' does nothing on a 'whatsapp_call' voice turn and the action-level value applies silently. Both are valid channel names, so no key-validity check can catch it; the only signal is that one member of a channel family is configured while its reachable sibling is not. Fires only when the sibling is genuinely reachable (its providing action is enabled on the agent) and only for keys whose absence changes behavior silently: skill_only_tools, denied_tools, pinned_tools. Per-channel divergence in history_limit, ack knobs and system_prompt_extra is normal and is ignored. Adds an 'advisory' severity to AgentYamlWarning. Advisories are printed but do not affect the exit code, so a heuristic lint cannot break an existing pipeline; 'jvagent validate --strict' promotes them to failures. CHANNEL_PROVIDERS keys on each action's published package.name, not its directory name (jvagent/action/whatsapp_voice/ publishes as jvagent/whatsapp_voice_action) — a test pins that against the real info.yaml, because a wrong ref makes the lint silently never fire while every behavioral test still passes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two commits on the same problem, follow-up to #138.
channel_overridesis resolved by the exactvisitor.channelstring — no prefix matching, no aliasing — so a block written forwhatsappdoes nothing on awhatsapp_call(voice) turn. The mis-keyed override silently no-ops and the action-level value applies instead, which presents as "channel overrides are broken" rather than as a config error. Both keys are valid channels, and the example app ships blocks for both, so this is easy to hit.test(orchestrator)— pins the per-channel resolution behavior, which was only partly covered.feat(validate)— makesjvagent validateadvise when this specific mistake is present.No change to resolution semantics: exact-key matching is correct and is now pinned against anyone "fixing" it into fuzzy matching.
Commit 1 — resolution tests
tests/action/orchestrator/test_skill_only_channel_overrides.py(7 tests). The existing suite covered only the "override with a different list" case.test_empty_override_ungates_that_channel[]means "gate nothing here", not "fall back" — a truthiness-based resolver gets this backwardstest_override_gates_when_action_level_is_emptytest_alternating_channels_do_not_leak_statetest_unknown_channel_falls_back_to_action_leveldefaultboth take the action-level listtest_override_key_must_match_the_channel_exactlywhatsappdoes not coverwhatsapp_calltest_channel_resolved_list_drives_the_lean_pooltest_denied_channel_override_still_beats_the_gateEach asserts the gate by calling the tool and checking for the steer string, not by inspecting config.
Commit 2 — the validate-time advisory
Emits an advisory when a subtractive knob (
skill_only_tools,denied_tools,pinned_tools) is set for one channel of a family but not for its reachable sibling:Deliberately conservative, because a lint operators learn to ignore is worse than no lint:
history_limit, ack knobs andsystem_prompt_extrais normal and is ignored.New
advisoryseverityEvery existing
AgentYamlWarningis CI-fatal (run_validatereturns 1 on any warning). Shipping a heuristic at that severity would break the build of any app whose asymmetry is deliberate. SoAgentYamlWarninggainsseverity, defaulting to"warning"— every existing call site is unchanged — and advisories are printed without affecting the exit code.jvagent validate --strictpromotes them to failures for teams that want them enforced.A note on why one test matters more than the others
CHANNEL_PROVIDERSmaps a channel to the action that provides it, and that map is the lint's reachability gate. My first draft keyed it on package directory names (jvagent/whatsapp_voice) rather than published package names (jvagent/whatsapp_voice_action) — they differ. Every behavioral test still passed, because the fixtures used the same wrong constant, and the lint would simply never have fired on a real app. Only an end-to-end run against a realagent.yamlcaught it.test_channel_providers_match_real_action_package_namesnow reads the actualinfo.yamlfiles and pins the refs. It was mutation-checked: reverting the ref turns it red.Verification
pytest tests/core/test_channel_override_coverage.py tests/cli/test_validate_advisories.py→ 17 passedpytest tests/action/orchestrator/test_skill_only_channel_overrides.py→ 7 passedpytest tests/→ exit 0pre-commit run --all-files→ 8/8 hooks pass, no modificationsvalidateexits 0,--strictexits 1pinned_toolson bothwhatsappandwhatsapp_call, produces no advisory (true negative)Design notes, including what was considered and dropped, are in
.planning/specs/2026-08-04-channel-overrides-validation-design.md.🤖 Generated with Claude Code