0.4.3 → 0.4.4 — Tool-trigger Filtering Fix (DEV-631)¤
Summary¤
This release fixes a silent cross-agent capability leak in
ToolReference._resolve_single. Per-agent trigger filtering no longer
leaks across agents sharing a setup_id: the SDK tool cache stops
trimming ToolModuleInfo.tools by per-selection triggers; the full
catalogue is always stored, and per-agent filtering happens at the
consumer site.
The cache contract changes shape (full catalogue per setup_id instead of
trimmed lists) but the public API is unchanged. Persisted resolved_tools
written by <= 0.4.3 may still hold incorrectly trimmed lists — they are
automatically flushed by the startup clear introduced in 0.4.3, but the
first mission run after upgrading may want a setup-version bump to be
absolutely safe. See the Migration section below.
Bug — capability leak across agents (DEV-631)¤
Symptom¤
In archetype-isaac, a single setup can host a main_agent, a team with
N members and a workflow with M steps. Each agent independently
selects which trigger protocols of a shared SDK tool (one setup_id) it
can call, via a tools field carrying a list of
ToolSelection({setup_id, triggers: {name: bool}}).
When several agents reference the same setup_id with different trigger
booleans, the first agent walked by the SDK silently dictated which
tools were loaded for every other agent. Example with three agents
sharing one setup_id (a Knowledge Graph tool with 5 triggers):
| Agent | Configured triggers | Effective tools (before fix) |
|---|---|---|
main_agent |
all 5 true | all 5 |
| member "search only" | {search: true, rest: false} |
all 5 |
| member "edit only" | {edit: true, rest: false} |
all 5 |
…and worse, if main_agent had edit: false, the "edit only" member would
lose access to edit entirely — its own trigger config was ignored.
Root cause¤
ToolReference._resolve_single previously trimmed the resolved
ToolModuleInfo.tools in place, based on a single ToolSelection's
triggers:
if enabled_triggers := {name for name, enabled in entry.triggers.items() if enabled}:
tool_info.tools = [t for t in tool_info.tools if t.name in enabled_triggers]
SetupModel._collect_from_tool_ref keys the resolution cache by setup_id
only and short-circuits via has_uncached. Subsequent ToolReference
instances pointing at the same setup_id therefore skipped resolution and
reused the first writer's trimmed object. The trimmed ToolModuleInfo then
populated context.tool_cache.entries, which downstream toolkit code
(e.g. archetype-isaac's ModuleToolkit) reads from. Per-agent
allowed_tools filters could only shrink that already-trimmed list,
never re-expand it — so any trigger the first resolver dropped became
permanently invisible to later agents.
resolved_tools is also persisted across mission runs (invalidated only on
setup-version bumps prior to 0.4.3, or on module startup since 0.4.3), so a
stale trimmed cache survived runs until the user edited the setup or
restarted the module.
Fix¤
ToolReference._resolve_single no longer touches tool_info.tools. It
returns the full module catalog, unchanged. The full canonical
ToolModuleInfo is stored under tool_cache.entries[setup_id] and
re-used by every agent that references the setup.
Per-agent filtering is the consumer's responsibility — for example
archetype-isaac builds one ModuleToolkit(allowed_tools=...) per agent
with allowed_tools derived from that agent's own ToolSelection.triggers.
Filtering after the cache, on each toolkit instance, means three agents
on the same setup_id with {search: true}, {edit: true} and
{everything: true} each end up with exactly the tools they declared,
even though the underlying cache entry holds the full catalog.
Architecture (after fix)¤
ToolReference.resolve
└─ _resolve_single(entry) # no triggers filter
└─ registry.discover_by_id # returns full ModuleInfo
└─ module_info_to_tool_module_info → ToolModuleInfo (full tools)
└─ tool_cache.entries[setup_id] = info # shared, untrimmed
Consumer (e.g. archetype-isaac)
└─ ToolkitMixin.create_toolkit_for_selection(setup_id, triggers)
├─ allowed_tools = {name for name, enabled in triggers.items() if enabled}
└─ ModuleToolkit(tool_module_info, allowed_tools=allowed_tools)
└─ filters Function objects per agent, never mutates the cache
Migration¤
- No code changes required in consumers. The
ToolReference,ToolSelection,ToolModuleInfoandToolCachetypes are unchanged in shape. - One-time data hygiene: persisted
resolved_toolsentries written by<= 0.4.3may still hold incorrectly trimmedtoolslists. The startup clear introduced in 0.4.3 already flushes them on the next module boot, so a vanilla restart after upgrading is enough. If you want belt-and-braces certainty (e.g. for shared production setups whoseresolved_toolswas written before the 0.4.3 startup clear landed), bump the affected setup's version once to force a clean re-resolution. - Archetypes relying on the previous (buggy) behaviour of having the
first agent's triggers silently restrict all peers must instead set
the desired triggers explicitly on each
ToolSelection— every agent is now independently honoured.
Verification¤
The SDK ships with new regression coverage in
tests/modules/test_tool_reference.py (class
TestSharedSetupIdAcrossAgents):
- two
ToolReferences sharing asetup_idwith disjoint triggers ({search: true, edit: false}and{search: false, edit: true}) both observe the full tool catalogue intool_cache.entries; - a second resolution call does not progressively trim the cache.
archetype-isaac mirrors this with three agents (main, search-only,
edit-only) sharing a single setup_id, asserting that each toolkit
holds exactly its agent's enabled triggers while the shared cache
entry remains untouched
(tests/toolkits/test_toolkit_mixin.py::TestSharedSetupAcrossAgents).