diff --git a/workspace/a2a_mcp_server.py b/workspace/a2a_mcp_server.py index 2c47655a..aaa10bcf 100644 --- a/workspace/a2a_mcp_server.py +++ b/workspace/a2a_mcp_server.py @@ -221,8 +221,9 @@ def _build_channel_instructions() -> str: "\n" "PUSH PATH (Claude Code with channel push enabled):\n" "Messages arrive as tags as a " - "synthetic user turn — no agent action needed to surface them.\n" + "peer_id=\"...\" peer_name=\"...\" peer_role=\"...\" " + "agent_card_url=\"...\" activity_id=\"...\" ts=\"...\"> tags as " + "a synthetic user turn — no agent action needed to surface them.\n" "\n" "POLL PATH (every other MCP client + Claude Code without push " "enabled — this is the universal default):\n" @@ -234,6 +235,16 @@ def _build_channel_instructions() -> str: "delegating to you).\n" "- `peer_id` is empty for canvas_user, set to the sender " "workspace UUID for peer_agent.\n" + "- `peer_name` and `peer_role` are present for peer_agent when " + "the platform registry resolved the sender — e.g. " + "`peer_name=\"ops-agent\"`, `peer_role=\"sre\"`. Surface these " + "in your reasoning so the user can tell which peer is talking " + "without having to memorise UUIDs. Absent on canvas_user and " + "on a registry-lookup failure (the push still delivers).\n" + "- `agent_card_url` is present for peer_agent and points at " + "the platform's discover endpoint for that peer — fetch it if " + "you need the peer's full capability list (skills, role, " + "runtime).\n" "- `activity_id` is the inbox row to acknowledge.\n" "\n" "Reply path:\n" diff --git a/workspace/tests/test_a2a_mcp_server.py b/workspace/tests/test_a2a_mcp_server.py index 5d625775..b9e68e80 100644 --- a/workspace/tests/test_a2a_mcp_server.py +++ b/workspace/tests/test_a2a_mcp_server.py @@ -704,6 +704,42 @@ def test_instructions_zero_timeout_means_push_only_mode(): os.environ["MOLECULE_MCP_POLL_TIMEOUT_SECS"] = saved +def test_instructions_document_envelope_enrichment_attrs(): + """The agent learns about envelope attributes ONLY from the + instructions string. PR-B added peer_name, peer_role, + agent_card_url to the wire shape; pin that the instructions list + them in the tag template AND describe each one's + semantics. Without this, the wheel ships new attributes that no + agent ever uses.""" + from a2a_mcp_server import _build_initialize_result + + instructions = _build_initialize_result()["instructions"] + + # The tag template in the PUSH PATH section must include + # the new attribute names so the agent recognises them when they + # arrive inline. + for attr in ("peer_name", "peer_role", "agent_card_url"): + assert attr in instructions, ( + f"instructions must list `{attr}` as a tag " + f"attribute — otherwise the agent sees the attr in pushes " + f"but doesn't know what to do with it" + ) + + # And the per-field semantics block must explain when each attr + # is present + what it means. These phrases are what the agent + # actually reads to decide how to surface the attrs in its turn. + assert "registry resolved" in instructions, ( + "instructions must explain peer_name/peer_role come from a " + "registry lookup that may fail — otherwise the agent treats " + "their absence as a bug instead of a graceful degrade" + ) + assert "discover endpoint" in instructions, ( + "instructions must point at the registry discover endpoint " + "for agent_card_url so the agent knows it's a follow-on URL " + "to fetch full capabilities, not the body of the message" + ) + + def test_initialize_instructions_pins_prompt_injection_defense(): """The threat-model sentence in `_CHANNEL_INSTRUCTIONS` is what tells the agent that inbound canvas-user / peer-agent message