test: pin globals a rebound component impl may load (#9534) - #9605
Conversation
`bind_component_globals` rebuilds every `*_impl` on the manager component types with `FunctionType(code, vars(subagent))`. The function keeps its own code but runs on `subagent.py`'s globals. So an import at the top of the module that defines it does nothing for it. The name has to exist in `subagent.py`. Nothing checked that. A missing name raises `NameError` only when its line runs. Several of these implementations load a global inside a `try:` whose `except Exception` denies, so the miss looks like a gate that refuses everything instead of an error. kirodotdev#7843 hit this twice in five days. This adds a test that walks the bytecode of every rebound `*_impl`, collects each global it loads (nested bodies included), and asserts the name resolves in `subagent.py` or in builtins. Bytecode, not `co_names`: `co_names` also holds attribute names, which would report dozens of names that are not globals. Current `main` is clean, so this is a regression test, not a code change. The docstring on `bind_component_globals` now states the consequence a reader has to know before adding an import to one of those modules. Fixes kirodotdev#9534
|
Intent: Stop a rebound |
Design Review (Fable 5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS A recurring, evidence-backed silent-failure class (#7843, twice) pinned by a zero-runtime-cost static sweep at the single call site — sound and proportionate. [DESIGN-REVIEWED] a6ee524 |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed |
First Principles Review (Fable 5, fork) — ✅ PASSPremise-level review of The evidence is consistent: the rebind mechanism exists at exactly one site, the First-Principles-Verdict: PASS Verify #7843 records the two silent-deny NameErrors; the tree corroborates the class only indirectly (admission.py:890's imported-inside-the-function comment). What this change shipsInventory (4 items) — 4 justifiedIntent: stop a rebound
Duplication and siblings counted: one [FIRST-PRINCIPLES-REVIEWED] a6ee524 |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Problem / Motivation
bind_component_globalsinsrc/kiro_crew/subagent_manager/_component.pyrebuilds everyfunction whose name ends in
_implwithFunctionType(code, vars(subagent)). The functionkeeps its own code but runs on
subagent.py's globals. So an import at the top of the modulethat defines the function does nothing for it. The name has to exist in
subagent.py.Nothing checked that. A rebound function can load a global
subagent.pydoes not define. Itimports fine and it reads fine in review. It raises
NameErroronly when that line runs.Why it matters
Several of these implementations load a global inside a
try:whoseexcept Exceptiondenies.The
NameErroris swallowed. The miss then looks like a gate that refuses everything, not likean error.
#7843 hit this twice in five days. First
HOOK_EVENT_PRE_TOOL_USE, then_should_block_results.Both times a PreToolUse security gate read as working while it denied every call without ever
consulting a hook.
What changed (motivation → approach → change)
The symptom is a silent deny. The cause is a name that resolves nowhere, and nobody looks until
the line runs.
Two fixes were on the table. Check at bind time, or check in a test. Bind time adds work to every
start and still only raises after the code has shipped. A test catches it before it ships and
costs nothing at runtime. So the change is a test.
The test walks the bytecode of every rebound
*_impl, collects each global it loads including theones in nested bodies, and asserts the name resolves in
subagent.pyor in builtins. Bytecode,not
co_names:co_namesalso holds attribute names, which would report dozens of names that arenot globals.
mainis clean today, so this is a regression test and no runtime behaviour changes.The docstring on
bind_component_globalsnow states the consequence, so the next person adding animport to one of those modules reads it right there.
Tests
New
test/test_subagent_component_globals.py, three tests:test_rebound_implementations_only_load_globals_subagent_defines— the sweep. Every global arebound
*_implloads resolves insubagent.pyor in builtins. Goes red on both fix(agents): enforce PreToolUse deny on subagent/task-runner paths #7843 names.test_component_implementations_are_rebound_onto_subagent_globals— proves the sweep hadsomething to sweep. If the rebind ever stops happening, the sweep would pass while checking
nothing. This test fails instead.
test_the_sweep_reports_a_global_subagent_does_not_define— proves the detector sees anunresolved name inside a nested function, so the recursion into
co_constsis covered.Runs in about 4 seconds.
Manual verification
N/A — unit coverage sufficient. There is no UI surface and no runtime behaviour change: the only
source edit is a docstring. The test leaves nothing behind — no child process, no
chdir, nowrites outside pytest's own tmp dir — and the repo tree is clean after a run.
Related Issues
Fixes #9534
Pattern harvest
Rule candidate: review-prompt
Pattern: a function rebound onto another module's globals loads a name that module does not define
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)