agentenv-framework-protocol 0.1.433__tar.gz → 0.1.435__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 (118) hide show
  1. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/PKG-INFO +39 -12
  2. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/README.md +38 -11
  3. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/pyproject.toml +1 -1
  4. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/app.py +7 -2
  5. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/app_data.py +4 -2
  6. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/app_runtime.py +67 -4
  7. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/dataplane.py +19 -10
  8. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/session.py +28 -1
  9. agentenv_framework_protocol-0.1.435/src/agentenv_protocol/website_browser.py +592 -0
  10. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_dataplane.py +43 -0
  11. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_website_browser.py +246 -18
  12. agentenv_framework_protocol-0.1.433/src/agentenv_protocol/website_browser.py +0 -296
  13. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/.gitignore +0 -0
  14. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/LICENSE +0 -0
  15. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/NOTICE +0 -0
  16. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/THIRD_PARTY_NOTICES.md +0 -0
  17. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/__init__.py +0 -0
  18. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/advanced_agent.py +0 -0
  19. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/basic_agent.py +0 -0
  20. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/custom_extensions_agent.py +0 -0
  21. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/multimodal_agent.py +0 -0
  22. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/reference_model.py +0 -0
  23. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/streaming_agent.py +0 -0
  24. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/__init__.py +0 -0
  25. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  26. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  27. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  28. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  29. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  30. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  31. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  32. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  33. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  34. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  35. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  36. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/agent_env_environment.py +0 -0
  37. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/client.py +0 -0
  38. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/__init__.py +0 -0
  39. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/_declare.py +0 -0
  40. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/_linux.py +0 -0
  41. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/_load.py +0 -0
  42. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/_with_fds.py +0 -0
  43. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/apps.py +0 -0
  44. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/card.py +0 -0
  45. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/checks.py +0 -0
  46. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  47. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  48. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  49. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  50. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  51. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  52. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  53. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/driver.py +0 -0
  54. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/errors.py +0 -0
  55. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/extensions.py +0 -0
  56. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/frames.py +0 -0
  57. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/keys.py +0 -0
  58. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/launch.py +0 -0
  59. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/manifest.py +0 -0
  60. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/operations.py +0 -0
  61. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/platform.py +0 -0
  62. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/queue.py +0 -0
  63. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/recording.py +0 -0
  64. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/results.py +0 -0
  65. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  66. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/runtime.py +0 -0
  67. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/serve.py +0 -0
  68. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/serve_app.py +0 -0
  69. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/settings.py +0 -0
  70. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/skill.py +0 -0
  71. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/spaces.py +0 -0
  72. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/tools.py +0 -0
  73. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/errors.py +0 -0
  74. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/manifest.py +0 -0
  75. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/preflight.py +0 -0
  76. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/tool_access.py +0 -0
  77. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/tool_client.py +0 -0
  78. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/tool_context.py +0 -0
  79. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/transfers.py +0 -0
  80. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/types.py +0 -0
  81. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/computer_fake.py +0 -0
  82. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/probe_fake.py +0 -0
  83. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_agent.py +0 -0
  84. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_agent_examples.py +0 -0
  85. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_cdp_config.py +0 -0
  86. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_object_transfers.py +0 -0
  87. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_peer_agents.py +0 -0
  88. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_proxy.py +0 -0
  89. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_staging.py +0 -0
  90. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_trajectory_log.py +0 -0
  91. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_agent_env_environment.py +0 -0
  92. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_client.py +0 -0
  93. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_app_data.py +0 -0
  94. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_app_server.py +0 -0
  95. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_conformance.py +0 -0
  96. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_conformance_checks.py +0 -0
  97. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_conformance_recording.py +0 -0
  98. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_declare.py +0 -0
  99. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_driver.py +0 -0
  100. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_extensions.py +0 -0
  101. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_frames.py +0 -0
  102. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_queue.py +0 -0
  103. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_recording.py +0 -0
  104. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_serve.py +0 -0
  105. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_spaces.py +0 -0
  106. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_control_token.py +0 -0
  107. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_data_objects.py +0 -0
  108. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_errors.py +0 -0
  109. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_extension_methods.py +0 -0
  110. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_intake.py +0 -0
  111. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_manifest.py +0 -0
  112. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_preflight.py +0 -0
  113. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_tool_access.py +0 -0
  114. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_tool_annotations.py +0 -0
  115. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_tool_client.py +0 -0
  116. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_tool_context.py +0 -0
  117. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_tool_context_fastmcp.py +0 -0
  118. {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/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.433
3
+ Version: 0.1.435
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
@@ -717,8 +717,9 @@ class MailApp(ComputerAppEnvironment):
717
717
 
718
718
  - **`@computer_app`** names the platforms the app supports, as a driver names them, and the
719
719
  `permissions` it needs of the computer. Its env.toml names the rest: the app itself
720
- (`environment_name`), the `computer` and `driver` it deploys alone on, and `build_settings`, its
721
- build settings' values (`build_settings = { locale = "fr_FR" }`).
720
+ (`environment_name`), the `computer` and `driver` it deploys alone on, `build_settings`, its
721
+ build settings' values (`build_settings = { locale = "fr_FR" }`), and `[settings]`, defaults for its
722
+ `Settings` each deploy starts from.
722
723
  - **`@linux(...)`** is how a Linux computer installs it, `Linux`'s fields: `files`, a folder of the app's
723
724
  copied under its prefix, which holds the class's file; `packages`, the OS's, as apt takes them
724
725
  (`Packages(apt=["name", "name=version", "name/suite"])`); `python`, a requirements file there,
@@ -754,9 +755,13 @@ class MailApp(ComputerAppEnvironment):
754
755
  - **Its data** is under the `paths` its OS's decorator names, relative to the session user's home,
755
756
  none inside another (`@linux(..., paths=["Mail", ".config/mail/accounts.json"])`), or what its own
756
757
  `@add_data`, `@get_data` and `@reset_data` hooks serve instead (below).
757
- - **`Settings`** is typed; a computer hands each app the values a deploy gives it, as one JSON value
758
- in `APP_SETTINGS_ENV` (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads
759
- its own, by `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.
758
+ - **`Settings`** is typed. Its values come from the app env's `[settings]`, then the computer env's
759
+ `settings` beside the app in its `apps` (`apps = [{ env = "mail", settings = { signature = "-- Ada" } }]`),
760
+ then the deploy's `env_params` `app_settings`, each over the one before by top-level field (a table
761
+ or list is replaced whole). Each is checked at put; a required one may be left to the deploy, which
762
+ checks them merged. A computer hands each app its values as one JSON value in `APP_SETTINGS_ENV`
763
+ (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads its own, by
764
+ `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.
760
765
  - **`BuildSettings`** is typed too: values the app's env fixes in its computer's image, which only
761
766
  another image changes, so a deploy can't give one. The composer writes them to
762
767
  `.agentenv/build_settings.json` in the app's folder, and the base class reads them, over the
@@ -808,9 +813,15 @@ are the profile's template. The base class adds:
808
813
  - **Its settings**, `BrowserSettings`, which a subclass's `Settings` extends: `start_urls`, opened in
809
814
  tabs at launch, the first active (`["about:blank"]` by default; none may start with `-`), headless
810
815
  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
816
+ 1280 × 800, alone only; `locale`; `time_zone`, set as `TZ` in its launch's `env`; `geolocation` and
817
+ `notifications`, `deny` (the default), which answers a page's prompt for it, or `ask`; and `dialogs`, who
818
+ answers a page's JavaScript dialogs: `agent` (the default), or agentenv, accepting (`accept`, a `prompt()`
819
+ with its default text) or dismissing (`dismiss`) each. Either way agentenv leaves a page that asks before
820
+ it's left (`beforeunload`), so none of its answers blocks a navigation.
821
+ - **The flags agentenv owns**, after the subclass's command and before the start tabs: its profile, its
822
+ HTTP cache under `~/.cache` (`--disk-cache-dir`), `--remote-debugging-pipe`, `--no-first-run`,
823
+ `--no-default-browser-check`, `--lang` and, when `/dev/shm` holds under 1 GiB (Docker gives 64 MiB),
824
+ `--disable-dev-shm-usage`, so it keeps its shared memory in files. With
814
825
  `AGENTENV_SITE_PROXY` set, it goes out through that proxy, with WebRTC's non-proxied UDP off.
815
826
  Alone it bypasses nothing, loopback included; in a computer it bypasses loopback (`127.0.0.1`,
816
827
  `[::1]`, `localhost`), so the apps beside it stay reachable while `*.localhost` names still go
@@ -818,6 +829,9 @@ are the profile's template. The base class adds:
818
829
  except on the open web (`AGENTENV_BROWSER_OPEN_WEB=1`), and refuses to start with no proxy.
819
830
  `declaration()` refuses a subclass whose command carries any of these, or `--remote-allow-origins`, or
820
831
  any other way to open a debugging port.
832
+ - **Before each launch** it empties its cache, so no launch inherits another's, and writes the deploy's
833
+ prompt answers into its profile's `Default/Preferences` (`profile.default_content_setting_values`,
834
+ `2` to deny, left out to ask), which it reads as it starts.
821
835
  - **Alone, no desktop**: its launch matches and waits on no window, its server adds no launcher and,
822
836
  without `DISPLAY`, waits for no display, and its ready checks say when it's ready.
823
837
  - **`write_policies`**, an install step that writes agentenv's unattended-use policies (`POLICIES`)
@@ -828,9 +842,22 @@ are the profile's template. The base class adds:
828
842
  the result or raises `DevToolsError`, and `ConnectionError` with no browser connected. No port
829
843
  listens and no `DevToolsActivePort` is written, so nothing else in the computer can reach it over
830
844
  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`.
845
+ launches it again, and a launch while it runs, from its menu launcher say, opens another window over
846
+ the pipe rather than a second copy, which would hand its window to the first and exit. The ready check
847
+ `devtools_answers` asks `Browser.getVersion` over the pipe.
848
+ - **Its control connection**, `self.control`, a `BrowserControl` over the pipe. At each launch it attaches
849
+ to every page with flattened sessions and keeps a record, `since` its server started: open tabs in
850
+ opened order, the one whose page has focus `active`; `closed_tabs`, each with `closed_at` and
851
+ `closed_by` (`agentenv`, over this connection; `unknown`, anyone else; `exit` or `stop`, when the browser
852
+ ended on its own or its server stopped it); `downloads`, into `~/Downloads`, each with its state
853
+ and, completed, its size and sha256; and `dialogs`, each with its type, message, answer and who gave it.
854
+ `hidden_page()` gives agentenv a page of its own for reading state, in no window and never in the record.
855
+ - **`browser/v1`** on its card: `chromium` (its `version`), `clock: "real"` and, alone, `window`; and
856
+ `record` (GET), framework-only, which returns that record with `launches`, the app server's log of each
857
+ launch (with its cause: `boot`, `server_restart`, `launch`, `data/add` or `data/reset`), each stop it made
858
+ and each exit it didn't, with its exit `code` or the `signal` that ended it, since the computer booted.
859
+ Nothing relaunches a browser that exits on its own: in a computer the agent can, and alone it's an infra
860
+ failure, its ready check false.
834
861
  - **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
835
862
  `~/Downloads`, folders it makes.
836
863
 
@@ -661,8 +661,9 @@ class MailApp(ComputerAppEnvironment):
661
661
 
662
662
  - **`@computer_app`** names the platforms the app supports, as a driver names them, and the
663
663
  `permissions` it needs of the computer. Its env.toml names the rest: the app itself
664
- (`environment_name`), the `computer` and `driver` it deploys alone on, and `build_settings`, its
665
- build settings' values (`build_settings = { locale = "fr_FR" }`).
664
+ (`environment_name`), the `computer` and `driver` it deploys alone on, `build_settings`, its
665
+ build settings' values (`build_settings = { locale = "fr_FR" }`), and `[settings]`, defaults for its
666
+ `Settings` each deploy starts from.
666
667
  - **`@linux(...)`** is how a Linux computer installs it, `Linux`'s fields: `files`, a folder of the app's
667
668
  copied under its prefix, which holds the class's file; `packages`, the OS's, as apt takes them
668
669
  (`Packages(apt=["name", "name=version", "name/suite"])`); `python`, a requirements file there,
@@ -698,9 +699,13 @@ class MailApp(ComputerAppEnvironment):
698
699
  - **Its data** is under the `paths` its OS's decorator names, relative to the session user's home,
699
700
  none inside another (`@linux(..., paths=["Mail", ".config/mail/accounts.json"])`), or what its own
700
701
  `@add_data`, `@get_data` and `@reset_data` hooks serve instead (below).
701
- - **`Settings`** is typed; a computer hands each app the values a deploy gives it, as one JSON value
702
- in `APP_SETTINGS_ENV` (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads
703
- its own, by `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.
702
+ - **`Settings`** is typed. Its values come from the app env's `[settings]`, then the computer env's
703
+ `settings` beside the app in its `apps` (`apps = [{ env = "mail", settings = { signature = "-- Ada" } }]`),
704
+ then the deploy's `env_params` `app_settings`, each over the one before by top-level field (a table
705
+ or list is replaced whole). Each is checked at put; a required one may be left to the deploy, which
706
+ checks them merged. A computer hands each app its values as one JSON value in `APP_SETTINGS_ENV`
707
+ (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads its own, by
708
+ `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.
704
709
  - **`BuildSettings`** is typed too: values the app's env fixes in its computer's image, which only
705
710
  another image changes, so a deploy can't give one. The composer writes them to
706
711
  `.agentenv/build_settings.json` in the app's folder, and the base class reads them, over the
@@ -752,9 +757,15 @@ are the profile's template. The base class adds:
752
757
  - **Its settings**, `BrowserSettings`, which a subclass's `Settings` extends: `start_urls`, opened in
753
758
  tabs at launch, the first active (`["about:blank"]` by default; none may start with `-`), headless
754
759
  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
760
+ 1280 × 800, alone only; `locale`; `time_zone`, set as `TZ` in its launch's `env`; `geolocation` and
761
+ `notifications`, `deny` (the default), which answers a page's prompt for it, or `ask`; and `dialogs`, who
762
+ answers a page's JavaScript dialogs: `agent` (the default), or agentenv, accepting (`accept`, a `prompt()`
763
+ with its default text) or dismissing (`dismiss`) each. Either way agentenv leaves a page that asks before
764
+ it's left (`beforeunload`), so none of its answers blocks a navigation.
765
+ - **The flags agentenv owns**, after the subclass's command and before the start tabs: its profile, its
766
+ HTTP cache under `~/.cache` (`--disk-cache-dir`), `--remote-debugging-pipe`, `--no-first-run`,
767
+ `--no-default-browser-check`, `--lang` and, when `/dev/shm` holds under 1 GiB (Docker gives 64 MiB),
768
+ `--disable-dev-shm-usage`, so it keeps its shared memory in files. With
758
769
  `AGENTENV_SITE_PROXY` set, it goes out through that proxy, with WebRTC's non-proxied UDP off.
759
770
  Alone it bypasses nothing, loopback included; in a computer it bypasses loopback (`127.0.0.1`,
760
771
  `[::1]`, `localhost`), so the apps beside it stay reachable while `*.localhost` names still go
@@ -762,6 +773,9 @@ are the profile's template. The base class adds:
762
773
  except on the open web (`AGENTENV_BROWSER_OPEN_WEB=1`), and refuses to start with no proxy.
763
774
  `declaration()` refuses a subclass whose command carries any of these, or `--remote-allow-origins`, or
764
775
  any other way to open a debugging port.
776
+ - **Before each launch** it empties its cache, so no launch inherits another's, and writes the deploy's
777
+ prompt answers into its profile's `Default/Preferences` (`profile.default_content_setting_values`,
778
+ `2` to deny, left out to ask), which it reads as it starts.
765
779
  - **Alone, no desktop**: its launch matches and waits on no window, its server adds no launcher and,
766
780
  without `DISPLAY`, waits for no display, and its ready checks say when it's ready.
767
781
  - **`write_policies`**, an install step that writes agentenv's unattended-use policies (`POLICIES`)
@@ -772,9 +786,22 @@ are the profile's template. The base class adds:
772
786
  the result or raises `DevToolsError`, and `ConnectionError` with no browser connected. No port
773
787
  listens and no `DevToolsActivePort` is written, so nothing else in the computer can reach it over
774
788
  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`.
789
+ launches it again, and a launch while it runs, from its menu launcher say, opens another window over
790
+ the pipe rather than a second copy, which would hand its window to the first and exit. The ready check
791
+ `devtools_answers` asks `Browser.getVersion` over the pipe.
792
+ - **Its control connection**, `self.control`, a `BrowserControl` over the pipe. At each launch it attaches
793
+ to every page with flattened sessions and keeps a record, `since` its server started: open tabs in
794
+ opened order, the one whose page has focus `active`; `closed_tabs`, each with `closed_at` and
795
+ `closed_by` (`agentenv`, over this connection; `unknown`, anyone else; `exit` or `stop`, when the browser
796
+ ended on its own or its server stopped it); `downloads`, into `~/Downloads`, each with its state
797
+ and, completed, its size and sha256; and `dialogs`, each with its type, message, answer and who gave it.
798
+ `hidden_page()` gives agentenv a page of its own for reading state, in no window and never in the record.
799
+ - **`browser/v1`** on its card: `chromium` (its `version`), `clock: "real"` and, alone, `window`; and
800
+ `record` (GET), framework-only, which returns that record with `launches`, the app server's log of each
801
+ launch (with its cause: `boot`, `server_restart`, `launch`, `data/add` or `data/reset`), each stop it made
802
+ and each exit it didn't, with its exit `code` or the `signal` that ended it, since the computer booted.
803
+ Nothing relaunches a browser that exits on its own: in a computer the agent can, and alone it's an infra
804
+ failure, its ready check false.
778
805
  - **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
779
806
  `~/Downloads`, folders it makes.
780
807
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.433"
3
+ version = "0.1.435"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -16,7 +16,7 @@ import json
16
16
  import os
17
17
  from dataclasses import dataclass
18
18
  from pathlib import Path, PurePosixPath
19
- from typing import Annotated, Any, Callable, ClassVar, Literal, Optional, Union
19
+ from typing import Annotated, Any, Awaitable, Callable, ClassVar, Literal, Optional, Union
20
20
 
21
21
  from pydantic import (AfterValidator, BaseModel, ConfigDict, Field, TypeAdapter, ValidationError, create_model,
22
22
  field_validator, model_validator)
@@ -409,7 +409,8 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
409
409
  self.launch, {fn.__name__: getattr(self, fn.__name__) for fn in _methods(cls)
410
410
  if hasattr(fn, _READY_CHECK_ATTR) and _runs_on(getattr(fn, _READY_CHECK_ATTR), on)},
411
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())
412
+ launch_fds=self._launch_fds, relaunch_on_start=self._relaunch_on_start(),
413
+ launch_running=self._launch_running())
413
414
  self._computer_app_data = AppDataPlane(
414
415
  self._computer_app, {op: getattr(self, name) for op, name in cls._data_hooks.items()}, self.paths,
415
416
  cls.data_timeout_s)
@@ -431,6 +432,10 @@ class ComputerAppEnvironment(AgentEnvEnvironment):
431
432
  """Whether only the server that launched it can talk to it, so a server that restarts launches it again."""
432
433
  return False
433
434
 
435
+ def _launch_running(self) -> Optional[Callable[[], Awaitable[None]]]:
436
+ """What a launch does while it runs instead of starting a second copy: nothing, so another copy starts."""
437
+ return None
438
+
434
439
  @classmethod
435
440
  def settings_on(cls, os_type: Optional[str]) -> type[BaseModel]:
436
441
  """Its ``Settings`` on ``os_type``, with the fields its OS adds."""
@@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Any, Optional
15
15
 
16
16
  from ..agent_env_environment import OP_ADD_DATA, OP_GET_DATA, OP_RESET_DATA
17
17
  from ..transfers import WriteNamespaceGrant
18
- from ..types import FilePart, uploaded_object_path
18
+ from ..types import METHOD_ADD, METHOD_RESET, FilePart, uploaded_object_path
19
19
  from .dataplane import file_parts, part_bytes
20
20
 
21
21
  if TYPE_CHECKING:
@@ -23,6 +23,8 @@ if TYPE_CHECKING:
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
+ #: The cause a launch after each data call logs.
27
+ _CAUSES = {OP_ADD_DATA: METHOD_ADD, OP_RESET_DATA: METHOD_RESET}
26
28
  #: Where an app's server keeps its paths as installed, in the app's folder.
27
29
  INITIAL = Path(".agentenv/initial")
28
30
 
@@ -80,7 +82,7 @@ class AppDataPlane:
80
82
  raise
81
83
  finally:
82
84
  if running:
83
- await self.runtime.launch_app()
85
+ await self.runtime.launch_app(_CAUSES[op])
84
86
  except Exception as e:
85
87
  if not answer.done():
86
88
  answer.set_exception(e)
@@ -5,10 +5,13 @@ from __future__ import annotations
5
5
 
6
6
  import asyncio
7
7
  import inspect
8
+ import json
8
9
  import os
9
10
  import shlex
11
+ import signal
10
12
  import time
11
13
  from collections.abc import Awaitable, Callable
14
+ from datetime import datetime, timezone
12
15
  from pathlib import Path
13
16
  from typing import TYPE_CHECKING, Any, Optional
14
17
 
@@ -26,6 +29,11 @@ CHECK_TIMEOUT_S = 10.0
26
29
  #: Where an app's server keeps the pid of the app it launched last, in the app's folder, so a server that restarts can
27
30
  #: still stop it.
28
31
  PID = Path(".agentenv/app.pid")
32
+ #: Where an app's server logs each launch, stop and exit of the app, beside its pid; a boot starts the app's folder as
33
+ #: its image has it, so the log is the boot's.
34
+ LAUNCHES = Path(".agentenv/launches")
35
+ #: How long an app that ended on its own may take to be reaped, for its exit code.
36
+ EXIT_WAIT_S = 5.0
29
37
  #: How long a stopped app may take to exit before it's killed.
30
38
  STOP_TIMEOUT_S = 10.0
31
39
 
@@ -50,13 +58,16 @@ class AppRuntime:
50
58
 
51
59
  def __init__(self, launch: Optional[Launch], ready_checks: dict[str, Callable[[], Any]], timeout_s: float, *,
52
60
  desktop: bool = True, launch_fds: Optional[Callable[[], Awaitable[dict[int, int]]]] = None,
53
- relaunch_on_start: bool = False) -> None:
61
+ relaunch_on_start: bool = False,
62
+ launch_running: Optional[Callable[[], Awaitable[None]]] = None) -> None:
54
63
  self.launch = launch
55
64
  self.ready_checks = ready_checks
56
65
  self.timeout_s = timeout_s
57
66
  self.desktop = desktop
58
67
  self.launch_fds = launch_fds
59
68
  self.relaunch_on_start = relaunch_on_start
69
+ self.launch_running = launch_running
70
+ self.exits: set[asyncio.Future] = set()
60
71
  self.started = time.time()
61
72
  self.launch_failed: Optional[str] = None
62
73
  self.checking: dict[str, asyncio.Future] = {}
@@ -78,13 +89,17 @@ class AppRuntime:
78
89
  try:
79
90
  if relaunch:
80
91
  await self.stop_app()
81
- await self.launch_app()
92
+ await self.launch_app("server_restart" if relaunch else "boot")
82
93
  except Exception as e:
83
94
  self.launch_failed = f"{type(e).__name__}: {e}"
84
95
 
85
- async def launch_app(self) -> int:
96
+ async def launch_app(self, cause: str = "launch") -> int:
86
97
  """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."""
98
+ closed once it has them, and logged with ``cause``; its pid. With ``launch_running``, an app this server
99
+ launched that still runs does that instead of starting a second copy, as a browser opens another window."""
100
+ if self.launch_running is not None and (running := _running()) is not None:
101
+ await self.launch_running()
102
+ return running
88
103
  fds = await self.launch_fds() if self.launch_fds is not None else {}
89
104
  try:
90
105
  pid = await session.launch(self.launch.command, Path.cwd(),
@@ -98,6 +113,7 @@ class AppRuntime:
98
113
  PID.write_text(str(pid))
99
114
  _mark().parent.mkdir(parents=True, exist_ok=True)
100
115
  _mark().touch()
116
+ self.log("launch", pid, cause=cause)
101
117
  return pid
102
118
 
103
119
  async def stop_app(self) -> bool:
@@ -109,9 +125,39 @@ class AppRuntime:
109
125
  PID.unlink()
110
126
  if not session.alive(pid):
111
127
  return False
128
+ self.log("stop", pid)
112
129
  await session.stop(pid, STOP_TIMEOUT_S)
113
130
  return True
114
131
 
132
+ def exited(self) -> bool:
133
+ """Log that the app this server launched last ended on its own, with its exit code or the signal that ended
134
+ it, unless the server stopped it; whether it did."""
135
+ if (pid := _recorded_pid()) is None:
136
+ return False
137
+ noting = asyncio.ensure_future(self._log_exit(pid))
138
+ self.exits.add(noting)
139
+ noting.add_done_callback(self.exits.discard)
140
+ return True
141
+
142
+ async def _log_exit(self, pid: int) -> None:
143
+ code = await session.returncode(pid, EXIT_WAIT_S)
144
+ self.log("exit", pid, **({} if code is None else
145
+ {"signal": signal.Signals(-code).name} if code < 0 else {"code": code}))
146
+
147
+ def log(self, event: str, pid: int, **fields: Any) -> None:
148
+ """Append ``event`` for the app's ``pid`` to its launch log, which outlives its server for the boot."""
149
+ entry = {"at": datetime.now(timezone.utc).isoformat(), "event": event, "pid": pid, **fields}
150
+ _log().parent.mkdir(parents=True, exist_ok=True)
151
+ with _log().open("a", encoding="utf-8") as log:
152
+ log.write(json.dumps(entry) + "\n")
153
+
154
+ def launches(self) -> list[dict]:
155
+ """Every launch, stop and exit of the app since the computer booted, in order."""
156
+ try:
157
+ return [json.loads(line) for line in _log().read_text(encoding="utf-8").splitlines() if line]
158
+ except FileNotFoundError:
159
+ return []
160
+
115
161
  async def readiness(self) -> AppReadiness:
116
162
  ready = self.launch.ready if self.launch is not None else None
117
163
  checks: list[tuple[str, Awaitable[Optional[str]]]] = []
@@ -148,6 +194,17 @@ def _mark() -> Path:
148
194
  return LAUNCHED / Path.cwd().name
149
195
 
150
196
 
197
+ def _log() -> Path:
198
+ return LAUNCHES
199
+
200
+
201
+ def _recorded_pid() -> Optional[int]:
202
+ try:
203
+ return int(PID.read_text())
204
+ except (FileNotFoundError, ValueError):
205
+ return None
206
+
207
+
151
208
  def _launched_at() -> Optional[float]:
152
209
  """When the app was last launched since the computer booted, by its mark."""
153
210
  try:
@@ -156,6 +213,12 @@ def _launched_at() -> Optional[float]:
156
213
  return None
157
214
 
158
215
 
216
+ def _running() -> Optional[int]:
217
+ """The pid of the app this server launched last, while it runs."""
218
+ pid = _recorded_pid()
219
+ return pid if pid is not None and session.alive(pid) else None
220
+
221
+
159
222
  async def _bounded(name: str, check: Awaitable[Optional[str]]) -> Optional[str]:
160
223
  """What ``check`` finds wrong, within ``CHECK_TIMEOUT_S``; a check that runs past it or raises says so."""
161
224
  try:
@@ -15,7 +15,7 @@ import httpx
15
15
  from pydantic import TypeAdapter, ValidationError
16
16
 
17
17
  from ..agent_env_environment import add_data, get_data
18
- from ..transfers import NamespaceUploader, ReadObject, RelativePath, WriteNamespaceGrant, download
18
+ from ..transfers import NamespaceUploader, ReadObject, RelativePath, TransferError, WriteNamespaceGrant, download
19
19
  from ..types import DATA_OBJECTS_EXTENSION_URI, FilePart, FileWithBytes, FileWithUri, uploaded_file_part, uploaded_object_path
20
20
 
21
21
  if TYPE_CHECKING:
@@ -64,22 +64,31 @@ class DataPlane:
64
64
  async def file_parts(home: Path, paths: list[str], write_namespace: Optional[WriteNamespaceGrant],
65
65
  leaving_out: list[str] = ()) -> list:
66
66
  """A part for each file under ``paths`` of ``home`` but under none of ``leaving_out``, naming its path relative to
67
- ``home``: uploaded under ``write_namespace`` when there is one, else inline."""
67
+ ``home``: uploaded under ``write_namespace`` when there is one, else inline. A file a running app removes before it's
68
+ read is left out."""
68
69
  files = [(relative, source) for relative, source in _data_files(home, paths)
69
70
  if not any(PurePosixPath(relative).is_relative_to(owned) for owned in leaving_out)]
71
+ parts = []
70
72
  if write_namespace is not None:
71
73
  uploader = NamespaceUploader(write_namespace)
72
- parts = []
73
74
  for relative, source in files:
74
- await uploader.upload(relative, source)
75
+ try:
76
+ await uploader.upload(relative, source)
77
+ except (TransferError, OSError):
78
+ if source.exists():
79
+ raise
80
+ continue
75
81
  parts.append(uploaded_file_part(relative, name=source.name, mime_type=_mime(source)))
76
82
  return parts
77
- return [
78
- FilePart(file=FileWithBytes(bytes=base64.b64encode(source.read_bytes()).decode("ascii"),
79
- name=source.name, mimeType=_mime(source)),
80
- metadata={DATA_OBJECTS_EXTENSION_URI: {"path": relative}})
81
- for relative, source in files
82
- ]
83
+ for relative, source in files:
84
+ try:
85
+ data = source.read_bytes()
86
+ except FileNotFoundError:
87
+ continue
88
+ parts.append(FilePart(file=FileWithBytes(bytes=base64.b64encode(data).decode("ascii"), name=source.name,
89
+ mimeType=_mime(source)),
90
+ metadata={DATA_OBJECTS_EXTENSION_URI: {"path": relative}}))
91
+ return parts
83
92
 
84
93
 
85
94
  async def part_bytes(part: FilePart) -> bytes:
@@ -17,6 +17,10 @@ POLL_S = 0.2
17
17
  WITH_FDS = Path(__file__).with_name("_with_fds.py")
18
18
  #: The processes launched into the session, kept until they exit so each is reaped.
19
19
  _running: set[asyncio.Task] = set()
20
+ #: Each launched process's reaping, by pid, until its exit is asked for: its return code, negative for a signal.
21
+ _exits: dict[int, asyncio.Future] = {}
22
+ #: How many exits no one has asked for are kept, the latest.
23
+ KEPT_EXITS = 16
20
24
 
21
25
 
22
26
  async def wait_until_up() -> None:
@@ -38,12 +42,35 @@ async def launch(argv: list[str], cwd: Path, env: dict[str, str], fds: Optional[
38
42
  reaped = asyncio.ensure_future(process.wait())
39
43
  _running.add(reaped)
40
44
  reaped.add_done_callback(_running.discard)
45
+ _exits[process.pid] = reaped
46
+ reaped.add_done_callback(lambda _: _forget_old_exits())
41
47
  return process.pid
42
48
 
43
49
 
50
+ def _forget_old_exits() -> None:
51
+ """Drop all but the latest ``KEPT_EXITS`` exits no one has asked for."""
52
+ for pid in [pid for pid, reaped in _exits.items() if reaped.done()][:-KEPT_EXITS]:
53
+ del _exits[pid]
54
+
55
+
56
+ async def returncode(pid: int, timeout_s: float) -> Optional[int]:
57
+ """The return code the process ``launch`` started as ``pid`` exited with, negative for the signal that ended it,
58
+ once it's reaped within ``timeout_s``; None for one ``launch`` didn't start or that hasn't exited."""
59
+ reaped = _exits.get(pid)
60
+ if reaped is None:
61
+ return None
62
+ try:
63
+ code = await asyncio.wait_for(asyncio.shield(reaped), timeout_s)
64
+ except TimeoutError:
65
+ return None
66
+ _exits.pop(pid, None)
67
+ return code
68
+
69
+
44
70
  async def stop(pid: int, timeout_s: float) -> None:
45
71
  """End the session ``launch`` started as ``pid``, every process in it: asked to exit, then killed after
46
- ``timeout_s``."""
72
+ ``timeout_s``. No one asks how it exited once it's stopped."""
73
+ _exits.pop(pid, None)
47
74
  try:
48
75
  os.killpg(pid, signal.SIGTERM)
49
76
  except ProcessLookupError: