Skip to content

fix(spawn): honor an agent's declared agents: access-control policy - #255

Merged
bkrabach merged 1 commit into
mainfrom
fix/honor-agents-declaration
Aug 3, 2026
Merged

fix(spawn): honor an agent's declared agents: access-control policy#255
bkrabach merged 1 commit into
mainfrom
fix/honor-agents-declaration

Conversation

@bkrabach

@bkrabach bkrabach commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

What

An agent can declare agents: — a Smart Single Value ("all"" | "none"" | list) that controls which sub-agents its spawned session may delegate to. merge_configs() honors it. The runtime-registry propagation block added later then silently undid it.

Measured on main (parent static = {explorer, builder}; live registry additionally holds mode-contributed sibling_b):

overlay declares after merge_configs after propagation
"none" [] [builder, explorer, sibling_b] delegation re-enabled
["sibling_b"] [] [builder, explorer, sibling_b] under- and over-delivers
["explorer"] [explorer] [builder, explorer, sibling_b] allowlist blown open
"all" [builder, explorer] [builder, explorer, sibling_b] correct
absent [builder, explorer] [builder, explorer, sibling_b] correct

An agent that declared agents: none received every agent in the live registry.

Why this is a bug and not a design decision

Traced through primary sources before changing anything:

  • d609bb2 (2025-11-30) introduced the field. Its commit message states the intent: ""none" → empty agents dict (disable delegation); list → filter parent's agents to specified names… This enables agents to control which sub-agents their spawned sessions can delegate to via the task tool."
  • Docs promise it. microsoft/amplifier-profiles docs/AGENT_AUTHORING.md documents agents: none # Cannot call task tool to delegate, followed by a three-step "How it works" describing exactly the filtering this block defeats. amplifier-app-cli/docs/AGENT_DELEGATION_IMPLEMENTATION.md:409-416 documents agents: none # Disable agents.
  • 774d8b9 (PR fix(session-spawner): propagate runtime overlay agents and skill capability to child sessions (#233) #178, for microsoft-amplifier/amplifier-support#233) added the propagation so mode-contributed agents reach children. Its rule was "Local agent_config declarations win over inherited live registry (never overwrite)" — implemented only as same-name collision avoidance, never as policy enforcement. Nothing in that PR, the issue, or the maintainer's closing comment addresses the filter interaction.
  • No test encodes the current behavior as correct. S1–S6 in test_session_spawner_issue_233.py never pass an overlay carrying an agents: key, and the two merge_configs filter tests in test_agent_config.py stop before the spawner runs. The two suites passed independently while contradicting each other at runtime.
  • fix: green the test suite — 2 real product bugs, 13 stale tests, no CI exclusions #253's body already recorded this as a known unfixed bug with the suggested gating shape.

So this restores stated intent rather than changing designed behavior.

It also fixes PR #178's own complaint

PR #178's body noted: "even an explicit agents: [sibling_b] declaration in agent A's config wouldn't reach sibling_b — the source dict is the wrong one." That under-delivery was never fixed — merge_configs filters the static snapshot, which does not contain mode-contributed agents, so an allowlist naming one resolved to {}.

Applying the declaration to the live registry as well reconciles both intents in one place: the allowlist is satisfied from the union of static + live agents. Row 2 of the table goes from "everything" to exactly [sibling_b] — the agent gets precisely what it asked for, no more and no less.

After

overlay declares child receives
"none" []
["sibling_b"] [sibling_b]
["explorer"] [explorer]
"all" [builder, explorer, sibling_b] (unchanged)
absent [builder, explorer, sibling_b] (unchanged)

The gate keys off agent_config — the overlay's own declaration — not off merged_config["agents"] being empty. An unrestricted agent whose parent simply has no static agents also produces an empty merged dict; gating on emptiness would wrongly suppress propagation for it. TestS6ParentIsolation::test_propagation_does_not_mutate_parent_config is exactly that case and still passes unchanged.

Preserved from #253: fresh-dict-and-rebind (no cross-session mutation of the parent's live config), deepcopy per propagated agent, same-name local-wins.

Blast radius: zero on the installed ecosystem

Surveyed every parsed bundle/agent/behavior/mode/profile config under ~/.amplifier/cache/ — 749 config files, 51 agents: key occurrences. All 51 are the dict-shaped defining section (agents: include: [...] rosters and inline agent definitions). Zero are the access-control Smart Single Value form. No installed agent declares a restriction, so nothing currently in the ecosystem changes behavior.

Checked the consumers that depend on propagation: 4 modes contribute agents at runtime, and context-intelligence is the one shipped configuration with a real sibling hop (facilitator → tool-designer). None of them declares agents:, so the gate leaves them untouched. Every session.spawn caller — tool-delegate, tool-recipes, tool-skills (which spawns agent_name="self", i.e. an empty overlay by construction), attractor's loop modules — passes no agents: declaration.

Zero current users is also the argument for doing this now rather than later: nobody depends on the broken behavior yet, so this is the cheapest this fix will ever be.

Tests

TestS7AccessControlDeclaration — 5 spawn-level tests, one per row, asserting the exact resulting agent-name set. This is the first spawn-level coverage of the access-control declaration; no prior assertion needed reversing.

uv run pytest -q              1287 passed, 1 skipped, 13 deselected, 1 xfailed   (was 1282; +5 new)
uv run pytest -m integration  13 passed

Non-vacuousness — with session_spawner.py reverted to main and the new tests kept:

3 failed, 2 passed
  test_agents_none_disables_all_delegation                      Got: ['builder', 'explorer', 'sibling_b'], expected {}
  test_agents_list_of_live_only_name_is_honored_from_live_...   Got: ['builder', 'explorer', 'sibling_b'], expected {'sibling_b'}
  test_agents_list_of_static_name_is_honored_as_allowlist       Got: ['builder', 'explorer', 'sibling_b'], expected {'explorer'}

The two "all"/absent tests correctly still pass — that behavior is unchanged by design.

Follow-up, not in this PR

amplifier-foundation's docs/AGENT_AUTHORING.md and docs/BUNDLE_GUIDE.md document agents: only in the bundle-roster sense and are silent on the access-control form; a repo-wide grep of the installed foundation docs for agents: none returns zero hits. The feature is documented only in amplifier-app-cli and amplifier-profiles. Worth a cross-repo docs pass now that the behavior matches the promise.

Follow-up to #178 and #253.

An agent can declare agents: -- a Smart Single Value that controls which sub-agents its spawned session may delegate to. merge_configs() honors it. The runtime-registry propagation block added later then silently undid it.

This commit:
- Applies agent_config declarations to the live registry propagation (not just merge_configs)
- Ensures same-name local-wins collision avoidance is preserved
- Adds comprehensive spawn-level test coverage for all declaration forms
- Fixes PR #178's original complaint about allowlists under-delivering

Blast radius: zero. Surveyed all 749 config files under ~/.amplifier/cache/ -- 51 agents: occurrences exist, all are dict-shaped agent rosters. Zero access-control declarations in the installed ecosystem, so nothing changes for deployed systems.

Preserves from #253: fresh-dict-and-rebind (no cross-session mutation), deepcopy per agent, local-wins.

Follow-up to #178 and #253.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@bkrabach
bkrabach merged commit 6c3fd86 into main Aug 3, 2026
7 checks passed
@bkrabach
bkrabach deleted the fix/honor-agents-declaration branch August 3, 2026 15:03
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.

2 participants