agentenv-framework-protocol 0.1.399__tar.gz → 0.1.400__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 (110) hide show
  1. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/.gitignore +1 -0
  2. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/PKG-INFO +35 -12
  3. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/README.md +31 -11
  4. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/pyproject.toml +7 -1
  5. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/_declare.py +6 -0
  6. agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/_load.py +22 -0
  7. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/app.py +66 -4
  8. agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/app_runtime.py +155 -0
  9. agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/apps.py +63 -0
  10. agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/launch.py +25 -0
  11. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/runtime.py +3 -0
  12. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/serve.py +2 -12
  13. agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/serve_app.py +64 -0
  14. agentenv_framework_protocol-0.1.400/src/agentenv_protocol/computer/session.py +74 -0
  15. agentenv_framework_protocol-0.1.400/tests/test_computer_app_server.py +341 -0
  16. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_computer_declare.py +21 -2
  17. agentenv_framework_protocol-0.1.400/tests/test_computer_serve.py +180 -0
  18. agentenv_framework_protocol-0.1.399/tests/test_computer_serve.py +0 -75
  19. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/LICENSE +0 -0
  20. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/NOTICE +0 -0
  21. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/THIRD_PARTY_NOTICES.md +0 -0
  22. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/examples/__init__.py +0 -0
  23. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/examples/advanced_agent.py +0 -0
  24. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/examples/basic_agent.py +0 -0
  25. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/examples/custom_extensions_agent.py +0 -0
  26. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/examples/multimodal_agent.py +0 -0
  27. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/examples/reference_model.py +0 -0
  28. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/examples/streaming_agent.py +0 -0
  29. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/__init__.py +0 -0
  30. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  31. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  32. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  33. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  34. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  35. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  36. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  37. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  38. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  39. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  40. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  41. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/agent_env_environment.py +0 -0
  42. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/client.py +0 -0
  43. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/__init__.py +0 -0
  44. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/card.py +0 -0
  45. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/checks.py +0 -0
  46. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  47. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  48. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  49. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  50. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  51. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  52. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  53. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/dataplane.py +0 -0
  54. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/driver.py +0 -0
  55. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/errors.py +0 -0
  56. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/extensions.py +0 -0
  57. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/frames.py +0 -0
  58. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/keys.py +0 -0
  59. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/linux.py +0 -0
  60. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/manifest.py +0 -0
  61. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/operations.py +0 -0
  62. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/platform.py +0 -0
  63. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/queue.py +0 -0
  64. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/recording.py +0 -0
  65. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/results.py +0 -0
  66. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/settings.py +0 -0
  67. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/skill.py +0 -0
  68. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/spaces.py +0 -0
  69. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/computer/tools.py +0 -0
  70. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/errors.py +0 -0
  71. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/manifest.py +0 -0
  72. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/preflight.py +0 -0
  73. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/tool_access.py +0 -0
  74. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/tool_client.py +0 -0
  75. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/tool_context.py +0 -0
  76. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/transfers.py +0 -0
  77. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/src/agentenv_protocol/types.py +0 -0
  78. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/computer_fake.py +0 -0
  79. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/probe_fake.py +0 -0
  80. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_agent.py +0 -0
  81. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_agent_examples.py +0 -0
  82. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_object_transfers.py +0 -0
  83. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_peer_agents.py +0 -0
  84. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_proxy.py +0 -0
  85. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_staging.py +0 -0
  86. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_a2a_trajectory_log.py +0 -0
  87. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_agent_env_environment.py +0 -0
  88. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_client.py +0 -0
  89. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_computer_conformance.py +0 -0
  90. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_computer_conformance_checks.py +0 -0
  91. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_computer_conformance_recording.py +0 -0
  92. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_computer_dataplane.py +0 -0
  93. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_computer_driver.py +0 -0
  94. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_computer_extensions.py +0 -0
  95. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_computer_frames.py +0 -0
  96. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_computer_queue.py +0 -0
  97. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_computer_recording.py +0 -0
  98. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_computer_spaces.py +0 -0
  99. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_data_objects.py +0 -0
  100. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_errors.py +0 -0
  101. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_extension_methods.py +0 -0
  102. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_intake.py +0 -0
  103. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_manifest.py +0 -0
  104. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_preflight.py +0 -0
  105. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_tool_access.py +0 -0
  106. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_tool_annotations.py +0 -0
  107. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_tool_client.py +0 -0
  108. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_tool_context.py +0 -0
  109. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_tool_context_fastmcp.py +0 -0
  110. {agentenv_framework_protocol-0.1.399 → agentenv_framework_protocol-0.1.400}/tests/test_tool_context_injection.py +0 -0
@@ -170,3 +170,4 @@ cython_debug/
170
170
 
171
171
  # Written by the release workflow into the wheels it publishes
172
172
  src/agent_env/env/published_images.json
173
+ src/agent_env/explorer/static/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentenv-framework-protocol
3
- Version: 0.1.399
3
+ Version: 0.1.400
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
@@ -34,6 +34,9 @@ Requires-Dist: mcp<2,>=1.25; extra == 'agent'
34
34
  Requires-Dist: python-multipart>=0.0.13; extra == 'agent'
35
35
  Requires-Dist: regex>=2023.12.25; extra == 'agent'
36
36
  Requires-Dist: uvicorn>=0.30; extra == 'agent'
37
+ Provides-Extra: app
38
+ Requires-Dist: mcp<2,>=1.25; extra == 'app'
39
+ Requires-Dist: uvicorn>=0.30; extra == 'app'
37
40
  Provides-Extra: computer
38
41
  Requires-Dist: av>=17; extra == 'computer'
39
42
  Requires-Dist: mcp<2,>=1.25; extra == 'computer'
@@ -481,8 +484,8 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
481
484
  the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
482
485
  class's `notes`, markdown for what its card can't say.
483
486
  - **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
484
- capture, plus the driver's own `ready_checks()`; tools refuse `device_unavailable` until they
485
- pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
487
+ capture, the driver's own `ready_checks()`, and one per app on the computer, asked of the app's
488
+ own server (see Computer apps); tools refuse `device_unavailable` until they pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
486
489
  descriptions), `computer-keyboard/v1`, `computer-exec/v1` (`run {argv, cwd?, env?, timeout_s?,
487
490
  request_id}`: output capped per stream, a repeated `request_id` answered with the first result) and,
488
491
  with `recording`, `computer-recording/v1` (below).
@@ -594,9 +597,10 @@ and gives it back.
594
597
 
595
598
  A computer app is software an agent uses on a computer, beside the driver: a desktop app, a service
596
599
  or both. Write one as a `ComputerAppEnvironment` subclass from `agentenv_protocol.computer`, an
597
- `AgentEnvEnvironment` whose class also says what it is to a computer. It needs only the plain
598
- package: `agentenv_protocol.computer` imports what a driver needs (mcp, uvicorn, Pillow, PyAV) only
599
- when a driver's names are used.
600
+ `AgentEnvEnvironment` whose class also says what it is to a computer. Writing one and reading what
601
+ it declares need only the plain package: `agentenv_protocol.computer` imports what a driver needs
602
+ (mcp, uvicorn, Pillow, PyAV) only when a driver's names are used. On a computer it runs as its own
603
+ server, which needs the `app` extra (below).
600
604
 
601
605
  ```python
602
606
  from pydantic import BaseModel, SecretStr
@@ -617,18 +621,37 @@ class MailApp(ComputerAppEnvironment):
617
621
  `permissions` it needs of the computer. Its env.toml names the rest: the app itself
618
622
  (`environment_name`) and the `computer` and `driver` it deploys alone on.
619
623
  - **`linux = Linux(...)`** is how a Linux computer installs it: `files`, a folder of the app's
620
- copied under its prefix; `python`, a requirements file there installed into a venv beside them;
621
- `services`, programs the computer's init runs from the prefix as the session user.
622
- - **`launch = Launch(...)`** says how it starts in the desktop session, with it (`at="boot"`) or when
623
- the agent opens it (`"demand"`), and `ready=Ready(...)` when it's ready; `@ready_check` marks a
624
- method for readiness that needs logic, and `@install_step(scope=...)` one a computer runs as it
625
- installs the app.
624
+ copied under its prefix, which holds the class's file; `python`, a requirements file there,
625
+ installed into the venv every app gets beside them; `services`, programs the computer's init runs
626
+ from the prefix as the session user.
627
+ - **`launch = Launch(...)`** says how it starts in the desktop session: with it (`at="boot"`) or when
628
+ the agent opens it (`"demand"`). `ready=Ready(...)` says when it's ready, within `timeout_s` of its
629
+ launch: a window matching `launch.window` by its class, instance or title, a port answering on
630
+ loopback, or a command exiting 0. `@ready_check` marks a method for readiness that needs logic (a
631
+ sync one runs in a thread), and `@install_step(scope=...)` one a computer runs as it installs the
632
+ app.
626
633
  - **Its skill** is a file of its folder (`skill = "skill.md"`) or an `@agent_skill` method, not both;
627
634
  its data hooks are the usual `@add_data`, `@get_data` and `@reset_data`.
628
635
  - **`Settings`** is typed; a computer hands each app the values a deploy gives it, as one JSON value
629
636
  in `APP_SETTINGS_ENV` (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads
630
637
  its own, by `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.
631
638
 
639
+ On a computer each app is its own server: `python -m agentenv_protocol.computer.serve_app
640
+ <file>:<Class>`, a program the computer's init runs from the app's folder in the app's venv, which
641
+ has the `app` extra (`agentenv-framework-protocol[app]`: mcp and uvicorn). It serves the class's
642
+ card and tools on the Unix socket `.agentenv/server.sock` in that folder, with `computer-app/v1`:
643
+ the app's `launch` as its params, and the methods `launch` (POST), which starts the app in the
644
+ desktop session, and `ready` (GET: an `AppReadiness` of `ready`, `problems`, `launched`, `waited_s`
645
+ and `timeout_s`), which runs the app's readiness checks then, at once, each within 10 s. A server
646
+ that can't start, or whose start in the session fails (adding the launcher, say), writes why to
647
+ `.agentenv/server.failure` in the folder and exits, and the driver reports that in its place. Once
648
+ the session is up the server adds the app's launcher to the session user's menu, which runs `python
649
+ -m agentenv_protocol.computer.launch <folder>` to ask the server, so the server starts every copy of
650
+ the app, and launches a `boot` app, once per boot. The driver finds each app by the socket in its
651
+ folder: the computer is ready once every app's server answers and each `boot` app, and each app with
652
+ no launch, says it's ready; a `demand` app counts once something launches it. What the server does
653
+ in the session (`agentenv_protocol.computer.session`) is Linux's.
654
+
632
655
  `MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
633
656
  env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
634
657
  JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
@@ -428,8 +428,8 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
428
428
  the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
429
429
  class's `notes`, markdown for what its card can't say.
430
430
  - **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
431
- capture, plus the driver's own `ready_checks()`; tools refuse `device_unavailable` until they
432
- pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
431
+ capture, the driver's own `ready_checks()`, and one per app on the computer, asked of the app's
432
+ own server (see Computer apps); tools refuse `device_unavailable` until they pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
433
433
  descriptions), `computer-keyboard/v1`, `computer-exec/v1` (`run {argv, cwd?, env?, timeout_s?,
434
434
  request_id}`: output capped per stream, a repeated `request_id` answered with the first result) and,
435
435
  with `recording`, `computer-recording/v1` (below).
@@ -541,9 +541,10 @@ and gives it back.
541
541
 
542
542
  A computer app is software an agent uses on a computer, beside the driver: a desktop app, a service
543
543
  or both. Write one as a `ComputerAppEnvironment` subclass from `agentenv_protocol.computer`, an
544
- `AgentEnvEnvironment` whose class also says what it is to a computer. It needs only the plain
545
- package: `agentenv_protocol.computer` imports what a driver needs (mcp, uvicorn, Pillow, PyAV) only
546
- when a driver's names are used.
544
+ `AgentEnvEnvironment` whose class also says what it is to a computer. Writing one and reading what
545
+ it declares need only the plain package: `agentenv_protocol.computer` imports what a driver needs
546
+ (mcp, uvicorn, Pillow, PyAV) only when a driver's names are used. On a computer it runs as its own
547
+ server, which needs the `app` extra (below).
547
548
 
548
549
  ```python
549
550
  from pydantic import BaseModel, SecretStr
@@ -564,18 +565,37 @@ class MailApp(ComputerAppEnvironment):
564
565
  `permissions` it needs of the computer. Its env.toml names the rest: the app itself
565
566
  (`environment_name`) and the `computer` and `driver` it deploys alone on.
566
567
  - **`linux = Linux(...)`** is how a Linux computer installs it: `files`, a folder of the app's
567
- copied under its prefix; `python`, a requirements file there installed into a venv beside them;
568
- `services`, programs the computer's init runs from the prefix as the session user.
569
- - **`launch = Launch(...)`** says how it starts in the desktop session, with it (`at="boot"`) or when
570
- the agent opens it (`"demand"`), and `ready=Ready(...)` when it's ready; `@ready_check` marks a
571
- method for readiness that needs logic, and `@install_step(scope=...)` one a computer runs as it
572
- installs the app.
568
+ copied under its prefix, which holds the class's file; `python`, a requirements file there,
569
+ installed into the venv every app gets beside them; `services`, programs the computer's init runs
570
+ from the prefix as the session user.
571
+ - **`launch = Launch(...)`** says how it starts in the desktop session: with it (`at="boot"`) or when
572
+ the agent opens it (`"demand"`). `ready=Ready(...)` says when it's ready, within `timeout_s` of its
573
+ launch: a window matching `launch.window` by its class, instance or title, a port answering on
574
+ loopback, or a command exiting 0. `@ready_check` marks a method for readiness that needs logic (a
575
+ sync one runs in a thread), and `@install_step(scope=...)` one a computer runs as it installs the
576
+ app.
573
577
  - **Its skill** is a file of its folder (`skill = "skill.md"`) or an `@agent_skill` method, not both;
574
578
  its data hooks are the usual `@add_data`, `@get_data` and `@reset_data`.
575
579
  - **`Settings`** is typed; a computer hands each app the values a deploy gives it, as one JSON value
576
580
  in `APP_SETTINGS_ENV` (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads
577
581
  its own, by `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.
578
582
 
583
+ On a computer each app is its own server: `python -m agentenv_protocol.computer.serve_app
584
+ <file>:<Class>`, a program the computer's init runs from the app's folder in the app's venv, which
585
+ has the `app` extra (`agentenv-framework-protocol[app]`: mcp and uvicorn). It serves the class's
586
+ card and tools on the Unix socket `.agentenv/server.sock` in that folder, with `computer-app/v1`:
587
+ the app's `launch` as its params, and the methods `launch` (POST), which starts the app in the
588
+ desktop session, and `ready` (GET: an `AppReadiness` of `ready`, `problems`, `launched`, `waited_s`
589
+ and `timeout_s`), which runs the app's readiness checks then, at once, each within 10 s. A server
590
+ that can't start, or whose start in the session fails (adding the launcher, say), writes why to
591
+ `.agentenv/server.failure` in the folder and exits, and the driver reports that in its place. Once
592
+ the session is up the server adds the app's launcher to the session user's menu, which runs `python
593
+ -m agentenv_protocol.computer.launch <folder>` to ask the server, so the server starts every copy of
594
+ the app, and launches a `boot` app, once per boot. The driver finds each app by the socket in its
595
+ folder: the computer is ready once every app's server answers and each `boot` app, and each app with
596
+ no launch, says it's ready; a `demand` app counts once something launches it. What the server does
597
+ in the session (`agentenv_protocol.computer.session`) is Linux's.
598
+
579
599
  `MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
580
600
  env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
581
601
  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.399"
3
+ version = "0.1.400"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -35,6 +35,12 @@ agent = [
35
35
  "regex>=2023.12.25",
36
36
  "uvicorn>=0.30",
37
37
  ]
38
+ # A computer app's own server: ComputerAppEnvironment serves its card and computer-app/v1 over mcp's FastMCP, on a Unix
39
+ # socket through uvicorn.
40
+ app = [
41
+ "mcp>=1.25,<2",
42
+ "uvicorn>=0.30",
43
+ ]
38
44
  # A computer driver: ComputerDriverEnvironment serves its tools over mcp's FastMCP, crops and encodes frames, and
39
45
  # records the display as H.264 with PyAV (computer-recording/v1).
40
46
  computer = [
@@ -14,6 +14,8 @@ import sys
14
14
  import types
15
15
  from pathlib import Path
16
16
 
17
+ from pydantic import ValidationError
18
+
17
19
  from .app import AppDeclaration, ComputerAppEnvironment
18
20
  from .card import COMPUTER
19
21
  from .driver import ComputerDriverEnvironment, DriverDeclaration, DriverPlatform
@@ -95,6 +97,10 @@ def main(argv: list[str]) -> int:
95
97
  try:
96
98
  with contextlib.redirect_stdout(sys.stderr):
97
99
  declared = declare(Path(folder), file, name)
100
+ except ValidationError as e:
101
+ print("ValueError: " + "; ".join(error["msg"].removeprefix("Value error, ") for error in e.errors()),
102
+ file=sys.stderr)
103
+ return 1
98
104
  except Exception as e:
99
105
  print(f"{type(e).__name__}: {e}", file=sys.stderr)
100
106
  return 1
@@ -0,0 +1,22 @@
1
+ """Import a class from a file of a driver's or an app's folder, as its computer's program starts it."""
2
+ from __future__ import annotations
3
+
4
+ import importlib.util
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def load_class(file: Path, name: str, base: type) -> type:
10
+ """The class ``name`` in ``file``, imported as a module of its own with the file's folder importable; exits saying
11
+ why when it isn't a subclass of ``base``."""
12
+ if not file.is_file():
13
+ raise SystemExit(f"{file} isn't a file")
14
+ sys.path.insert(0, str(file.resolve().parent))
15
+ spec = importlib.util.spec_from_file_location(file.stem, file)
16
+ module = importlib.util.module_from_spec(spec)
17
+ sys.modules[file.stem] = module
18
+ spec.loader.exec_module(module)
19
+ cls = getattr(module, name, None)
20
+ if not (isinstance(cls, type) and issubclass(cls, base)):
21
+ raise SystemExit(f"{file} has no {base.__name__} subclass named {name}")
22
+ return cls
@@ -2,24 +2,37 @@
2
2
  computer: the platforms it supports and the permissions it needs (``@computer_app``), how each OS installs it
3
3
  (``linux = Linux(...)``), how it launches (``launch``), its install steps and ready checks, its skill, and its typed
4
4
  ``Settings``. A ``computer_app`` env's put reads that declaration from the class; its data hooks are the base class's.
5
- Its name is its env's ``environment_name``."""
5
+ Its name is its env's ``environment_name``. On the computer the class runs as the app's own server (``serve_app``),
6
+ which launches the app, adds its launcher and says whether it's ready (``computer-app/v1``)."""
6
7
  from __future__ import annotations
7
8
 
8
9
  import inspect
9
10
  import json
10
11
  import os
12
+ from pathlib import Path
11
13
  from typing import Any, Callable, ClassVar, Literal, Optional
12
14
 
13
- from pydantic import BaseModel, ConfigDict, Field
15
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
14
16
 
15
- from ..agent_env_environment import _EXT_ATTR, _OP_ATTR, AgentEnvEnvironment
17
+ from ..agent_env_environment import _EXT_ATTR, _OP_ATTR, AgentEnvEnvironment, extension
16
18
  from ..types import METHOD_ADD, METHOD_GET, METHOD_RESET, SKILL_EXTENSION_URI
19
+ from .app_runtime import AppRuntime
20
+ from .errors import InvalidArgument
17
21
 
18
22
  #: The environment variable a computer delivers its apps' settings in: JSON of each app's values by its name.
19
23
  APP_SETTINGS_ENV = "AGENTENV_APP_SETTINGS"
20
24
  _INSTALL_STEP_ATTR = "_aee_install_step"
21
25
  _READY_CHECK_ATTR = "_aee_ready_check"
22
26
  _DATA_METHODS = {"reset_data": METHOD_RESET, "add_data": METHOD_ADD, "get_data": METHOD_GET}
27
+ #: What a launch's window can be matched by, as the session lists each window.
28
+ WINDOW_KEYS = frozenset({"class", "instance", "title"})
29
+ #: The framework extension an app's own server serves: its launch as params, and its ``launch`` and ``ready`` methods.
30
+ COMPUTER_APP = "urn:agentenv:computer-app/v1"
31
+ #: Where an app's own server listens, in the app's folder, and where it says why it last failed to start.
32
+ SOCKET = Path(".agentenv/server.sock")
33
+ FAILURE = Path(".agentenv/server.failure")
34
+ #: How long an app may take to be ready when its launch doesn't say.
35
+ DEFAULT_TIMEOUT_S = 60.0
23
36
 
24
37
 
25
38
  class Service(BaseModel):
@@ -51,7 +64,13 @@ class Ready(BaseModel):
51
64
  window: bool = False
52
65
  port: Optional[int] = None
53
66
  command: Optional[list[str]] = None
54
- timeout_s: float = 60
67
+ timeout_s: float = DEFAULT_TIMEOUT_S
68
+
69
+ @model_validator(mode="after")
70
+ def _waits_on_something(self) -> Ready:
71
+ if not self.window and self.port is None and not self.command:
72
+ raise ValueError("Ready waits on nothing: set window, port or command")
73
+ return self
55
74
 
56
75
 
57
76
  class Launch(BaseModel):
@@ -61,9 +80,23 @@ class Launch(BaseModel):
61
80
 
62
81
  at: Literal["boot", "demand"] = "boot"
63
82
  command: list[str]
83
+ #: Matches the app's window by its WM_CLASS ``class`` or ``instance`` and its ``title``, as given.
64
84
  window: Optional[dict[str, str]] = None
65
85
  ready: Optional[Ready] = None
66
86
 
87
+ @field_validator("window")
88
+ @classmethod
89
+ def _matchable(cls, window: Optional[dict[str, str]]) -> Optional[dict[str, str]]:
90
+ if window is not None and (not window or set(window) - WINDOW_KEYS):
91
+ raise ValueError(f"window matches by {', '.join(sorted(WINDOW_KEYS))}, not {window!r}")
92
+ return window
93
+
94
+ @model_validator(mode="after")
95
+ def _window_to_wait_for(self) -> Launch:
96
+ if self.ready is not None and self.ready.window and self.window is None:
97
+ raise ValueError("ready waits on a window: name it with Launch(window={'class': ...})")
98
+ return self
99
+
67
100
 
68
101
  class AppSkill(BaseModel):
69
102
  """Where an app's skill for agents comes from: a ``file`` of its folder, or the ``@agent_skill`` ``method`` that
@@ -134,6 +167,7 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
134
167
 
135
168
  supports: ClassVar[list] = []
136
169
  permissions: ClassVar[list[str]] = []
170
+ _computer_app: Optional[AppRuntime] = None
137
171
  linux: ClassVar[Optional[Linux]] = None
138
172
  launch: ClassVar[Optional[Launch]] = None
139
173
  #: A file of the app's folder holding its skill for agents, when no ``@agent_skill`` method writes it.
@@ -145,6 +179,33 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
145
179
  def __init__(self) -> None:
146
180
  values = json.loads(os.environ.get(APP_SETTINGS_ENV) or "{}").get(os.environ.get("ENVIRONMENT_NAME"), {})
147
181
  self.settings = self.Settings.model_validate(values)
182
+ ready = self.launch.ready if self.launch is not None else None
183
+ self._computer_app = AppRuntime(
184
+ self.launch, {fn.__name__: getattr(self, fn.__name__) for fn in _methods(type(self))
185
+ if hasattr(fn, _READY_CHECK_ATTR)},
186
+ ready.timeout_s if ready is not None else DEFAULT_TIMEOUT_S)
187
+
188
+ @extension(COMPUTER_APP, op="launch", description=(
189
+ "The app's own server: launch launches the app from its folder, as its launcher and a boot do; ready says "
190
+ "whether it's ready, by its launch's ready and its @ready_check methods."))
191
+ async def _computer_app_launch(self) -> dict:
192
+ if self.launch is None:
193
+ raise InvalidArgument(f"{type(self).__name__} declares no launch")
194
+ return {"pid": await self._computer_app_runtime().launch_app()}
195
+
196
+ @extension(COMPUTER_APP, op="ready", method="GET")
197
+ async def _computer_app_ready(self) -> dict:
198
+ return (await self._computer_app_runtime().readiness()).model_dump()
199
+
200
+ def _computer_app_runtime(self) -> AppRuntime:
201
+ if self._computer_app is None:
202
+ raise TypeError(f"{type(self).__qualname__}.__init__ must call super().__init__()")
203
+ return self._computer_app
204
+
205
+ def extension_params(self, uri: str) -> dict:
206
+ if uri == COMPUTER_APP:
207
+ return {"launch": self.launch.model_dump(mode="json", exclude_none=True) if self.launch else None}
208
+ return super().extension_params(uri)
148
209
 
149
210
  @classmethod
150
211
  def declaration(cls) -> AppDeclaration:
@@ -176,3 +237,4 @@ def _methods(cls: type) -> list[Callable]:
176
237
  names = dict.fromkeys(name for klass in reversed(cls.__mro__) for name, member in vars(klass).items()
177
238
  if inspect.isfunction(member))
178
239
  return [method for name in names if inspect.isfunction(method := getattr(cls, name))]
240
+
@@ -0,0 +1,155 @@
1
+ """A computer app's own server at work: it launches the app into the desktop session, marking each launch for the rest
2
+ of the boot, and says whether the app is ready, its checks run at once, each within ``CHECK_TIMEOUT_S``
3
+ (``AppReadiness``). ``ComputerAppEnvironment`` keeps one as ``self._computer_app``."""
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import inspect
8
+ import os
9
+ import shlex
10
+ import time
11
+ from collections.abc import Awaitable, Callable
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Any, Optional
14
+
15
+ from pydantic import BaseModel, ConfigDict
16
+
17
+ from . import session
18
+
19
+ if TYPE_CHECKING:
20
+ from .app import Launch
21
+
22
+ #: Where an app's server marks that it launched the app since the computer booted, at boot or on demand.
23
+ LAUNCHED = Path("/run/agentenv/apps")
24
+ #: How long one readiness check may take; an app's checks run at once.
25
+ CHECK_TIMEOUT_S = 10.0
26
+
27
+
28
+ class AppReadiness(BaseModel):
29
+ """What an app's server says of its readiness, ``computer-app/v1``'s ``ready``: the ``problems`` that keep it from
30
+ being ``ready``, whether it was ``launched`` since the computer booted, and ``waited_s`` since then, or for an app
31
+ with no launch since its server started, against its ``timeout_s``."""
32
+
33
+ model_config = ConfigDict(extra="forbid")
34
+
35
+ ready: bool
36
+ problems: list[str]
37
+ launched: bool
38
+ waited_s: float
39
+ timeout_s: float
40
+
41
+
42
+ class AppRuntime:
43
+ """An app's server's state: its ``launch``, its ``@ready_check`` methods by name, bound to the app, how long it may
44
+ take to be ready, why its last launch failed, and each sync check still running in its thread."""
45
+
46
+ def __init__(self, launch: Optional[Launch], ready_checks: dict[str, Callable[[], Any]], timeout_s: float) -> None:
47
+ self.launch = launch
48
+ self.ready_checks = ready_checks
49
+ self.timeout_s = timeout_s
50
+ self.started = time.time()
51
+ self.launch_failed: Optional[str] = None
52
+ self.checking: dict[str, asyncio.Future] = {}
53
+
54
+ async def start(self) -> None:
55
+ """Once the desktop session is up, add the app's menu launcher and launch a boot app, unless the computer has
56
+ launched it since it booted, at boot or on demand: a server that restarts leaves the app be."""
57
+ if self.launch is None:
58
+ return
59
+ await session.wait_until_up()
60
+ session.add_launcher(os.environ["ENVIRONMENT_NAME"], Path.cwd())
61
+ if self.launch.at == "boot" and _launched_at() is None:
62
+ try:
63
+ await self.launch_app()
64
+ except Exception as e:
65
+ self.launch_failed = f"{type(e).__name__}: {e}"
66
+
67
+ async def launch_app(self) -> int:
68
+ """Start the app from its folder, marked as launched; its pid."""
69
+ pid = await session.launch(self.launch.command, Path.cwd(), {"ENVIRONMENT_NAME": os.environ["ENVIRONMENT_NAME"]})
70
+ self.launch_failed = None
71
+ _mark().parent.mkdir(parents=True, exist_ok=True)
72
+ _mark().touch()
73
+ return pid
74
+
75
+ async def readiness(self) -> AppReadiness:
76
+ ready = self.launch.ready if self.launch is not None else None
77
+ checks: list[tuple[str, Awaitable[Optional[str]]]] = []
78
+ if ready is not None and ready.window:
79
+ checks.append(("window", _window(self.launch.window)))
80
+ if ready is not None and ready.port is not None:
81
+ checks.append(("port", _port(ready.port)))
82
+ if ready is not None and ready.command:
83
+ checks.append(("command", _command(ready.command)))
84
+ checks += [(f"@ready_check {name}", self._ready_check(name, fn)) for name, fn in self.ready_checks.items()]
85
+ found = await asyncio.gather(*(_bounded(name, check) for name, check in checks))
86
+ problems = [problem for problem in found if problem is not None]
87
+ launched = _launched_at()
88
+ if self.launch is not None and launched is None:
89
+ problems.insert(0, f"its launch failed: {self.launch_failed}" if self.launch_failed else "not launched yet")
90
+ return AppReadiness(ready=not problems, problems=problems, launched=launched is not None,
91
+ waited_s=round(time.time() - (launched or self.started), 1), timeout_s=self.timeout_s)
92
+
93
+ async def _ready_check(self, name: str, fn: Callable[[], Any]) -> Optional[str]:
94
+ """A sync check runs in a thread, so it can't hold up the server, and once at a time: a thread can't be
95
+ stopped, so a poll while one runs past its bound waits on that same call."""
96
+ if inspect.iscoroutinefunction(fn):
97
+ passed = await fn()
98
+ else:
99
+ running = self.checking.get(name)
100
+ if running is None or running.done():
101
+ running = self.checking[name] = asyncio.ensure_future(asyncio.to_thread(fn))
102
+ running.add_done_callback(lambda done: done.cancelled() or done.exception())
103
+ passed = await asyncio.shield(running)
104
+ return None if passed else f"@ready_check {name} returned {passed!r}"
105
+
106
+
107
+ def _mark() -> Path:
108
+ return LAUNCHED / Path.cwd().name
109
+
110
+
111
+ def _launched_at() -> Optional[float]:
112
+ """When the app was last launched since the computer booted, by its mark."""
113
+ try:
114
+ return _mark().stat().st_mtime
115
+ except FileNotFoundError:
116
+ return None
117
+
118
+
119
+ async def _bounded(name: str, check: Awaitable[Optional[str]]) -> Optional[str]:
120
+ """What ``check`` finds wrong, within ``CHECK_TIMEOUT_S``; a check that runs past it or raises says so."""
121
+ try:
122
+ return await asyncio.wait_for(check, CHECK_TIMEOUT_S)
123
+ except TimeoutError:
124
+ return f"{name} ran past {CHECK_TIMEOUT_S:g} s"
125
+ except Exception as e:
126
+ return f"{name}: {type(e).__name__}: {e}"
127
+
128
+
129
+ async def _window(match: dict[str, str]) -> Optional[str]:
130
+ if any(all(window.get(key) == value for key, value in match.items()) for window in await session.windows()):
131
+ return None
132
+ return f"no window with {', '.join(f'{key} {value!r}' for key, value in match.items())}"
133
+
134
+
135
+ async def _port(port: int) -> Optional[str]:
136
+ try:
137
+ _, writer = await asyncio.open_connection("127.0.0.1", port)
138
+ except OSError:
139
+ return f"nothing answers on port {port}"
140
+ writer.close()
141
+ return None
142
+
143
+
144
+ async def _command(argv: list[str]) -> Optional[str]:
145
+ process = await asyncio.create_subprocess_exec(*argv, cwd=Path.cwd(), stdout=asyncio.subprocess.DEVNULL,
146
+ stderr=asyncio.subprocess.PIPE)
147
+ try:
148
+ _, err = await process.communicate()
149
+ except asyncio.CancelledError:
150
+ process.kill()
151
+ raise
152
+ if process.returncode == 0:
153
+ return None
154
+ reason = (err.decode(errors="replace").strip().splitlines() or [""])[-1]
155
+ return f"{shlex.join(argv)} exited {process.returncode}" + (f": {reason}" if reason else "")
@@ -0,0 +1,63 @@
1
+ """The apps composed onto a computer, as its driver finds them, and whether each is ready. Each app's own server
2
+ answers on its folder's socket (``serve_app``): its card says how the app launches, and its ``computer-app/v1``
3
+ ``ready`` whether it's ready. Every app's server must answer before the computer is ready; an app that starts with the
4
+ computer must also be ready, and a ``demand`` app is checked once something launches it."""
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ import httpx
10
+
11
+ from .. import client
12
+ from ..types import WELL_KNOWN_PATH
13
+ from .app import COMPUTER_APP, FAILURE, SOCKET
14
+ from .app_runtime import CHECK_TIMEOUT_S, AppReadiness
15
+ from .operations import ReadyCheck
16
+
17
+ #: Where a composed computer installs its apps, a folder each.
18
+ APPS = Path("/opt/agentenv/apps")
19
+ #: How long an app's server may take to answer, its checks run at once, before it counts as not answering.
20
+ ANSWER_TIMEOUT_S = CHECK_TIMEOUT_S + 5
21
+
22
+
23
+ def expected() -> list[Path]:
24
+ """The folders of the apps composed onto this computer."""
25
+ return sorted(path for path in APPS.iterdir() if path.is_dir()) if APPS.is_dir() else []
26
+
27
+
28
+ async def check(folder: Path) -> ReadyCheck:
29
+ """Whether the app in ``folder`` is ready, as its own server says, or why its server last failed to start; named by
30
+ its card once the server answers."""
31
+ name = folder.name
32
+ try:
33
+ async with _client(folder / SOCKET) as http:
34
+ card = (await http.get(WELL_KNOWN_PATH)).raise_for_status().json()
35
+ name = card["name"]
36
+ launch = client.extension_params(card, COMPUTER_APP).get("launch")
37
+ answer = AppReadiness.model_validate((await http.get(endpoint(card, "ready"))).raise_for_status().json())
38
+ except (httpx.HTTPError, OSError):
39
+ return ReadyCheck(f"app {name}", False, _unanswered(folder))
40
+ if launch is not None and launch["at"] == "demand" and not answer.launched:
41
+ return ReadyCheck(f"app {name}", True, "its server answers; it launches on demand")
42
+ if answer.ready:
43
+ return ReadyCheck(f"app {name}", True, "ready")
44
+ when = (f"not ready after {answer.timeout_s:.0f} s" if answer.waited_s > answer.timeout_s
45
+ else f"not ready yet, {answer.waited_s:.0f} s in")
46
+ return ReadyCheck(f"app {name}", False, f"{when}: {'; '.join(answer.problems)}")
47
+
48
+
49
+ def endpoint(card: dict, method: str) -> str:
50
+ """The path of a ``computer-app/v1`` method, as the app's card advertises it."""
51
+ return client.find_extension_method(card, COMPUTER_APP, method)["endpoint"]
52
+
53
+
54
+ def _unanswered(folder: Path) -> str:
55
+ try:
56
+ return f"its server failed to start: {(folder / FAILURE).read_text()}"
57
+ except FileNotFoundError:
58
+ return "its server isn't answering yet"
59
+
60
+
61
+ def _client(socket: Path) -> httpx.AsyncClient:
62
+ return httpx.AsyncClient(transport=httpx.AsyncHTTPTransport(uds=str(socket)), base_url="http://app",
63
+ timeout=ANSWER_TIMEOUT_S)
@@ -0,0 +1,25 @@
1
+ """Launch a computer app through its own server, as its menu launcher does:
2
+ ``python -m agentenv_protocol.computer.launch <app folder>`` asks the server on the folder's socket to launch it, so
3
+ the server starts every copy of the app."""
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import httpx
10
+
11
+ from ..types import WELL_KNOWN_PATH
12
+ from .app import SOCKET
13
+ from .apps import endpoint
14
+
15
+
16
+ def main(argv: list[str]) -> None:
17
+ if len(argv) != 1:
18
+ raise SystemExit("usage: python -m agentenv_protocol.computer.launch <app folder>")
19
+ with httpx.Client(transport=httpx.HTTPTransport(uds=str(Path(argv[0]) / SOCKET)), base_url="http://app") as http:
20
+ card = http.get(WELL_KNOWN_PATH).raise_for_status().json()
21
+ http.post(endpoint(card, "launch"), json={}).raise_for_status()
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main(sys.argv[1:])
@@ -13,6 +13,7 @@ from mcp.types import CallToolResult
13
13
 
14
14
  from ..errors import AgentEnvError
15
15
  from ..tool_context import ToolContext
16
+ from . import apps
16
17
  from .errors import DeliveryFailed, DeviceUnavailable, InputBusy, Internal
17
18
  from .frames import Frame, FrameStore, to_pil
18
19
  from .operations import GROUPS, INPUT, Display, ReadyCheck, resolve
@@ -40,6 +41,7 @@ class ComputerRuntime:
40
41
  self._commands_running: dict[str, asyncio.Task] = {}
41
42
  self._commands_finished: OrderedDict = OrderedDict()
42
43
  self.recordings = Recordings(self)
44
+ self.apps = apps.expected()
43
45
 
44
46
  def has(self, capability: str) -> bool:
45
47
  if capability in GROUPS:
@@ -175,6 +177,7 @@ class ComputerRuntime:
175
177
  checks += list(await self.driver.ready_checks())
176
178
  except Exception as e:
177
179
  checks.append(ReadyCheck("driver", False, f"{type(e).__name__}: {e}"))
180
+ checks += await asyncio.gather(*(apps.check(folder) for folder in self.apps))
178
181
  if all(c.ok for c in checks):
179
182
  self.ready = True
180
183
  return checks