agentenv-framework-protocol 0.1.419__tar.gz → 0.1.421__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 (114) hide show
  1. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/PKG-INFO +40 -6
  2. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/README.md +39 -5
  3. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/pyproject.toml +1 -1
  4. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/__init__.py +2 -0
  5. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/agent_env_environment.py +35 -5
  6. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/client.py +68 -14
  7. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/__init__.py +2 -1
  8. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/app.py +142 -25
  9. agentenv_framework_protocol-0.1.421/src/agentenv_protocol/computer/app_data.py +217 -0
  10. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/app_runtime.py +21 -2
  11. agentenv_framework_protocol-0.1.421/src/agentenv_protocol/computer/apps.py +125 -0
  12. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/conformance/__init__.py +5 -3
  13. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/conformance/__main__.py +8 -2
  14. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/conformance/checks.py +33 -2
  15. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/conformance/client.py +9 -6
  16. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/dataplane.py +30 -21
  17. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/driver.py +32 -5
  18. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/runtime.py +4 -0
  19. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/serve_app.py +9 -3
  20. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/session.py +33 -1
  21. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/types.py +3 -0
  22. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_client.py +8 -0
  23. agentenv_framework_protocol-0.1.421/tests/test_computer_app_data.py +280 -0
  24. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_app_server.py +81 -1
  25. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_conformance.py +37 -2
  26. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_serve.py +6 -2
  27. agentenv_framework_protocol-0.1.421/tests/test_control_token.py +148 -0
  28. agentenv_framework_protocol-0.1.419/src/agentenv_protocol/computer/apps.py +0 -63
  29. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/.gitignore +0 -0
  30. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/LICENSE +0 -0
  31. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/NOTICE +0 -0
  32. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/THIRD_PARTY_NOTICES.md +0 -0
  33. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/examples/__init__.py +0 -0
  34. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/examples/advanced_agent.py +0 -0
  35. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/examples/basic_agent.py +0 -0
  36. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/examples/custom_extensions_agent.py +0 -0
  37. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/examples/multimodal_agent.py +0 -0
  38. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/examples/reference_model.py +0 -0
  39. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/examples/streaming_agent.py +0 -0
  40. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  41. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  42. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  43. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  44. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  45. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  46. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  47. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  48. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  49. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  50. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  51. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/_declare.py +0 -0
  52. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/_linux.py +0 -0
  53. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/_load.py +0 -0
  54. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/card.py +0 -0
  55. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/checks.py +0 -0
  56. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  57. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  58. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  59. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/errors.py +0 -0
  60. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/extensions.py +0 -0
  61. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/frames.py +0 -0
  62. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/keys.py +0 -0
  63. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/launch.py +0 -0
  64. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/manifest.py +0 -0
  65. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/operations.py +0 -0
  66. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/platform.py +0 -0
  67. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/queue.py +0 -0
  68. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/recording.py +0 -0
  69. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/results.py +0 -0
  70. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  71. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/serve.py +0 -0
  72. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/settings.py +0 -0
  73. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/skill.py +0 -0
  74. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/spaces.py +0 -0
  75. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/computer/tools.py +0 -0
  76. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/errors.py +0 -0
  77. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/manifest.py +0 -0
  78. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/preflight.py +0 -0
  79. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/tool_access.py +0 -0
  80. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/tool_client.py +0 -0
  81. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/tool_context.py +0 -0
  82. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/src/agentenv_protocol/transfers.py +0 -0
  83. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/computer_fake.py +0 -0
  84. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/probe_fake.py +0 -0
  85. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_a2a_agent.py +0 -0
  86. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_a2a_agent_examples.py +0 -0
  87. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_a2a_object_transfers.py +0 -0
  88. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_a2a_peer_agents.py +0 -0
  89. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_a2a_proxy.py +0 -0
  90. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_a2a_staging.py +0 -0
  91. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_a2a_trajectory_log.py +0 -0
  92. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_agent_env_environment.py +0 -0
  93. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_conformance_checks.py +0 -0
  94. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_conformance_recording.py +0 -0
  95. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_dataplane.py +0 -0
  96. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_declare.py +0 -0
  97. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_driver.py +0 -0
  98. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_extensions.py +0 -0
  99. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_frames.py +0 -0
  100. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_queue.py +0 -0
  101. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_recording.py +0 -0
  102. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_computer_spaces.py +0 -0
  103. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_data_objects.py +0 -0
  104. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_errors.py +0 -0
  105. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_extension_methods.py +0 -0
  106. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_intake.py +0 -0
  107. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_manifest.py +0 -0
  108. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_preflight.py +0 -0
  109. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_tool_access.py +0 -0
  110. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_tool_annotations.py +0 -0
  111. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_tool_client.py +0 -0
  112. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_tool_context.py +0 -0
  113. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_tool_context_fastmcp.py +0 -0
  114. {agentenv_framework_protocol-0.1.419 → agentenv_framework_protocol-0.1.421}/tests/test_tool_context_injection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentenv-framework-protocol
3
- Version: 0.1.419
3
+ Version: 0.1.421
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
@@ -283,6 +283,23 @@ async def display_get(self) -> dict: ...
283
283
  async def display_set(self, width: int, height: int) -> dict: ...
284
284
  ```
285
285
 
286
+ ### Framework-only routes and the control token
287
+
288
+ Everything an environment serves but its MCP endpoint and its card is agent-env's own: the data
289
+ plane, every `@extension` route and the tool-access routes. When agent-env deploys an environment
290
+ that takes one, it boots it with a per-deploy token in `AGENTENV_CONTROL_TOKEN`
291
+ (`CONTROL_TOKEN_ENV`), and the SDK then refuses a call to those routes that doesn't carry it as
292
+ `Authorization: Bearer <token>`, answering 401 with the code `unauthorized` before the route runs.
293
+ The MCP endpoint and the card stay open, so an agent reaches the environment's tools as before and
294
+ nothing else. Without the variable, as when the environment runs on its own, every route is open.
295
+ `control_guard()` wraps a route an environment adds itself the same way, as a computer driver does
296
+ its `/svc/<app>/` routes.
297
+
298
+ On the client side each call takes `headers`, and `client.control_headers(token)` builds them.
299
+ Code that names none runs under `client.authorized({env_url: client.control_headers(token)})`:
300
+ within it, a call to an address under `env_url` sends those headers. agent-env runs every task
301
+ step, a verifier's `verify` included, under such a grant for each environment it deployed.
302
+
286
303
  ### Tool access without a gateway
287
304
 
288
305
  An environment agents reach directly, with no gateway in front, can enforce per-role tool access
@@ -561,8 +578,8 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
561
578
  or a `transfers.ReadObject` read grant under the marker's `read`, and writes them atomically.
562
579
  `data/get` returns the files under the `paths` it's given, relative to the home, or under the
563
580
  card's `default_data_paths` (`Desktop`, `Documents`, `Downloads`) when it's given none, inline or
564
- uploaded under a `write_namespace` grant. An environment whose `@get_data` hook takes no `paths`
565
- refuses them.
581
+ uploaded under a `write_namespace` grant, leaving out the paths its apps keep their data in. An
582
+ environment whose `@get_data` hook takes no `paths` refuses them.
566
583
  - **The card** (the name from `ENVIRONMENT_NAME`, then `@computer_driver`'s), and per-role tool
567
584
  access (`tool_access` is on).
568
585
 
@@ -644,7 +661,10 @@ report = await run_probe("http://localhost:18765", "full") # a ProbeReport
644
661
  failed = [c for c in report.checks if c.status == "failed"]
645
662
  ```
646
663
 
647
- The CLI exits 0 when no check failed, 1 when one did and 2 when it can't reach the driver.
664
+ The CLI exits 0 when no check failed, 1 when one did and 2 when it can't reach the driver. A
665
+ driver booted with a control token is probed with it: `--token`, by default
666
+ `$AGENTENV_CONTROL_TOKEN`, or `run_probe(..., token=...)`. Given one, the probe also checks that
667
+ `computer-ready`'s status refuses a call without it and with a wrong one.
648
668
 
649
669
  | Level | Checks | Needs on the computer |
650
670
  |---|---|---|
@@ -730,8 +750,10 @@ class MailApp(ComputerAppEnvironment):
730
750
  naming one the app declares no install for is refused when the class is read. A step reads
731
751
  `self.build_settings`; a deploy's `self.settings` aren't known at build, and reading one raises
732
752
  `AttributeError` (`python -m agentenv_protocol.computer.run_install_step <file>:<Class> <method>`).
733
- - **Its skill** is a file of its folder (`skill = "skill.md"`) or an `@agent_skill` method, not both;
734
- its data hooks are the usual `@add_data`, `@get_data` and `@reset_data`.
753
+ - **Its skill** is a file of its folder (`skill = "skill.md"`) or an `@agent_skill` method, not both.
754
+ - **Its data** is under the `paths` its OS's decorator names, relative to the session user's home,
755
+ none inside another (`@linux(..., paths=["Mail", ".config/mail/accounts.json"])`), or what its own
756
+ `@add_data`, `@get_data` and `@reset_data` hooks serve instead (below).
735
757
  - **`Settings`** is typed; a computer hands each app the values a deploy gives it, as one JSON value
736
758
  in `APP_SETTINGS_ENV` (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads
737
759
  its own, by `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.
@@ -757,6 +779,18 @@ folder: the computer is ready once every app's server answers and each `boot` ap
757
779
  no launch, says it's ready; a `demand` app counts once something launches it. What the server does
758
780
  in the session (`agentenv_protocol.computer.session`) is Linux's.
759
781
 
782
+ The server serves the app's data plane and its skill. With `paths` alone, `data/add` writes the files
783
+ it's given under them and refuses any other path, or one a link leads out of them; `data/get`
784
+ returns the files there, a file tree; and `data/reset` puts them back as the server found them when
785
+ it first started on this boot, which is the app as installed. Hooks serve what they mark instead.
786
+ A hook's `data/add` parts come as they were sent, inline or as a read grant, and
787
+ `part_bytes(part)` returns a part's bytes either way. Either way the base class takes data calls and
788
+ launches one at a time, runs a load and a reset with the app stopped and relaunches it after,
789
+ follows a failed load with a reset, and fails a call that runs past `data_timeout_s` (300 s); a sync
790
+ hook runs in a thread, which the reset and relaunch after it wait for. The driver fronts its apps:
791
+ its card lists each one's card as a child environment, rooted at `/svc/<app>/`, where it forwards
792
+ requests to the app's server.
793
+
760
794
  `MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
761
795
  env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
762
796
  JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
@@ -227,6 +227,23 @@ async def display_get(self) -> dict: ...
227
227
  async def display_set(self, width: int, height: int) -> dict: ...
228
228
  ```
229
229
 
230
+ ### Framework-only routes and the control token
231
+
232
+ Everything an environment serves but its MCP endpoint and its card is agent-env's own: the data
233
+ plane, every `@extension` route and the tool-access routes. When agent-env deploys an environment
234
+ that takes one, it boots it with a per-deploy token in `AGENTENV_CONTROL_TOKEN`
235
+ (`CONTROL_TOKEN_ENV`), and the SDK then refuses a call to those routes that doesn't carry it as
236
+ `Authorization: Bearer <token>`, answering 401 with the code `unauthorized` before the route runs.
237
+ The MCP endpoint and the card stay open, so an agent reaches the environment's tools as before and
238
+ nothing else. Without the variable, as when the environment runs on its own, every route is open.
239
+ `control_guard()` wraps a route an environment adds itself the same way, as a computer driver does
240
+ its `/svc/<app>/` routes.
241
+
242
+ On the client side each call takes `headers`, and `client.control_headers(token)` builds them.
243
+ Code that names none runs under `client.authorized({env_url: client.control_headers(token)})`:
244
+ within it, a call to an address under `env_url` sends those headers. agent-env runs every task
245
+ step, a verifier's `verify` included, under such a grant for each environment it deployed.
246
+
230
247
  ### Tool access without a gateway
231
248
 
232
249
  An environment agents reach directly, with no gateway in front, can enforce per-role tool access
@@ -505,8 +522,8 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
505
522
  or a `transfers.ReadObject` read grant under the marker's `read`, and writes them atomically.
506
523
  `data/get` returns the files under the `paths` it's given, relative to the home, or under the
507
524
  card's `default_data_paths` (`Desktop`, `Documents`, `Downloads`) when it's given none, inline or
508
- uploaded under a `write_namespace` grant. An environment whose `@get_data` hook takes no `paths`
509
- refuses them.
525
+ uploaded under a `write_namespace` grant, leaving out the paths its apps keep their data in. An
526
+ environment whose `@get_data` hook takes no `paths` refuses them.
510
527
  - **The card** (the name from `ENVIRONMENT_NAME`, then `@computer_driver`'s), and per-role tool
511
528
  access (`tool_access` is on).
512
529
 
@@ -588,7 +605,10 @@ report = await run_probe("http://localhost:18765", "full") # a ProbeReport
588
605
  failed = [c for c in report.checks if c.status == "failed"]
589
606
  ```
590
607
 
591
- The CLI exits 0 when no check failed, 1 when one did and 2 when it can't reach the driver.
608
+ The CLI exits 0 when no check failed, 1 when one did and 2 when it can't reach the driver. A
609
+ driver booted with a control token is probed with it: `--token`, by default
610
+ `$AGENTENV_CONTROL_TOKEN`, or `run_probe(..., token=...)`. Given one, the probe also checks that
611
+ `computer-ready`'s status refuses a call without it and with a wrong one.
592
612
 
593
613
  | Level | Checks | Needs on the computer |
594
614
  |---|---|---|
@@ -674,8 +694,10 @@ class MailApp(ComputerAppEnvironment):
674
694
  naming one the app declares no install for is refused when the class is read. A step reads
675
695
  `self.build_settings`; a deploy's `self.settings` aren't known at build, and reading one raises
676
696
  `AttributeError` (`python -m agentenv_protocol.computer.run_install_step <file>:<Class> <method>`).
677
- - **Its skill** is a file of its folder (`skill = "skill.md"`) or an `@agent_skill` method, not both;
678
- its data hooks are the usual `@add_data`, `@get_data` and `@reset_data`.
697
+ - **Its skill** is a file of its folder (`skill = "skill.md"`) or an `@agent_skill` method, not both.
698
+ - **Its data** is under the `paths` its OS's decorator names, relative to the session user's home,
699
+ none inside another (`@linux(..., paths=["Mail", ".config/mail/accounts.json"])`), or what its own
700
+ `@add_data`, `@get_data` and `@reset_data` hooks serve instead (below).
679
701
  - **`Settings`** is typed; a computer hands each app the values a deploy gives it, as one JSON value
680
702
  in `APP_SETTINGS_ENV` (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads
681
703
  its own, by `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.
@@ -701,6 +723,18 @@ folder: the computer is ready once every app's server answers and each `boot` ap
701
723
  no launch, says it's ready; a `demand` app counts once something launches it. What the server does
702
724
  in the session (`agentenv_protocol.computer.session`) is Linux's.
703
725
 
726
+ The server serves the app's data plane and its skill. With `paths` alone, `data/add` writes the files
727
+ it's given under them and refuses any other path, or one a link leads out of them; `data/get`
728
+ returns the files there, a file tree; and `data/reset` puts them back as the server found them when
729
+ it first started on this boot, which is the app as installed. Hooks serve what they mark instead.
730
+ A hook's `data/add` parts come as they were sent, inline or as a read grant, and
731
+ `part_bytes(part)` returns a part's bytes either way. Either way the base class takes data calls and
732
+ launches one at a time, runs a load and a reset with the app stopped and relaunches it after,
733
+ follows a failed load with a reset, and fails a call that runs past `data_timeout_s` (300 s); a sync
734
+ hook runs in a thread, which the reset and relaunch after it wait for. The driver fronts its apps:
735
+ its card lists each one's card as a child environment, rooted at `/svc/<app>/`, where it forwards
736
+ requests to the app's server.
737
+
704
738
  `MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
705
739
  env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
706
740
  JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.419"
3
+ version = "0.1.421"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -53,6 +53,7 @@ from .manifest import (
53
53
  manifest_compatible,
54
54
  )
55
55
  from .types import (
56
+ CONTROL_TOKEN_ENV,
56
57
  DATA_OBJECTS_EXTENSION_URI,
57
58
  INTAKE_EXTENSION_URI,
58
59
  MCP_PATH,
@@ -161,6 +162,7 @@ __all__ = [
161
162
  "ExtensionCallError",
162
163
  "PROTOCOL_VERSION",
163
164
  "RPC_PATH",
165
+ "CONTROL_TOKEN_ENV",
164
166
  "WELL_KNOWN_PATH",
165
167
  "METHOD_RESET",
166
168
  "METHOD_ADD",
@@ -5,6 +5,7 @@ import datetime
5
5
  import decimal
6
6
  import enum
7
7
  import functools
8
+ import hmac
8
9
  import inspect
9
10
  import json
10
11
  import logging
@@ -14,7 +15,8 @@ import sys
14
15
  import types
15
16
  import uuid
16
17
  from abc import ABC, abstractmethod
17
- from typing import Annotated, Any, Callable, ClassVar, Literal, NamedTuple, Union, get_args, get_origin, get_type_hints
18
+ from typing import (Annotated, Any, Callable, ClassVar, Literal, NamedTuple, Optional, Union, get_args, get_origin,
19
+ get_type_hints)
18
20
 
19
21
  from pydantic import TypeAdapter, ValidationError
20
22
  from pydantic.fields import FieldInfo
@@ -27,7 +29,7 @@ from .tool_access import DISABLE as TOOL_DISABLE, ENABLE as TOOL_ENABLE, PATHS a
27
29
  from .tool_access import extensions as tool_access_extensions
28
30
  from .tool_context import EMPTY, ToolContext, bound, context_from_http, context_from_mcp
29
31
  from .transfers import WriteNamespaceGrant
30
- from .types import DATA_OBJECTS_EXTENSION_URI, MCP_TRANSPORT, SKILL_EXTENSION_URI, METHOD_ADD, METHOD_GET, METHOD_RESET, RPC_PATH, WELL_KNOWN_PATH, AddDataRequest, AddDataResponse, EnvironmentCapabilities, EnvironmentCard, EnvironmentExtension, EnvironmentInterface, EnvironmentTool, GetDataResponse, ResetDataResponse, error_body
32
+ from .types import CONTROL_TOKEN_ENV, DATA_OBJECTS_EXTENSION_URI, MCP_TRANSPORT, SKILL_EXTENSION_URI, METHOD_ADD, METHOD_GET, METHOD_RESET, RPC_PATH, WELL_KNOWN_PATH, AddDataRequest, AddDataResponse, EnvironmentCapabilities, EnvironmentCard, EnvironmentExtension, EnvironmentInterface, EnvironmentTool, GetDataResponse, ResetDataResponse, error_body
31
33
 
32
34
  logger = logging.getLogger(__name__)
33
35
 
@@ -342,10 +344,11 @@ class AgentEnvApplication(ABC):
342
344
  if isinstance(self.handler, AgentEnvEnvironment):
343
345
  self.handler._application = self
344
346
  self.environment_card = self._card_for(app)
345
- self._add_route(app, rpc_url, ["POST"], self._dispatch)
347
+ guard = control_guard()
348
+ self._add_route(app, rpc_url, ["POST"], guard(self._dispatch))
346
349
  self._add_route(app, card_url, ["GET"], self._serve_card)
347
350
  for path, method, handler in self._ext_routes:
348
- self._add_route(app, path, [method], handler)
351
+ self._add_route(app, path, [method], guard(handler))
349
352
  for descriptor, fn in self._tools:
350
353
  self._register_tool(app, descriptor, fn)
351
354
  for resource in self._resources:
@@ -376,7 +379,10 @@ class AgentEnvApplication(ABC):
376
379
  return self.environment_card
377
380
 
378
381
  async def _serve_card(self, request: Request) -> Response:
379
- return _json_response(self.environment_card.model_dump())
382
+ card = self.environment_card
383
+ if isinstance(self.handler, AgentEnvEnvironment) and (children := await self.handler.children_environments()):
384
+ card = card.model_copy(update={"children_environments": children})
385
+ return _json_response(card.model_dump())
380
386
 
381
387
  async def update_tool(self, name: str, *, description: str) -> None:
382
388
  """Change a served tool's description after the mount: on the card and in tools/list alike."""
@@ -662,6 +668,11 @@ class AgentEnvEnvironment:
662
668
  """Declaration-only extensions to advertise, like ``intake/v1``: a promise with params and no handler."""
663
669
  return []
664
670
 
671
+ async def children_environments(self) -> Optional[list[EnvironmentCard]]:
672
+ """The environments this one fronts, as each serves its card with its paths under this one's, listed on the card
673
+ each time it's served; None for none."""
674
+ return None
675
+
665
676
  def served_extensions(self, extensions: list[EnvironmentExtension]) -> list[EnvironmentExtension]:
666
677
  """The ``@extension`` handlers' extensions to serve, chosen at mount: one left out is not served, and of one served
667
678
  with ``op``, only the methods its returned descriptor still lists."""
@@ -1535,6 +1546,25 @@ def _error_response(error: AgentEnvError) -> Response:
1535
1546
  return _json_response(error_body(**error.fields()), error.status)
1536
1547
 
1537
1548
 
1549
+ def control_guard() -> Callable[[Callable], Callable]:
1550
+ """What wraps a framework-only route, everything an env serves but its MCP endpoint and its card: with
1551
+ ``CONTROL_TOKEN_ENV`` set, a call that doesn't carry that token as its bearer is refused before the route runs;
1552
+ without it, nothing."""
1553
+ token = os.environ.get(CONTROL_TOKEN_ENV)
1554
+ if not token:
1555
+ return lambda handler: handler
1556
+ expected = f"Bearer {token}".encode()
1557
+
1558
+ def guard(handler: Callable) -> Callable:
1559
+ async def guarded(request: Request) -> Response:
1560
+ if not hmac.compare_digest(request.headers.get("authorization", "").encode(), expected):
1561
+ return _error_response(AgentEnvError(
1562
+ "unauthorized", "this route is agent-env's own: it takes the env's control token", status=401))
1563
+ return await handler(request)
1564
+ return guarded
1565
+ return guard
1566
+
1567
+
1538
1568
  def _tool_access_handler(access: ToolAccess, action: str) -> Callable:
1539
1569
  async def handler(request: Request) -> Response:
1540
1570
  raw = await request.body()
@@ -1,7 +1,12 @@
1
- """Data-plane wire protocol client (JSON-RPC) for servers implementing the AgentEnv protocol."""
1
+ """Data-plane wire protocol client (JSON-RPC) for servers implementing the AgentEnv protocol. A call to an env's
2
+ framework-only routes carries its control token: as the call's ``headers`` (``control_headers``), or, for code that
3
+ names none, from the grant it runs under (``authorized``)."""
2
4
  from __future__ import annotations
3
5
 
6
+ import contextlib
4
7
  import logging
8
+ from collections.abc import Iterator
9
+ from contextvars import ContextVar
5
10
 
6
11
  import httpx
7
12
 
@@ -11,6 +16,9 @@ from .types import INTAKE_EXTENSION_URI, MCP_PATH, MCP_TRANSPORT, METHOD_ADD, ME
11
16
 
12
17
  logger = logging.getLogger(__name__)
13
18
 
19
+ #: The headers that carry the control token of each env address the running code is granted, by address.
20
+ _GRANTS: ContextVar[dict[str, dict[str, str]]] = ContextVar("agentenv_control_grants", default={})
21
+
14
22
 
15
23
  class ExtensionCallError(httpx.HTTPStatusError):
16
24
  """A non-2xx answer from an extension method, with the error fields the server sent. A body without them
@@ -35,10 +43,45 @@ def _error_fields(response: httpx.Response) -> dict:
35
43
  return error if isinstance(error, dict) else {}
36
44
 
37
45
 
38
- async def _rpc(base_url: str, method: str, params: dict, timeout: int, verify: bool) -> dict:
46
+ def control_headers(token: str | None) -> dict[str, str]:
47
+ """The headers that carry an env's control token to its framework-only routes; none without one."""
48
+ return {"Authorization": f"Bearer {token}"} if token else {}
49
+
50
+
51
+ @contextlib.contextmanager
52
+ def authorized(grants: dict[str | None, dict[str, str]]) -> Iterator[None]:
53
+ """Within it, a call to an address under one of ``grants``, an env's address and the headers that carry its
54
+ control token (``control_headers``), sends those headers when the call names none of its own: how the steps
55
+ agent-env runs, and the verifiers they call, reach an env's framework-only routes as written."""
56
+ granted = _GRANTS.set({**_GRANTS.get(),
57
+ **{url.rstrip("/"): sent for url, sent in grants.items() if url and sent}})
58
+ try:
59
+ yield
60
+ finally:
61
+ _GRANTS.reset(granted)
62
+
63
+
64
+ def request_headers(url: str, headers: dict[str, str] | None = None) -> dict[str, str]:
65
+ """What a call to ``url`` sends: ``headers`` when it names them, else the headers its grant gives
66
+ (``authorized``), if any."""
67
+ if headers is not None:
68
+ return headers
69
+ return granted_headers(url, _GRANTS.get())
70
+
71
+
72
+ def granted_headers(url: str, grants: dict[str | None, dict[str, str]]) -> dict[str, str]:
73
+ """What ``grants``, env addresses and the headers that carry their control tokens, give a call to ``url``: those of
74
+ the address it is or is under, and none anywhere else."""
75
+ return next((sent for base, sent in grants.items()
76
+ if base and (url == base.rstrip("/") or url.startswith(f"{base.rstrip('/')}/"))), {})
77
+
78
+
79
+ async def _rpc(base_url: str, method: str, params: dict, timeout: int, verify: bool,
80
+ headers: dict[str, str] | None) -> dict:
39
81
  request = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
82
+ url = f"{base_url}{RPC_PATH}"
40
83
  async with httpx.AsyncClient(verify=verify) as client:
41
- response = await client.post(f"{base_url}{RPC_PATH}", json=request, timeout=timeout)
84
+ response = await client.post(url, json=request, timeout=timeout, headers=request_headers(url, headers))
42
85
  response.raise_for_status()
43
86
  body = response.json()
44
87
  if "error" in body:
@@ -47,18 +90,20 @@ async def _rpc(base_url: str, method: str, params: dict, timeout: int, verify: b
47
90
  return body["result"]
48
91
 
49
92
 
50
- async def reset_data(base_url: str, timeout: int = 30, verify: bool = True) -> ResetDataResponse:
51
- return ResetDataResponse.model_validate(await _rpc(base_url, METHOD_RESET, {}, timeout, verify))
93
+ async def reset_data(base_url: str, timeout: int = 30, verify: bool = True, *,
94
+ headers: dict[str, str] | None = None) -> ResetDataResponse:
95
+ return ResetDataResponse.model_validate(await _rpc(base_url, METHOD_RESET, {}, timeout, verify, headers))
52
96
 
53
97
 
54
- async def add_data(base_url: str, parts: list[Part], timeout: int = 120, verify: bool = True) -> AddDataResponse:
98
+ async def add_data(base_url: str, parts: list[Part], timeout: int = 120, verify: bool = True, *,
99
+ headers: dict[str, str] | None = None) -> AddDataResponse:
55
100
  params = {"parts": [p.model_dump(mode="json", exclude_none=True) for p in parts]}
56
- return AddDataResponse.model_validate(await _rpc(base_url, METHOD_ADD, params, timeout, verify))
101
+ return AddDataResponse.model_validate(await _rpc(base_url, METHOD_ADD, params, timeout, verify, headers))
57
102
 
58
103
 
59
104
  async def get_data(
60
105
  base_url: str, timeout: int = 30, verify: bool = True, *, write_namespace: WriteNamespaceGrant | None = None,
61
- paths: list[str] | None = None,
106
+ paths: list[str] | None = None, headers: dict[str, str] | None = None,
62
107
  ) -> GetDataResponse:
63
108
  """`data/get`. A server advertising `DATA_OBJECTS_EXTENSION_URI` may be handed `write_namespace` to upload
64
109
  its export under; `uploaded_object_path` reads from the answer where it did. `paths` names what a computer
@@ -66,7 +111,7 @@ async def get_data(
66
111
  params = {} if write_namespace is None else {"write_namespace": write_namespace.model_dump(mode="json", exclude_none=True)}
67
112
  if paths is not None:
68
113
  params["paths"] = paths
69
- return GetDataResponse.model_validate(await _rpc(base_url, METHOD_GET, params, timeout, verify))
114
+ return GetDataResponse.model_validate(await _rpc(base_url, METHOD_GET, params, timeout, verify, headers))
70
115
 
71
116
 
72
117
  async def get_card(base_url: str, timeout: int = 10, verify: bool = True) -> dict:
@@ -147,6 +192,12 @@ def intake_declaration(card: dict) -> dict | None:
147
192
  return params if params else None
148
193
 
149
194
 
195
+ def operations(card: dict) -> list[str] | None:
196
+ """The data-plane methods a card declares it serves (``capabilities.operations``), or None when it declares none,
197
+ as an older card doesn't."""
198
+ return (card.get("capabilities") or {}).get("operations")
199
+
200
+
150
201
  def find_tool(card: dict, name: str) -> dict | None:
151
202
  """Return the MCP tool advertised under `name`, or None.
152
203
 
@@ -174,7 +225,8 @@ def find_child(card: dict, name: str) -> dict | None:
174
225
  return None
175
226
 
176
227
 
177
- async def get_interface_manifest(base_url: str, interface: str, endpoint: str = INTERFACE_MANIFEST_PATH, timeout: int = 30, verify: bool = True):
228
+ async def get_interface_manifest(base_url: str, interface: str, endpoint: str = INTERFACE_MANIFEST_PATH, timeout: int = 30, verify: bool = True, *,
229
+ headers: dict[str, str] | None = None):
178
230
  """Fetch one interface's manifest from a server's manifest index endpoint.
179
231
 
180
232
  ``endpoint`` is the index path — pass the one advertised on the card under
@@ -185,12 +237,13 @@ async def get_interface_manifest(base_url: str, interface: str, endpoint: str =
185
237
  """
186
238
  url = f"{base_url.rstrip('/')}{endpoint}/{interface}"
187
239
  async with httpx.AsyncClient(verify=verify) as client:
188
- response = await client.get(url, timeout=timeout)
240
+ response = await client.get(url, timeout=timeout, headers=request_headers(url, headers))
189
241
  response.raise_for_status()
190
242
  return response.json()
191
243
 
192
244
 
193
- async def invoke_extension(base_url: str, card: dict, uri: str, params: dict | None = None, timeout: int = 30, verify: bool = True, *, method: str | None = None):
245
+ async def invoke_extension(base_url: str, card: dict, uri: str, params: dict | None = None, timeout: int = 30, verify: bool = True, *, method: str | None = None,
246
+ headers: dict[str, str] | None = None):
194
247
  """Invoke a card extension via its advertised REST endpoint.
195
248
 
196
249
  Reads the endpoint and HTTP verb of the extension method named `method` from the card, so the
@@ -209,11 +262,12 @@ async def invoke_extension(base_url: str, card: dict, uri: str, params: dict | N
209
262
  if not op["endpoint"]:
210
263
  raise ValueError(f"extension {uri} has no endpoint in params")
211
264
  url = f"{base_url}{op['endpoint']}"
265
+ sent = request_headers(url, headers)
212
266
  async with httpx.AsyncClient(verify=verify) as client:
213
267
  if op["method"] == "GET":
214
- response = await client.get(url, params=params or {}, timeout=timeout)
268
+ response = await client.get(url, params=params or {}, timeout=timeout, headers=sent)
215
269
  else:
216
- response = await client.request(op["method"], url, json=params or {}, timeout=timeout)
270
+ response = await client.request(op["method"], url, json=params or {}, timeout=timeout, headers=sent)
217
271
  if response.status_code >= 400:
218
272
  raise ExtensionCallError(response)
219
273
  return response.json()
@@ -67,6 +67,7 @@ from .operations import (
67
67
  type_text,
68
68
  write_files,
69
69
  )
70
+ from .dataplane import part_bytes
70
71
  from .keys import PRINTABLE_KEYS
71
72
  from .manifest import ManifestSegment, RecordingManifest
72
73
  from .platform import Platform
@@ -107,7 +108,7 @@ __all__ = [
107
108
  "ComputerAppEnvironment", "computer_app", "install", "linux", "install_step", "ready_check", "Linux", "Service",
108
109
  "Launch", "Ready", "Packages", "AptSource", "Download", "RunScript",
109
110
  "AppDeclaration", "AppOs",
110
- "APP_SETTINGS_ENV",
111
+ "APP_SETTINGS_ENV", "part_bytes",
111
112
  "Display", "Image", "Effect", "ExecResult", "ReadyCheck", "Platform", "ComputerSettings", "SETTINGS_ENV",
112
113
  "PRINTABLE_KEYS",
113
114
  "display", "set_display", "capture", "click", "drag", "scroll", "type_text", "press",