agentenv-framework-protocol 0.1.428__tar.gz → 0.1.432__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 (117) hide show
  1. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/PKG-INFO +59 -6
  2. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/README.md +58 -5
  3. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/pyproject.toml +1 -1
  4. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/__init__.py +6 -0
  5. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/extensions.py +68 -1
  6. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/framework.py +44 -4
  7. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/tasks/v1.py +35 -0
  8. agentenv_framework_protocol-0.1.432/src/agentenv_protocol/computer/_with_fds.py +26 -0
  9. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/app.py +24 -3
  10. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/app_runtime.py +29 -8
  11. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/session.py +10 -3
  12. agentenv_framework_protocol-0.1.432/src/agentenv_protocol/website_browser.py +296 -0
  13. agentenv_framework_protocol-0.1.432/tests/test_a2a_cdp_config.py +207 -0
  14. agentenv_framework_protocol-0.1.432/tests/test_website_browser.py +361 -0
  15. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/.gitignore +0 -0
  16. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/LICENSE +0 -0
  17. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/NOTICE +0 -0
  18. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/THIRD_PARTY_NOTICES.md +0 -0
  19. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/__init__.py +0 -0
  20. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/advanced_agent.py +0 -0
  21. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/basic_agent.py +0 -0
  22. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/custom_extensions_agent.py +0 -0
  23. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/multimodal_agent.py +0 -0
  24. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/reference_model.py +0 -0
  25. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/examples/streaming_agent.py +0 -0
  26. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/__init__.py +0 -0
  27. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  28. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  29. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  30. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  31. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  32. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  33. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  34. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/agent_env_environment.py +0 -0
  35. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/client.py +0 -0
  36. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/__init__.py +0 -0
  37. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/_declare.py +0 -0
  38. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/_linux.py +0 -0
  39. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/_load.py +0 -0
  40. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/app_data.py +0 -0
  41. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/apps.py +0 -0
  42. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/card.py +0 -0
  43. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/checks.py +0 -0
  44. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  45. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  46. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  47. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  48. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  49. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  50. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  51. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/dataplane.py +0 -0
  52. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/driver.py +0 -0
  53. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/errors.py +0 -0
  54. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/extensions.py +0 -0
  55. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/frames.py +0 -0
  56. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/keys.py +0 -0
  57. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/launch.py +0 -0
  58. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/manifest.py +0 -0
  59. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/operations.py +0 -0
  60. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/platform.py +0 -0
  61. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/queue.py +0 -0
  62. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/recording.py +0 -0
  63. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/results.py +0 -0
  64. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  65. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/runtime.py +0 -0
  66. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/serve.py +0 -0
  67. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/serve_app.py +0 -0
  68. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/settings.py +0 -0
  69. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/skill.py +0 -0
  70. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/spaces.py +0 -0
  71. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/computer/tools.py +0 -0
  72. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/errors.py +0 -0
  73. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/manifest.py +0 -0
  74. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/preflight.py +0 -0
  75. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/tool_access.py +0 -0
  76. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/tool_client.py +0 -0
  77. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/tool_context.py +0 -0
  78. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/transfers.py +0 -0
  79. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/src/agentenv_protocol/types.py +0 -0
  80. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/computer_fake.py +0 -0
  81. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/probe_fake.py +0 -0
  82. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_agent.py +0 -0
  83. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_agent_examples.py +0 -0
  84. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_object_transfers.py +0 -0
  85. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_peer_agents.py +0 -0
  86. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_proxy.py +0 -0
  87. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_staging.py +0 -0
  88. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_a2a_trajectory_log.py +0 -0
  89. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_agent_env_environment.py +0 -0
  90. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_client.py +0 -0
  91. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_app_data.py +0 -0
  92. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_app_server.py +0 -0
  93. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_conformance.py +0 -0
  94. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_conformance_checks.py +0 -0
  95. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_conformance_recording.py +0 -0
  96. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_dataplane.py +0 -0
  97. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_declare.py +0 -0
  98. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_driver.py +0 -0
  99. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_extensions.py +0 -0
  100. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_frames.py +0 -0
  101. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_queue.py +0 -0
  102. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_recording.py +0 -0
  103. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_serve.py +0 -0
  104. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_computer_spaces.py +0 -0
  105. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_control_token.py +0 -0
  106. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_data_objects.py +0 -0
  107. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_errors.py +0 -0
  108. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_extension_methods.py +0 -0
  109. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_intake.py +0 -0
  110. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_manifest.py +0 -0
  111. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_preflight.py +0 -0
  112. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_tool_access.py +0 -0
  113. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_tool_annotations.py +0 -0
  114. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_tool_client.py +0 -0
  115. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_tool_context.py +0 -0
  116. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/tests/test_tool_context_fastmcp.py +0 -0
  117. {agentenv_framework_protocol-0.1.428 → agentenv_framework_protocol-0.1.432}/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.428
3
+ Version: 0.1.432
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
@@ -743,7 +743,7 @@ class MailApp(ComputerAppEnvironment):
743
743
  - **`launch=Launch(...)`** says how it starts in the desktop session: with it (`at="boot"`) or when
744
744
  the agent opens it (`"demand"`). `ready=Ready(...)` says when it's ready, within `timeout_s` of its
745
745
  launch: a window matching `launch.window` by its class, instance or title, a port answering on
746
- loopback, or a command exiting 0. `@ready_check` marks a method for readiness that needs logic (a
746
+ loopback, or a command exiting 0. `env` sets variables in the app's environment, over its server's. `@ready_check` marks a method for readiness that needs logic (a
747
747
  sync one runs in a thread), and `@install_step(scope=...)` one a computer runs as it installs the
748
748
  app, after its run scripts, offline and without the class's `__init__`. Each runs on every OS the
749
749
  app installs on, or on those its `os=` names (`@install_step(os="linux")`, `@ready_check(os=[...])`);
@@ -772,9 +772,10 @@ desktop session, and `ready` (GET: an `AppReadiness` of `ready`, `problems`, `la
772
772
  and `timeout_s`), which runs the app's readiness checks then, at once, each within 10 s. A server
773
773
  that can't start, or whose start in the session fails (adding the launcher, say), writes why to
774
774
  `.agentenv/server.failure` in the folder and exits, and the driver reports that in its place. Once
775
- the session is up the server adds the app's launcher to the session user's menu, which runs `python
776
- -m agentenv_protocol.computer.launch <folder>` to ask the server, so the server starts every copy of
777
- the app, and launches a `boot` app, once per boot. The driver finds each app by the socket in its
775
+ the X display is up, when `DISPLAY` names one, the server adds the app's launcher to the session user's
776
+ menu, which runs `python -m agentenv_protocol.computer.launch <folder>` to ask the server, so the
777
+ server starts every copy of the app, and launches a `boot` app, once per boot. An app that runs with
778
+ no desktop, a website browser alone, gets no launcher. The driver finds each app by the socket in its
778
779
  folder: the computer is ready once every app's server answers and each `boot` app, and each app with
779
780
  no launch, says it's ready; a `demand` app counts once something launches it. What the server does
780
781
  in the session (`agentenv_protocol.computer.session`) is Linux's.
@@ -795,6 +796,44 @@ requests to the app's server.
795
796
  env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
796
797
  JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
797
798
 
799
+ ### Website browsers
800
+
801
+ `agentenv_protocol.website_browser.WebsiteBrowserEnvironment` is the computer app class for a Chromium
802
+ website browser, run inside a computer or alone. A subclass declares its build as any app does, its
803
+ `Launch` running the browser itself. It sets `version`, its build, and `policy_dir`, where that build
804
+ reads managed policies; `policies` adds its own over agentenv's, and `profile` is its profile folder
805
+ under the session user's home (`.config/chromium` by default), whose contents from its install steps
806
+ are the profile's template. The base class adds:
807
+
808
+ - **Its settings**, `BrowserSettings`, which a subclass's `Settings` extends: `start_urls`, opened in
809
+ tabs at launch, the first active (`["about:blank"]` by default; none may start with `-`), headless
810
+ the first from its command line and the rest over its pipe, since headless Chromium takes one; `window`,
811
+ 1280 × 800, alone only; `locale`; and `time_zone`, set as `TZ` in its launch's `env`.
812
+ - **The flags agentenv owns**, after the subclass's command and before the start tabs: its profile,
813
+ `--remote-debugging-pipe`, `--no-first-run`, `--no-default-browser-check` and `--lang`. With
814
+ `AGENTENV_SITE_PROXY` set, it goes out through that proxy, with WebRTC's non-proxied UDP off.
815
+ Alone it bypasses nothing, loopback included; in a computer it bypasses loopback (`127.0.0.1`,
816
+ `[::1]`, `localhost`), so the apps beside it stay reachable while `*.localhost` names still go
817
+ through. Alone (`AGENTENV_BROWSER_ALONE=1`) it also takes `--window-size` and `--headless=new`,
818
+ except on the open web (`AGENTENV_BROWSER_OPEN_WEB=1`), and refuses to start with no proxy.
819
+ `declaration()` refuses a subclass whose command carries any of these, or `--remote-allow-origins`, or
820
+ any other way to open a debugging port.
821
+ - **Alone, no desktop**: its launch matches and waits on no window, its server adds no launcher and,
822
+ without `DISPLAY`, waits for no display, and its ready checks say when it's ready.
823
+ - **`write_policies`**, an install step that writes agentenv's unattended-use policies (`POLICIES`)
824
+ and the subclass's to `agentenv.json` in `policy_dir`.
825
+ - **A debugging pipe, not a port.** Its server launches it with a new pipe's ends as its fds 3 and 4
826
+ (`session.launch`'s `fds`, moved into place by `computer/_with_fds.py`, run alone with `-I -S`) and keeps the
827
+ other ends as `self.devtools`, a `DevToolsPipe`: `await self.devtools.call(method, params)` returns
828
+ the result or raises `DevToolsError`, and `ConnectionError` with no browser connected. No port
829
+ listens and no `DevToolsActivePort` is written, so nothing else in the computer can reach it over
830
+ CDP. Since only the server that launched it holds the pipe, a server that restarts stops it and
831
+ launches it again. The ready check `devtools_answers` asks `Browser.getVersion` over the pipe.
832
+ - **`browser/v1`** on its card, params only: `chromium` (its `version`), `clock: "real"` and, alone,
833
+ `window`.
834
+ - **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
835
+ `~/Downloads`, folders it makes.
836
+
798
837
  ## A2A agent framework
799
838
 
800
839
  The distribution exposes two unrelated decorators named `extension`:
@@ -909,7 +948,7 @@ protocol. `TaskRequest` and `TaskResult` are framework boundary types; the
909
948
  executor maps them to and from the wire-level `a2a.types.Task` lifecycle.
910
949
 
911
950
  `TaskRequest` is a frozen record whose nested JSON values are detached copies.
912
- Its `config`, `mcp_servers`, `skills`, `metadata`, and inbound `DataPart.data`
951
+ Its `config`, `mcp_servers`, `cdp_endpoints`, `skills`, `metadata`, and inbound `DataPart.data`
913
952
  retain their declared `dict`/`list` types, so normal Pydantic serialization,
914
953
  copying, and `json.dumps(...)` work. `TaskRequest.trajectory` is the exception:
915
954
  it is the task's live trajectory log, described below. Within `tasks.v1`, new
@@ -1228,6 +1267,20 @@ tools with (agent-env relays the env card's name: the MultiEnv's declared name,
1228
1267
  `env` + 4 random digits, giving e.g. `mcp__env4821__<tool>`); when absent the agent mints
1229
1268
  `mcp_<8 hex>`.
1230
1269
 
1270
+ `CDP_CONFIG_V1` gives an agent the CDP endpoints it drives itself. `add` takes
1271
+ `name`, `http_url` (`http` or `https`, for clients that discover through
1272
+ `/json/version`) and `ws_url` (`ws` or `wss`, for clients that connect
1273
+ directly), with optional `headers` and `metadata`; adding a name again replaces
1274
+ it. `GET /ext/cdp-config` returns `{"cdp_endpoints": {name: {"has_headers",
1275
+ "metadata"}}}`: neither the URLs nor the header values, since either can carry
1276
+ the endpoint's credential. `DELETE /ext/cdp-config` with `{"name"}` removes one;
1277
+ removing a name that isn't there is not an error. Every task's
1278
+ `TaskRequest.cdp_endpoints` holds them in full. `TaskRequest.cdp_endpoint(name=None)`
1279
+ returns the named endpoint, or the only one, and otherwise fails the task as an
1280
+ `infra_error` with `cdp.no_endpoint` (none, or none by that name) or
1281
+ `cdp.ambiguous` (several and no name). It fails the task by raising, so call it
1282
+ before any handler of your own that catches every exception.
1283
+
1231
1284
  `PEER_AGENTS_V1` lets an agent delegate to the other agents in its task. AgentEnv
1232
1285
  sets the routing table with `POST /ext/peer-agents` (`{"peers": [{"name", "url",
1233
1286
  "card", "description"}]}`; an empty list clears it, and names must be unique), and
@@ -687,7 +687,7 @@ class MailApp(ComputerAppEnvironment):
687
687
  - **`launch=Launch(...)`** says how it starts in the desktop session: with it (`at="boot"`) or when
688
688
  the agent opens it (`"demand"`). `ready=Ready(...)` says when it's ready, within `timeout_s` of its
689
689
  launch: a window matching `launch.window` by its class, instance or title, a port answering on
690
- loopback, or a command exiting 0. `@ready_check` marks a method for readiness that needs logic (a
690
+ loopback, or a command exiting 0. `env` sets variables in the app's environment, over its server's. `@ready_check` marks a method for readiness that needs logic (a
691
691
  sync one runs in a thread), and `@install_step(scope=...)` one a computer runs as it installs the
692
692
  app, after its run scripts, offline and without the class's `__init__`. Each runs on every OS the
693
693
  app installs on, or on those its `os=` names (`@install_step(os="linux")`, `@ready_check(os=[...])`);
@@ -716,9 +716,10 @@ desktop session, and `ready` (GET: an `AppReadiness` of `ready`, `problems`, `la
716
716
  and `timeout_s`), which runs the app's readiness checks then, at once, each within 10 s. A server
717
717
  that can't start, or whose start in the session fails (adding the launcher, say), writes why to
718
718
  `.agentenv/server.failure` in the folder and exits, and the driver reports that in its place. Once
719
- the session is up the server adds the app's launcher to the session user's menu, which runs `python
720
- -m agentenv_protocol.computer.launch <folder>` to ask the server, so the server starts every copy of
721
- the app, and launches a `boot` app, once per boot. The driver finds each app by the socket in its
719
+ the X display is up, when `DISPLAY` names one, the server adds the app's launcher to the session user's
720
+ menu, which runs `python -m agentenv_protocol.computer.launch <folder>` to ask the server, so the
721
+ server starts every copy of the app, and launches a `boot` app, once per boot. An app that runs with
722
+ no desktop, a website browser alone, gets no launcher. The driver finds each app by the socket in its
722
723
  folder: the computer is ready once every app's server answers and each `boot` app, and each app with
723
724
  no launch, says it's ready; a `demand` app counts once something launches it. What the server does
724
725
  in the session (`agentenv_protocol.computer.session`) is Linux's.
@@ -739,6 +740,44 @@ requests to the app's server.
739
740
  env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
740
741
  JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
741
742
 
743
+ ### Website browsers
744
+
745
+ `agentenv_protocol.website_browser.WebsiteBrowserEnvironment` is the computer app class for a Chromium
746
+ website browser, run inside a computer or alone. A subclass declares its build as any app does, its
747
+ `Launch` running the browser itself. It sets `version`, its build, and `policy_dir`, where that build
748
+ reads managed policies; `policies` adds its own over agentenv's, and `profile` is its profile folder
749
+ under the session user's home (`.config/chromium` by default), whose contents from its install steps
750
+ are the profile's template. The base class adds:
751
+
752
+ - **Its settings**, `BrowserSettings`, which a subclass's `Settings` extends: `start_urls`, opened in
753
+ tabs at launch, the first active (`["about:blank"]` by default; none may start with `-`), headless
754
+ the first from its command line and the rest over its pipe, since headless Chromium takes one; `window`,
755
+ 1280 × 800, alone only; `locale`; and `time_zone`, set as `TZ` in its launch's `env`.
756
+ - **The flags agentenv owns**, after the subclass's command and before the start tabs: its profile,
757
+ `--remote-debugging-pipe`, `--no-first-run`, `--no-default-browser-check` and `--lang`. With
758
+ `AGENTENV_SITE_PROXY` set, it goes out through that proxy, with WebRTC's non-proxied UDP off.
759
+ Alone it bypasses nothing, loopback included; in a computer it bypasses loopback (`127.0.0.1`,
760
+ `[::1]`, `localhost`), so the apps beside it stay reachable while `*.localhost` names still go
761
+ through. Alone (`AGENTENV_BROWSER_ALONE=1`) it also takes `--window-size` and `--headless=new`,
762
+ except on the open web (`AGENTENV_BROWSER_OPEN_WEB=1`), and refuses to start with no proxy.
763
+ `declaration()` refuses a subclass whose command carries any of these, or `--remote-allow-origins`, or
764
+ any other way to open a debugging port.
765
+ - **Alone, no desktop**: its launch matches and waits on no window, its server adds no launcher and,
766
+ without `DISPLAY`, waits for no display, and its ready checks say when it's ready.
767
+ - **`write_policies`**, an install step that writes agentenv's unattended-use policies (`POLICIES`)
768
+ and the subclass's to `agentenv.json` in `policy_dir`.
769
+ - **A debugging pipe, not a port.** Its server launches it with a new pipe's ends as its fds 3 and 4
770
+ (`session.launch`'s `fds`, moved into place by `computer/_with_fds.py`, run alone with `-I -S`) and keeps the
771
+ other ends as `self.devtools`, a `DevToolsPipe`: `await self.devtools.call(method, params)` returns
772
+ the result or raises `DevToolsError`, and `ConnectionError` with no browser connected. No port
773
+ listens and no `DevToolsActivePort` is written, so nothing else in the computer can reach it over
774
+ CDP. Since only the server that launched it holds the pipe, a server that restarts stops it and
775
+ launches it again. The ready check `devtools_answers` asks `Browser.getVersion` over the pipe.
776
+ - **`browser/v1`** on its card, params only: `chromium` (its `version`), `clock: "real"` and, alone,
777
+ `window`.
778
+ - **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
779
+ `~/Downloads`, folders it makes.
780
+
742
781
  ## A2A agent framework
743
782
 
744
783
  The distribution exposes two unrelated decorators named `extension`:
@@ -853,7 +892,7 @@ protocol. `TaskRequest` and `TaskResult` are framework boundary types; the
853
892
  executor maps them to and from the wire-level `a2a.types.Task` lifecycle.
854
893
 
855
894
  `TaskRequest` is a frozen record whose nested JSON values are detached copies.
856
- Its `config`, `mcp_servers`, `skills`, `metadata`, and inbound `DataPart.data`
895
+ Its `config`, `mcp_servers`, `cdp_endpoints`, `skills`, `metadata`, and inbound `DataPart.data`
857
896
  retain their declared `dict`/`list` types, so normal Pydantic serialization,
858
897
  copying, and `json.dumps(...)` work. `TaskRequest.trajectory` is the exception:
859
898
  it is the task's live trajectory log, described below. Within `tasks.v1`, new
@@ -1172,6 +1211,20 @@ tools with (agent-env relays the env card's name: the MultiEnv's declared name,
1172
1211
  `env` + 4 random digits, giving e.g. `mcp__env4821__<tool>`); when absent the agent mints
1173
1212
  `mcp_<8 hex>`.
1174
1213
 
1214
+ `CDP_CONFIG_V1` gives an agent the CDP endpoints it drives itself. `add` takes
1215
+ `name`, `http_url` (`http` or `https`, for clients that discover through
1216
+ `/json/version`) and `ws_url` (`ws` or `wss`, for clients that connect
1217
+ directly), with optional `headers` and `metadata`; adding a name again replaces
1218
+ it. `GET /ext/cdp-config` returns `{"cdp_endpoints": {name: {"has_headers",
1219
+ "metadata"}}}`: neither the URLs nor the header values, since either can carry
1220
+ the endpoint's credential. `DELETE /ext/cdp-config` with `{"name"}` removes one;
1221
+ removing a name that isn't there is not an error. Every task's
1222
+ `TaskRequest.cdp_endpoints` holds them in full. `TaskRequest.cdp_endpoint(name=None)`
1223
+ returns the named endpoint, or the only one, and otherwise fails the task as an
1224
+ `infra_error` with `cdp.no_endpoint` (none, or none by that name) or
1225
+ `cdp.ambiguous` (several and no name). It fails the task by raising, so call it
1226
+ before any handler of your own that catches every exception.
1227
+
1175
1228
  `PEER_AGENTS_V1` lets an agent delegate to the other agents in its task. AgentEnv
1176
1229
  sets the routing table with `POST /ext/peer-agents` (`{"peers": [{"name", "url",
1177
1230
  "card", "description"}]}`; an empty list clears it, and names must be unique), and
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.428"
3
+ version = "0.1.432"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -5,6 +5,7 @@ from a2a.types import AgentCapabilities
5
5
  from .extensions import (
6
6
  AGENT_CONFIG_V1,
7
7
  ATTRIBUTION_PROBE_V1,
8
+ CDP_CONFIG_V1,
8
9
  PROXY_V1,
9
10
  INSTALL_V1,
10
11
  MCP_CONFIG_V1,
@@ -16,6 +17,8 @@ from .extensions import (
16
17
  TRIGGERS_V1,
17
18
  AttributionProbeResponse,
18
19
  BundleSkillRequest,
20
+ CdpAddRequest,
21
+ CdpRemoveRequest,
19
22
  ChangelogIncrement,
20
23
  ContextEventsTrajectoryRequest,
21
24
  ContextObjectTrajectoryRequest,
@@ -137,6 +140,7 @@ from ..transfers import (
137
140
  __all__ = [
138
141
  "AGENT_CONFIG_V1",
139
142
  "ATTRIBUTION_PROBE_V1",
143
+ "CDP_CONFIG_V1",
140
144
  "PROXY_V1",
141
145
  "INSTALL_V1",
142
146
  "MCP_CONFIG_V1",
@@ -157,6 +161,8 @@ __all__ = [
157
161
  "AttributionProbeResponse",
158
162
  "BuiltinTools",
159
163
  "BundleSkillRequest",
164
+ "CdpAddRequest",
165
+ "CdpRemoveRequest",
160
166
  "ChangelogIncrement",
161
167
  "ContextEventsTrajectoryRequest",
162
168
  "ContextObjectTrajectoryRequest",
@@ -11,6 +11,7 @@ from dataclasses import dataclass, field
11
11
  from enum import Enum
12
12
  from types import MappingProxyType
13
13
  from typing import Annotated, Any, Literal
14
+ from urllib.parse import urlsplit
14
15
 
15
16
  from pydantic import (
16
17
  BaseModel,
@@ -19,6 +20,7 @@ from pydantic import (
19
20
  PositiveInt,
20
21
  StrictInt,
21
22
  ValidationError,
23
+ field_validator,
22
24
  model_validator,
23
25
  )
24
26
 
@@ -50,7 +52,9 @@ class ExtensionResponse(BaseModel):
50
52
  model_config = ConfigDict(extra="forbid", frozen=True)
51
53
 
52
54
 
53
- _SkillName = Annotated[str, Field(pattern=r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")]
55
+ _NAME_PATTERN = r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
56
+ _SkillName = Annotated[str, Field(pattern=_NAME_PATTERN)]
57
+ _CdpEndpointName = Annotated[str, Field(pattern=_NAME_PATTERN)]
54
58
 
55
59
 
56
60
  class McpAddRequest(ExtensionRequest):
@@ -59,6 +63,43 @@ class McpAddRequest(ExtensionRequest):
59
63
  name: str | None = None
60
64
 
61
65
 
66
+ def _absolute_url(url: str, schemes: tuple[str, ...]) -> str:
67
+ """``url``, once it names one of ``schemes``, a host and, if it has one, a valid port."""
68
+ parts = urlsplit(url)
69
+ try:
70
+ valid = parts.scheme in schemes and bool(parts.hostname) and (parts.port is None or parts.port >= 0)
71
+ except ValueError:
72
+ valid = False
73
+ if not valid:
74
+ raise ValueError(f"must be an absolute {' or '.join(schemes)} URL with a host and a valid port")
75
+ return url
76
+
77
+
78
+ class CdpAddRequest(ExtensionRequest):
79
+ """A CDP endpoint: ``http_url`` for clients that discover through ``/json/version``, ``ws_url`` for clients
80
+ that connect to the browser directly."""
81
+
82
+ name: _CdpEndpointName
83
+ http_url: str
84
+ ws_url: str
85
+ headers: dict[str, str] | None = None
86
+ metadata: dict[str, Any] | None = None
87
+
88
+ @field_validator("http_url")
89
+ @classmethod
90
+ def _http_url(cls, url: str) -> str:
91
+ return _absolute_url(url, ("http", "https"))
92
+
93
+ @field_validator("ws_url")
94
+ @classmethod
95
+ def _ws_url(cls, url: str) -> str:
96
+ return _absolute_url(url, ("ws", "wss"))
97
+
98
+
99
+ class CdpRemoveRequest(ExtensionRequest):
100
+ name: _CdpEndpointName
101
+
102
+
62
103
  class InlineSkillRequest(ExtensionRequest):
63
104
  name: _SkillName
64
105
  description: str
@@ -1101,6 +1142,31 @@ MCP_CONFIG_V1 = ExtensionDefinition(
1101
1142
  },
1102
1143
  )
1103
1144
 
1145
+ CDP_CONFIG_V1 = ExtensionDefinition(
1146
+ uri="urn:agentenv:cdp-config/v1",
1147
+ description="Register CDP endpoints the agent drives itself.",
1148
+ endpoint="/ext/cdp-config",
1149
+ core_operations={
1150
+ "add": _op(
1151
+ "add",
1152
+ "POST",
1153
+ "/ext/cdp-config",
1154
+ SDK,
1155
+ request_model=CdpAddRequest,
1156
+ ),
1157
+ "list": _op(
1158
+ "list", "GET", "/ext/cdp-config", SDK, response_required=("cdp_endpoints",)
1159
+ ),
1160
+ "remove": _op(
1161
+ "remove",
1162
+ "DELETE",
1163
+ "/ext/cdp-config",
1164
+ SDK,
1165
+ request_model=CdpRemoveRequest,
1166
+ ),
1167
+ },
1168
+ )
1169
+
1104
1170
  SKILL_CONFIG_V1 = ExtensionDefinition(
1105
1171
  uri="urn:agentenv:skill-config/v1",
1106
1172
  description="Register skills made available to the agent.",
@@ -1319,6 +1385,7 @@ STANDARD_EXTENSIONS: Mapping[str, ExtensionDefinition] = MappingProxyType(
1319
1385
  for extension in (
1320
1386
  AGENT_CONFIG_V1,
1321
1387
  MCP_CONFIG_V1,
1388
+ CDP_CONFIG_V1,
1322
1389
  SKILL_CONFIG_V1,
1323
1390
  TRAJECTORY_V1,
1324
1391
  SNAPSHOT_V1,
@@ -32,6 +32,7 @@ from ._trajectory_log import MAX_TRAJECTORY_READ_BYTES, TaskTrajectories
32
32
  from ._triggers import TriggerEngine, TriggerError
33
33
  from .extensions import (
34
34
  AGENT_CONFIG_V1,
35
+ CDP_CONFIG_V1,
35
36
  MAX_TRAJECTORY_READ_EVENTS,
36
37
  MCP_CONFIG_V1,
37
38
  PEER_AGENTS_V1,
@@ -41,6 +42,8 @@ from .extensions import (
41
42
  ContextEventsTrajectoryRequest,
42
43
  ContextObjectTrajectoryRequest,
43
44
  ContextTrajectoryRequest,
45
+ CdpAddRequest,
46
+ CdpRemoveRequest,
44
47
  ExtensionActivation,
45
48
  ExtensionDefinition,
46
49
  ImplementationOwner,
@@ -66,6 +69,7 @@ from .tasks.v1 import (
66
69
  TaskProgress,
67
70
  TaskRequest,
68
71
  TaskResult,
72
+ _TaskFailed,
69
73
  )
70
74
  from .tasks.v1 import (
71
75
  DataPart as TaskDataPart,
@@ -541,6 +545,7 @@ class _SdkServices:
541
545
  self.config_model
542
546
  )
543
547
  self.mcp_servers: dict[str, dict[str, Any]] = {}
548
+ self.cdp_endpoints: dict[str, dict[str, Any]] = {}
544
549
  self.task_trajectories = TaskTrajectories()
545
550
  self._context_sessions = _BoundedContextSessions()
546
551
  self.skills: list[Mapping[str, Any]] = []
@@ -616,6 +621,9 @@ class _SdkServices:
616
621
  (AGENT_CONFIG_V1.uri, "get"): self.agent_config_get,
617
622
  (MCP_CONFIG_V1.uri, "add"): self.mcp_add,
618
623
  (MCP_CONFIG_V1.uri, "list"): self.mcp_list,
624
+ (CDP_CONFIG_V1.uri, "add"): self.cdp_add,
625
+ (CDP_CONFIG_V1.uri, "list"): self.cdp_list,
626
+ (CDP_CONFIG_V1.uri, "remove"): self.cdp_remove,
619
627
  (PEER_AGENTS_V1.uri, "set"): self.peer_agents_set,
620
628
  (PEER_AGENTS_V1.uri, "list"): self.peer_agents_list,
621
629
  (SKILL_CONFIG_V1.uri, "list"): self.skill_list,
@@ -721,6 +729,32 @@ class _SdkServices:
721
729
  }
722
730
  }
723
731
 
732
+ async def cdp_add(self, request: CdpAddRequest) -> dict[str, Any]:
733
+ """Adding a name again replaces its endpoint."""
734
+ self.cdp_endpoints[request.name] = {
735
+ "http_url": request.http_url,
736
+ "ws_url": request.ws_url,
737
+ "headers": dict(request.headers) if request.headers else None,
738
+ "metadata": dict(request.metadata or {}),
739
+ }
740
+ return {"status": "added", "name": request.name}
741
+
742
+ async def cdp_list(self) -> dict[str, Any]:
743
+ """Names and metadata only: an endpoint's URLs can carry its credential, as its headers can."""
744
+ return {
745
+ "cdp_endpoints": {
746
+ name: {
747
+ "has_headers": bool(endpoint["headers"]),
748
+ "metadata": endpoint["metadata"],
749
+ }
750
+ for name, endpoint in self.cdp_endpoints.items()
751
+ }
752
+ }
753
+
754
+ async def cdp_remove(self, request: CdpRemoveRequest) -> dict[str, Any]:
755
+ removed = self.cdp_endpoints.pop(request.name, None) is not None
756
+ return {"status": "removed" if removed else "absent", "name": request.name}
757
+
724
758
  def task_mcp_servers(self) -> dict[str, dict[str, Any]]:
725
759
  """The MCP servers a task gets: those registered, and the SDK's peer-agents server when it serves one."""
726
760
  peer_tools = self.peers.registration() if self.peers is not None else None
@@ -1238,16 +1272,22 @@ class _StandardExecutor:
1238
1272
  ),
1239
1273
  config=task_config,
1240
1274
  mcp_servers=self._services.task_mcp_servers(),
1275
+ cdp_endpoints=self._services.cdp_endpoints,
1241
1276
  skills=tuple(self._services.skills),
1242
1277
  metadata=metadata,
1243
1278
  session_ref=self._services.session_ref_for_context(task.context_id),
1244
1279
  workspace=self._workspace,
1245
1280
  trajectory=trajectory,
1246
1281
  )
1247
- if self._streaming:
1248
- result = await self._run_streaming(request, updater, event_queue)
1249
- else:
1250
- result = await self._handler(request)
1282
+ try:
1283
+ if self._streaming:
1284
+ result = await self._run_streaming(request, updater, event_queue)
1285
+ else:
1286
+ result = await self._handler(request)
1287
+ except _TaskFailed as failed:
1288
+ result = TaskResult.failure(
1289
+ failed.error.code, failed.error.message, error_type=failed.error.error_type
1290
+ )
1251
1291
  if not isinstance(result, TaskResult):
1252
1292
  raise TypeError("agent.run must return TaskResult")
1253
1293
  if result.session_ref is not None:
@@ -217,6 +217,7 @@ class TaskRequest(Generic[ConfigT]):
217
217
  parts: tuple[TaskPart, ...]
218
218
  config: ConfigT
219
219
  mcp_servers: Mapping[str, Any] = field(default_factory=dict)
220
+ cdp_endpoints: Mapping[str, Any] = field(default_factory=dict)
220
221
  skills: tuple[Mapping[str, Any], ...] = ()
221
222
  metadata: Mapping[str, Any] = field(default_factory=dict)
222
223
  session_ref: str | None = None
@@ -233,11 +234,31 @@ class TaskRequest(Generic[ConfigT]):
233
234
  # JSON-native types declared by the config model and request contract.
234
235
  object.__setattr__(self, "config", self.config.model_copy(deep=True))
235
236
  object.__setattr__(self, "mcp_servers", _copy_json_value(self.mcp_servers))
237
+ object.__setattr__(self, "cdp_endpoints", _copy_json_value(self.cdp_endpoints))
236
238
  object.__setattr__(
237
239
  self, "skills", tuple(_copy_json_value(skill) for skill in self.skills)
238
240
  )
239
241
  object.__setattr__(self, "metadata", _copy_json_value(self.metadata))
240
242
 
243
+ def cdp_endpoint(self, name: str | None = None) -> Mapping[str, Any]:
244
+ """The CDP endpoint named ``name``, or the turn's only one. Anything else fails the turn, so an agent
245
+ never falls back to a browser of its own."""
246
+ if name is not None:
247
+ if name in self.cdp_endpoints:
248
+ return self.cdp_endpoints[name]
249
+ raise _TaskFailed(
250
+ "cdp.no_endpoint", f"the task gave no browser named {name!r}", error_type="infra_error"
251
+ )
252
+ if len(self.cdp_endpoints) == 1:
253
+ return next(iter(self.cdp_endpoints.values()))
254
+ if not self.cdp_endpoints:
255
+ raise _TaskFailed("cdp.no_endpoint", "the task gave no browser", error_type="infra_error")
256
+ raise _TaskFailed(
257
+ "cdp.ambiguous",
258
+ f"the task gave several browsers; name one of {', '.join(sorted(self.cdp_endpoints))}",
259
+ error_type="infra_error",
260
+ )
261
+
241
262
 
242
263
  class TaskOutcome(str, Enum):
243
264
  SUCCEEDED = "succeeded"
@@ -255,6 +276,20 @@ class TaskError:
255
276
  raise ValueError("error_type must be 'agent_error' or 'infra_error'")
256
277
 
257
278
 
279
+ class _TaskFailed(Exception):
280
+ """Ends the turn as ``TaskResult.failure(code, message, error_type=error_type)``; ``cdp_endpoint`` raises it."""
281
+
282
+ def __init__(
283
+ self,
284
+ code: str,
285
+ message: str,
286
+ *,
287
+ error_type: Literal["agent_error", "infra_error"] = "agent_error",
288
+ ) -> None:
289
+ super().__init__(message)
290
+ self.error = TaskError(code=code, message=message, error_type=error_type)
291
+
292
+
258
293
  @dataclass(frozen=True, slots=True, kw_only=True)
259
294
  class NativeTrajectory:
260
295
  format: str
@@ -0,0 +1,26 @@
1
+ """Run a program with fds it inherits moved to the numbers it expects: ``python -I -S _with_fds.py <from>=<to> ... --
2
+ <argv>``. ``session.launch`` runs an app launched with ``fds`` through it, by path, so its server runs no Python between
3
+ its fork and its exec and this loads nothing but the standard library."""
4
+ from __future__ import annotations
5
+
6
+ import fcntl
7
+ import os
8
+ import sys
9
+
10
+
11
+ def main(argv: list[str]) -> None:
12
+ split = argv.index("--")
13
+ moves = [tuple(int(number) for number in pair.split("=")) for pair in argv[:split]]
14
+ # Each source copied above every number named first, so no copy, source or target is another.
15
+ above = max((number for move in moves for number in move), default=2) + 1
16
+ copies = [(fcntl.fcntl(source, fcntl.F_DUPFD_CLOEXEC, above), target) for source, target in moves]
17
+ for source, _ in moves:
18
+ os.close(source)
19
+ for copy, target in copies:
20
+ os.dup2(copy, target)
21
+ os.close(copy)
22
+ os.execvp(argv[split + 1], argv[split + 1:])
23
+
24
+
25
+ if __name__ == "__main__":
26
+ main(sys.argv[1:])
@@ -169,6 +169,8 @@ class Launch(BaseModel):
169
169
  #: Matches the app's window by its WM_CLASS ``class`` or ``instance`` and its ``title``, as given.
170
170
  window: Optional[dict[str, str]] = None
171
171
  ready: Optional[Ready] = None
172
+ #: Variables set in the app's environment, over its server's.
173
+ env: Optional[dict[str, str]] = None
172
174
 
173
175
  @field_validator("window")
174
176
  @classmethod
@@ -399,17 +401,36 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
399
401
  self.settings = cls.settings_on(on).model_validate(values)
400
402
  self.build_settings = _built_settings(cls, on)
401
403
  declared = cls._installs.get(on)
402
- self.launch = declared.launch if declared is not None else None
404
+ launch = declared.launch if declared is not None else None
405
+ self.launch = self._launch_for(launch) if launch is not None else None
403
406
  self.paths = list(declared.paths) if declared is not None else []
404
- ready = self.launch.ready if self.launch is not None else None
407
+ ready = launch.ready if launch is not None else None
405
408
  self._computer_app = AppRuntime(
406
409
  self.launch, {fn.__name__: getattr(self, fn.__name__) for fn in _methods(cls)
407
410
  if hasattr(fn, _READY_CHECK_ATTR) and _runs_on(getattr(fn, _READY_CHECK_ATTR), on)},
408
- ready.timeout_s if ready is not None else DEFAULT_TIMEOUT_S)
411
+ ready.timeout_s if ready is not None else DEFAULT_TIMEOUT_S, desktop=self._on_desktop(),
412
+ launch_fds=self._launch_fds, relaunch_on_start=self._relaunch_on_start())
409
413
  self._computer_app_data = AppDataPlane(
410
414
  self._computer_app, {op: getattr(self, name) for op, name in cls._data_hooks.items()}, self.paths,
411
415
  cls.data_timeout_s)
412
416
 
417
+ def _launch_for(self, launch: Launch) -> Launch:
418
+ """The launch its server uses, from the one declared for its OS, whose ``ready`` timeout it keeps: as
419
+ declared."""
420
+ return launch
421
+
422
+ def _on_desktop(self) -> bool:
423
+ """Whether it runs in a computer's desktop session, which lists it in its menu."""
424
+ return True
425
+
426
+ async def _launch_fds(self) -> dict[int, int]:
427
+ """The fds each launch hands it, by the number each gets in it, made new for that launch: none."""
428
+ return {}
429
+
430
+ def _relaunch_on_start(self) -> bool:
431
+ """Whether only the server that launched it can talk to it, so a server that restarts launches it again."""
432
+ return False
433
+
413
434
  @classmethod
414
435
  def settings_on(cls, os_type: Optional[str]) -> type[BaseModel]:
415
436
  """Its ``Settings`` on ``os_type``, with the fields its OS adds."""
@@ -48,30 +48,51 @@ class AppRuntime:
48
48
  """An app's server's state: its ``launch``, its ``@ready_check`` methods by name, bound to the app, how long it may
49
49
  take to be ready, why its last launch failed, and each sync check still running in its thread."""
50
50
 
51
- def __init__(self, launch: Optional[Launch], ready_checks: dict[str, Callable[[], Any]], timeout_s: float) -> None:
51
+ def __init__(self, launch: Optional[Launch], ready_checks: dict[str, Callable[[], Any]], timeout_s: float, *,
52
+ desktop: bool = True, launch_fds: Optional[Callable[[], Awaitable[dict[int, int]]]] = None,
53
+ relaunch_on_start: bool = False) -> None:
52
54
  self.launch = launch
53
55
  self.ready_checks = ready_checks
54
56
  self.timeout_s = timeout_s
57
+ self.desktop = desktop
58
+ self.launch_fds = launch_fds
59
+ self.relaunch_on_start = relaunch_on_start
55
60
  self.started = time.time()
56
61
  self.launch_failed: Optional[str] = None
57
62
  self.checking: dict[str, asyncio.Future] = {}
58
63
 
59
64
  async def start(self) -> None:
60
- """Once the desktop session is up, add the app's menu launcher and launch a boot app, unless the computer has
61
- launched it since it booted, at boot or on demand: a server that restarts leaves the app be."""
65
+ """Once its X display is up, when it has one, add the app's menu launcher on a desktop and launch a boot app,
66
+ unless the computer has launched it since it booted, at boot or on demand: a server that restarts leaves the
67
+ app be, or with ``relaunch_on_start``, for an app only the server that launched it can talk to, stops it and
68
+ launches it again, whether it was launched at boot or on demand."""
62
69
  if self.launch is None:
63
70
  return
64
- await session.wait_until_up()
65
- session.add_launcher(os.environ["ENVIRONMENT_NAME"], Path.cwd())
66
- if self.launch.at == "boot" and _launched_at() is None:
71
+ if os.environ.get("DISPLAY"):
72
+ await session.wait_until_up()
73
+ if self.desktop:
74
+ session.add_launcher(os.environ["ENVIRONMENT_NAME"], Path.cwd())
75
+ launched = _launched_at() is not None
76
+ relaunch = self.relaunch_on_start and launched
77
+ if relaunch or (self.launch.at == "boot" and not launched):
67
78
  try:
79
+ if relaunch:
80
+ await self.stop_app()
68
81
  await self.launch_app()
69
82
  except Exception as e:
70
83
  self.launch_failed = f"{type(e).__name__}: {e}"
71
84
 
72
85
  async def launch_app(self) -> int:
73
- """Start the app from its folder, marked as launched; its pid."""
74
- pid = await session.launch(self.launch.command, Path.cwd(), {"ENVIRONMENT_NAME": os.environ["ENVIRONMENT_NAME"]})
86
+ """Start the app from its folder, marked as launched, with the fds the app gives it, whose copies here are
87
+ closed once it has them; its pid."""
88
+ fds = await self.launch_fds() if self.launch_fds is not None else {}
89
+ try:
90
+ pid = await session.launch(self.launch.command, Path.cwd(),
91
+ {**(self.launch.env or {}), "ENVIRONMENT_NAME": os.environ["ENVIRONMENT_NAME"]},
92
+ **({"fds": fds} if fds else {}))
93
+ finally:
94
+ for fd in fds.values():
95
+ os.close(fd)
75
96
  self.launch_failed = None
76
97
  PID.parent.mkdir(parents=True, exist_ok=True)
77
98
  PID.write_text(str(pid))