Part of the v0.58.7 → HEAD TinyAgents-migration audit (see parent issue).
Problem
Sub-agent tool exposure inverted from fail-closed to fail-open. Old subagent_runner/ops/tool_source.rs:53-107 (v0.58.7) denied any tool not in the allowlist — an empty allowlist meant deny-all. The new registration predicate at src/openhuman/tinyagents/mod.rs:1126 is allowed.is_empty() || allowed.contains(name) — an empty allowlist now registers every tool across tool_sets, which includes parent.all_tools: shell, file-write, spawn/delegate tools.
Reachable by:
- a custom agent TOML with
tools = [] (ToolScope::Named is user-deserializable, src/openhuman/agent/harness/definition.rs:621-629);
- a
skill_filter that matches zero tools;
- a
named list whose entries are absent from the parent surface (silently resolves to empty).
Compounding factors: the spawn-tool strip at subagent_runner/ops/runner.rs:415 operates on allowed_indices only, so the "sub-agents must never spawn" invariant is bypassed too; and subagent turns pass tool_policy: None (ops/graph.rs:267), so registration is the only gate.
Failure scenario
A deliberately tool-less summarizer agent processing untrusted content (web page, email) silently gains shell + file-write + spawn — a prompt-injection privilege escalation.
Fix plan
- In
src/openhuman/tinyagents/mod.rs:1126, distinguish "no filter supplied" (None → all tools) from "empty allowlist" (Some(empty) → deny all). Change the plumbed type from Vec<String>/HashSet<String> to Option<HashSet<String>> end-to-end (subagent_runner/ops/runner.rs:690-698 → assemble_turn_harness).
- Make
ToolScope::Named([]) and a zero-match skill_filter produce Some(empty) (deny-all), matching v0.58.7 semantics. Log a warn ([subagent] tool allowlist resolved empty — registering no tools) so misconfigured agents are diagnosable.
- Re-assert the spawn-tool invariant at registration time (strip spawn/delegate names from whatever set is registered, not just from
allowed_indices).
- Consider also passing a real
tool_policy on the subagent path instead of None (defense in depth).
Acceptance criteria
tools = [] / zero-match filters register zero tools; the subagent can still complete text-only turns.
- No subagent configuration can implicitly inherit the parent's full tool surface.
- Spawn/delegate tools are never registered to a subagent regardless of allowlist content.
- Unit tests over the registration predicate for:
None, Some(empty), Some({named}), named-but-unresolvable; plus an e2e asserting a tools = [] agent has no shell.
Part of the v0.58.7 → HEAD TinyAgents-migration audit (see parent issue).
Problem
Sub-agent tool exposure inverted from fail-closed to fail-open. Old
subagent_runner/ops/tool_source.rs:53-107(v0.58.7) denied any tool not in the allowlist — an empty allowlist meant deny-all. The new registration predicate atsrc/openhuman/tinyagents/mod.rs:1126isallowed.is_empty() || allowed.contains(name)— an empty allowlist now registers every tool acrosstool_sets, which includesparent.all_tools: shell, file-write, spawn/delegate tools.Reachable by:
tools = [](ToolScope::Namedis user-deserializable,src/openhuman/agent/harness/definition.rs:621-629);skill_filterthat matches zero tools;namedlist whose entries are absent from the parent surface (silently resolves to empty).Compounding factors: the spawn-tool strip at
subagent_runner/ops/runner.rs:415operates onallowed_indicesonly, so the "sub-agents must never spawn" invariant is bypassed too; and subagent turns passtool_policy: None(ops/graph.rs:267), so registration is the only gate.Failure scenario
A deliberately tool-less summarizer agent processing untrusted content (web page, email) silently gains
shell+ file-write + spawn — a prompt-injection privilege escalation.Fix plan
src/openhuman/tinyagents/mod.rs:1126, distinguish "no filter supplied" (None→ all tools) from "empty allowlist" (Some(empty)→ deny all). Change the plumbed type fromVec<String>/HashSet<String>toOption<HashSet<String>>end-to-end (subagent_runner/ops/runner.rs:690-698→assemble_turn_harness).ToolScope::Named([])and a zero-matchskill_filterproduceSome(empty)(deny-all), matching v0.58.7 semantics. Log a warn ([subagent] tool allowlist resolved empty — registering no tools) so misconfigured agents are diagnosable.allowed_indices).tool_policyon the subagent path instead ofNone(defense in depth).Acceptance criteria
tools = []/ zero-match filters register zero tools; the subagent can still complete text-only turns.None,Some(empty),Some({named}), named-but-unresolvable; plus an e2e asserting atools = []agent has noshell.