agentenv-framework-protocol 0.1.454__tar.gz → 0.1.455__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/PKG-INFO +11 -1
  2. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/README.md +10 -0
  3. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/pyproject.toml +1 -1
  4. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/client.py +80 -0
  5. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_client.py +52 -0
  6. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/.gitignore +0 -0
  7. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/LICENSE +0 -0
  8. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/NOTICE +0 -0
  9. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/THIRD_PARTY_NOTICES.md +0 -0
  10. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/examples/__init__.py +0 -0
  11. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/examples/advanced_agent.py +0 -0
  12. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/examples/basic_agent.py +0 -0
  13. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/examples/custom_extensions_agent.py +0 -0
  14. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/examples/multimodal_agent.py +0 -0
  15. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/examples/reference_model.py +0 -0
  16. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/examples/streaming_agent.py +0 -0
  17. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/__init__.py +0 -0
  18. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  19. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  20. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  21. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  22. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  23. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  24. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  25. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  26. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  27. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  28. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  29. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/agent_env_environment.py +0 -0
  30. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/browser_cdp.py +0 -0
  31. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/__init__.py +0 -0
  32. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/_declare.py +0 -0
  33. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/_linux.py +0 -0
  34. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/_load.py +0 -0
  35. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/_with_fds.py +0 -0
  36. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/app.py +0 -0
  37. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/app_data.py +0 -0
  38. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/app_runtime.py +0 -0
  39. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/apps.py +0 -0
  40. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/card.py +0 -0
  41. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/checks.py +0 -0
  42. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  43. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  44. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  45. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  46. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  47. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  48. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/conformance/page_fixture.py +0 -0
  49. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  50. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/dataplane.py +0 -0
  51. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/driver.py +0 -0
  52. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/errors.py +0 -0
  53. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/extensions.py +0 -0
  54. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/frames.py +0 -0
  55. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/keys.py +0 -0
  56. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/launch.py +0 -0
  57. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/manifest.py +0 -0
  58. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/operations.py +0 -0
  59. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/platform.py +0 -0
  60. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/queue.py +0 -0
  61. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/recording.py +0 -0
  62. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/results.py +0 -0
  63. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  64. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/runtime.py +0 -0
  65. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/seed.py +0 -0
  66. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/serve.py +0 -0
  67. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/serve_app.py +0 -0
  68. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/session.py +0 -0
  69. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/settings.py +0 -0
  70. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/skill.py +0 -0
  71. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/spaces.py +0 -0
  72. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/svc.py +0 -0
  73. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/computer/tools.py +0 -0
  74. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/errors.py +0 -0
  75. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/manifest.py +0 -0
  76. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/preflight.py +0 -0
  77. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/tool_access.py +0 -0
  78. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/tool_client.py +0 -0
  79. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/tool_context.py +0 -0
  80. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/transfers.py +0 -0
  81. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/types.py +0 -0
  82. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/src/agentenv_protocol/website_browser.py +0 -0
  83. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/computer_fake.py +0 -0
  84. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/probe_fake.py +0 -0
  85. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_a2a_agent.py +0 -0
  86. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_a2a_agent_examples.py +0 -0
  87. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_a2a_cdp_config.py +0 -0
  88. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_a2a_object_transfers.py +0 -0
  89. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_a2a_peer_agents.py +0 -0
  90. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_a2a_proxy.py +0 -0
  91. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_a2a_staging.py +0 -0
  92. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_a2a_trajectory_log.py +0 -0
  93. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_agent_env_environment.py +0 -0
  94. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_browser_cdp.py +0 -0
  95. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_app_data.py +0 -0
  96. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_app_server.py +0 -0
  97. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_conformance.py +0 -0
  98. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_conformance_checks.py +0 -0
  99. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_conformance_page.py +0 -0
  100. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_conformance_recording.py +0 -0
  101. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_dataplane.py +0 -0
  102. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_declare.py +0 -0
  103. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_driver.py +0 -0
  104. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_extensions.py +0 -0
  105. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_frames.py +0 -0
  106. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_page.py +0 -0
  107. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_queue.py +0 -0
  108. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_recording.py +0 -0
  109. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_seed.py +0 -0
  110. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_serve.py +0 -0
  111. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_spaces.py +0 -0
  112. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_computer_svc_forward.py +0 -0
  113. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_control_token.py +0 -0
  114. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_data_objects.py +0 -0
  115. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_errors.py +0 -0
  116. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_extension_methods.py +0 -0
  117. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_intake.py +0 -0
  118. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_manifest.py +0 -0
  119. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_preflight.py +0 -0
  120. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_tool_access.py +0 -0
  121. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_tool_annotations.py +0 -0
  122. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_tool_client.py +0 -0
  123. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_tool_context.py +0 -0
  124. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_tool_context_fastmcp.py +0 -0
  125. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_tool_context_injection.py +0 -0
  126. {agentenv_framework_protocol-0.1.454 → agentenv_framework_protocol-0.1.455}/tests/test_website_browser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentenv-framework-protocol
3
- Version: 0.1.454
3
+ Version: 0.1.455
4
4
  Summary: Open data-plane protocol and server SDK for agent environments.
5
5
  Project-URL: Homepage, https://www.agentenvframework.com
6
6
  Project-URL: Documentation, https://www.agentenvframework.com/docs
@@ -258,6 +258,16 @@ with any other transport are kept beside the SDK's entry. Card URLs are paths, r
258
258
  address the card was fetched from. On the client side, `client.mcp_path(card)` returns the
259
259
  declared path, or `/mcp` for a card without one.
260
260
 
261
+ A composed card, such as a gateway's, nests each backing environment's card under
262
+ `children_environments`, its `url` and endpoints rooted at the route the composer serves it at.
263
+ `client.children(card)` returns each as a `ChildEnv` (its `name`, `url` and `card`, with its
264
+ `extensions`, `operations` and `serves(method)`, and `base_url(address)`, its data plane resolved
265
+ against the composed card's address). Two children may share a name, as a website and an MCP
266
+ server can, so `client.child_at(card, url)` finds one by its url or route; `client.child_named(card,
267
+ name)` raises `AmbiguousChildError` for a shared name, and `client.find_child` returns the first.
268
+ `client.serves(card, method)` says whether a card serves a data-plane method by its declared
269
+ `operations`, None for a card that declares none.
270
+
261
271
  Extensions are invoked from what the card advertises. `client.find_extension_method(card, uri,
262
272
  method)` returns one advertised method, whose `endpoint` is the method's own or else the
263
273
  extension's, and `client.invoke_extension(base_url, card, uri, params, method=...)` calls it with
@@ -199,6 +199,16 @@ with any other transport are kept beside the SDK's entry. Card URLs are paths, r
199
199
  address the card was fetched from. On the client side, `client.mcp_path(card)` returns the
200
200
  declared path, or `/mcp` for a card without one.
201
201
 
202
+ A composed card, such as a gateway's, nests each backing environment's card under
203
+ `children_environments`, its `url` and endpoints rooted at the route the composer serves it at.
204
+ `client.children(card)` returns each as a `ChildEnv` (its `name`, `url` and `card`, with its
205
+ `extensions`, `operations` and `serves(method)`, and `base_url(address)`, its data plane resolved
206
+ against the composed card's address). Two children may share a name, as a website and an MCP
207
+ server can, so `client.child_at(card, url)` finds one by its url or route; `client.child_named(card,
208
+ name)` raises `AmbiguousChildError` for a shared name, and `client.find_child` returns the first.
209
+ `client.serves(card, method)` says whether a card serves a data-plane method by its declared
210
+ `operations`, None for a card that declares none.
211
+
202
212
  Extensions are invoked from what the card advertises. `client.find_extension_method(card, uri,
203
213
  method)` returns one advertised method, whose `endpoint` is the method's own or else the
204
214
  extension's, and `client.invoke_extension(base_url, card, uri, params, method=...)` calls it with
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.454"
3
+ version = "0.1.455"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -7,6 +7,7 @@ import contextlib
7
7
  import logging
8
8
  from collections.abc import Iterator
9
9
  from contextvars import ContextVar
10
+ from dataclasses import dataclass, field
10
11
 
11
12
  import httpx
12
13
 
@@ -210,6 +211,83 @@ def find_tool(card: dict, name: str) -> dict | None:
210
211
  return None
211
212
 
212
213
 
214
+ def serves(card: dict, method: str) -> bool | None:
215
+ """Whether a card serves the data-plane ``method`` (``data/get`` say): True or False by its declared
216
+ ``capabilities.operations``, or None when it declares none, as an older card doesn't, so the caller decides as it
217
+ did before cards declared them (a probe, say)."""
218
+ declared = operations(card)
219
+ return None if declared is None else method in declared
220
+
221
+
222
+ @dataclass(frozen=True)
223
+ class ChildEnv:
224
+ """One child env a composed card nests under ``children_environments``: its ``name`` and ``url`` as its card gives
225
+ them, and that ``card``, which says what it offers. A composer roots each child's ``url`` (and its endpoints) at
226
+ the route it serves the child at, so two children sharing a name have distinct urls; ``url`` is None for a child
227
+ whose card names none."""
228
+
229
+ name: str | None
230
+ url: str | None
231
+ card: dict = field(hash=False, repr=False)
232
+
233
+ @property
234
+ def extensions(self) -> list[dict]:
235
+ """The extensions the child's card advertises (``capabilities.extensions``)."""
236
+ return list((self.card.get("capabilities") or {}).get("extensions") or [])
237
+
238
+ @property
239
+ def operations(self) -> list[str] | None:
240
+ """The data-plane methods the child's card declares (``operations``), or None when it declares none."""
241
+ return operations(self.card)
242
+
243
+ def find_extension(self, uri: str) -> dict | None:
244
+ """The extension the child's card advertises under ``uri``, or None (``find_extension``)."""
245
+ return find_extension(self.card, uri)
246
+
247
+ def serves(self, method: str) -> bool | None:
248
+ """Whether the child's card serves the data-plane ``method``, None when it declares no operations (``serves``)."""
249
+ return serves(self.card, method)
250
+
251
+ def base_url(self, address: str) -> str:
252
+ """Where the child's data plane is, its ``url`` without the RPC path, resolved against ``address``, the composed
253
+ card's: a path joins onto it, a full URL stands alone, and a child naming no url is at ``address`` itself."""
254
+ url = (self.url or RPC_PATH).removesuffix(RPC_PATH)
255
+ return url if "://" in url else f"{address.rstrip('/')}{url}"
256
+
257
+
258
+ def children(card: dict) -> list[ChildEnv]:
259
+ """Each child env a composed card nests under ``children_environments``, in its order; none for a leaf card, or
260
+ one whose ``children_environments`` is missing or null. An entry that isn't an object is skipped."""
261
+ return [ChildEnv(name=child.get("name"), url=child.get("url"), card=child)
262
+ for child in card.get("children_environments") or [] if isinstance(child, dict)]
263
+
264
+
265
+ def child_at(card: dict, url: str) -> ChildEnv | None:
266
+ """The child env of a composed card at ``url``: the one whose ``url`` is ``url``, or whose data plane is there
267
+ (``url`` without the RPC path, ``/svc/mcp-slack`` for ``/svc/mcp-slack/agentenv`` say), a trailing slash aside;
268
+ None when no child is. A composer serves each child at its own route, so this is the lookup two children sharing a
269
+ name can't confuse."""
270
+ wanted = url.rstrip("/")
271
+ for child in children(card):
272
+ if child.url is not None and wanted in (child.url.rstrip("/"), child.url.removesuffix(RPC_PATH).rstrip("/")):
273
+ return child
274
+ return None
275
+
276
+
277
+ class AmbiguousChildError(LookupError):
278
+ """More than one child env of a composed card has the name asked for, so the name can't say which is meant."""
279
+
280
+
281
+ def child_named(card: dict, name: str) -> ChildEnv | None:
282
+ """The one child env of a composed card named ``name``, or None when none is; raises ``AmbiguousChildError`` when
283
+ several are, as a website and an MCP server sharing a name are, which ``child_at`` tells apart by their urls."""
284
+ found = [child for child in children(card) if child.name == name]
285
+ if len(found) > 1:
286
+ raise AmbiguousChildError(f"{len(found)} child envs are named {name!r} ({', '.join(str(c.url) for c in found)}); "
287
+ "look one up by its url")
288
+ return found[0] if found else None
289
+
290
+
213
291
  def find_child(card: dict, name: str) -> dict | None:
214
292
  """Return the nested child EnvironmentCard named `name`, or None.
215
293
 
@@ -218,6 +296,8 @@ def find_child(card: dict, name: str) -> dict | None:
218
296
  rewritten to be reachable from the composed card's base_url. Callers resolve a service by
219
297
  name here, then use the child card with `find_extension`/`invoke_extension`. Tolerates a
220
298
  missing/null `children_environments` so single/leaf cards return None rather than raising.
299
+ Two children sharing `name` return the first; `child_named` refuses that, and `child_at`
300
+ finds a child by its url, which tells them apart.
221
301
  """
222
302
  for child in card.get("children_environments") or []:
223
303
  if child.get("name") == name:
@@ -190,6 +190,58 @@ def test_find_child_tolerates_missing_children():
190
190
  assert protocol_v1.find_child({"children_environments": None}, "slack") is None
191
191
 
192
192
 
193
+ _SHARED_NAME = {"name": "gw", "children_environments": [
194
+ {"name": "mail", "url": "/svc/mcp-mail/agentenv", "capabilities": {"operations": ["data/get"], "extensions": [
195
+ {"uri": "urn:agentenv:clock/v1"}]}},
196
+ {"name": "mail", "url": "/svc/mail/agentenv", "capabilities": {"operations": []}},
197
+ {"name": "docs", "capabilities": {}},
198
+ "not a card",
199
+ ]}
200
+
201
+
202
+ def test_children_are_each_nested_card_by_its_name_url_and_card():
203
+ found = protocol_v1.children(_SHARED_NAME)
204
+
205
+ assert [(c.name, c.url) for c in found] == [("mail", "/svc/mcp-mail/agentenv"), ("mail", "/svc/mail/agentenv"), ("docs", None)]
206
+ assert found[0].card is _SHARED_NAME["children_environments"][0]
207
+ assert [c.extensions for c in found] == [[{"uri": "urn:agentenv:clock/v1"}], [], []]
208
+ assert [c.operations for c in found] == [["data/get"], [], None]
209
+ assert [c.serves("data/get") for c in found] == [True, False, None]
210
+ assert found[0].find_extension("urn:agentenv:clock/v1") is not None and found[1].find_extension("urn:agentenv:clock/v1") is None
211
+ assert protocol_v1.children({"name": "leaf"}) == [] and protocol_v1.children({"children_environments": None}) == []
212
+
213
+
214
+ def test_a_childs_base_url_is_its_url_without_the_rpc_path_against_the_composed_cards_address():
215
+ mcp, website, docs = protocol_v1.children(_SHARED_NAME)
216
+
217
+ assert mcp.base_url("https://env.example/") == "https://env.example/svc/mcp-mail"
218
+ assert website.base_url("https://env.example") == "https://env.example/svc/mail"
219
+ assert docs.base_url("https://env.example") == "https://env.example"
220
+ assert protocol_v1.ChildEnv("x", "https://x.example/agentenv", {}).base_url("https://env.example") == "https://x.example"
221
+
222
+
223
+ def test_child_at_finds_a_child_by_its_url_or_data_plane_route_where_two_share_a_name():
224
+ assert protocol_v1.child_at(_SHARED_NAME, "/svc/mail").url == "/svc/mail/agentenv"
225
+ assert protocol_v1.child_at(_SHARED_NAME, "/svc/mcp-mail/").url == "/svc/mcp-mail/agentenv"
226
+ assert protocol_v1.child_at(_SHARED_NAME, "/svc/mail/agentenv").url == "/svc/mail/agentenv"
227
+ assert protocol_v1.child_at(_SHARED_NAME, "/svc/docs") is None
228
+ assert protocol_v1.child_at({"name": "leaf"}, "/svc/mail") is None
229
+
230
+
231
+ def test_child_named_refuses_a_name_two_children_share():
232
+ assert protocol_v1.child_named(_SHARED_NAME, "docs").name == "docs"
233
+ assert protocol_v1.child_named(_SHARED_NAME, "gmail") is None
234
+ with pytest.raises(protocol_v1.AmbiguousChildError, match="2 child envs are named 'mail'"):
235
+ protocol_v1.child_named(_SHARED_NAME, "mail")
236
+ assert protocol_v1.find_child(_SHARED_NAME, "mail")["url"] == "/svc/mcp-mail/agentenv"
237
+
238
+
239
+ def test_serves_is_what_a_card_declares_of_a_method_and_none_when_it_declares_no_operations():
240
+ assert protocol_v1.serves({"capabilities": {"operations": ["data/get"]}}, "data/get") is True
241
+ assert protocol_v1.serves({"capabilities": {"operations": ["data/add"]}}, "data/get") is False
242
+ assert protocol_v1.serves({"capabilities": {}}, "data/get") is None
243
+
244
+
193
245
  def test_find_extension_ignores_children():
194
246
  # set-errors/v1 is advertised only on the children, never at the top level
195
247
  assert protocol_v1.find_extension(_CARD_WITH_CHILDREN, "urn:agentenv:set-errors/v1") is None