agentenv-framework-protocol 0.1.432__tar.gz → 0.1.434__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/PKG-INFO +23 -10
  2. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/README.md +22 -9
  3. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/pyproject.toml +1 -1
  4. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/app.py +7 -2
  5. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/app_runtime.py +17 -2
  6. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/website_browser.py +66 -8
  7. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_website_browser.py +72 -11
  8. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/.gitignore +0 -0
  9. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/LICENSE +0 -0
  10. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/NOTICE +0 -0
  11. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/THIRD_PARTY_NOTICES.md +0 -0
  12. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/examples/__init__.py +0 -0
  13. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/examples/advanced_agent.py +0 -0
  14. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/examples/basic_agent.py +0 -0
  15. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/examples/custom_extensions_agent.py +0 -0
  16. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/examples/multimodal_agent.py +0 -0
  17. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/examples/reference_model.py +0 -0
  18. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/examples/streaming_agent.py +0 -0
  19. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/__init__.py +0 -0
  20. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  21. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  22. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  23. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  24. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  25. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  26. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  27. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  28. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  29. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  30. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  31. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/agent_env_environment.py +0 -0
  32. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/client.py +0 -0
  33. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/__init__.py +0 -0
  34. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/_declare.py +0 -0
  35. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/_linux.py +0 -0
  36. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/_load.py +0 -0
  37. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/_with_fds.py +0 -0
  38. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/app_data.py +0 -0
  39. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/apps.py +0 -0
  40. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/card.py +0 -0
  41. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/checks.py +0 -0
  42. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  43. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  44. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  45. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  46. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  47. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  48. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  49. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/dataplane.py +0 -0
  50. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/driver.py +0 -0
  51. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/errors.py +0 -0
  52. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/extensions.py +0 -0
  53. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/frames.py +0 -0
  54. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/keys.py +0 -0
  55. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/launch.py +0 -0
  56. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/manifest.py +0 -0
  57. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/operations.py +0 -0
  58. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/platform.py +0 -0
  59. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/queue.py +0 -0
  60. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/recording.py +0 -0
  61. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/results.py +0 -0
  62. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/run_install_step.py +0 -0
  63. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/runtime.py +0 -0
  64. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/serve.py +0 -0
  65. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/serve_app.py +0 -0
  66. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/session.py +0 -0
  67. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/settings.py +0 -0
  68. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/skill.py +0 -0
  69. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/spaces.py +0 -0
  70. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/computer/tools.py +0 -0
  71. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/errors.py +0 -0
  72. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/manifest.py +0 -0
  73. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/preflight.py +0 -0
  74. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/tool_access.py +0 -0
  75. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/tool_client.py +0 -0
  76. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/tool_context.py +0 -0
  77. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/transfers.py +0 -0
  78. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/src/agentenv_protocol/types.py +0 -0
  79. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/computer_fake.py +0 -0
  80. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/probe_fake.py +0 -0
  81. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_a2a_agent.py +0 -0
  82. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_a2a_agent_examples.py +0 -0
  83. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_a2a_cdp_config.py +0 -0
  84. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_a2a_object_transfers.py +0 -0
  85. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_a2a_peer_agents.py +0 -0
  86. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_a2a_proxy.py +0 -0
  87. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_a2a_staging.py +0 -0
  88. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_a2a_trajectory_log.py +0 -0
  89. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_agent_env_environment.py +0 -0
  90. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_client.py +0 -0
  91. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_app_data.py +0 -0
  92. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_app_server.py +0 -0
  93. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_conformance.py +0 -0
  94. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_conformance_checks.py +0 -0
  95. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_conformance_recording.py +0 -0
  96. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_dataplane.py +0 -0
  97. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_declare.py +0 -0
  98. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_driver.py +0 -0
  99. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_extensions.py +0 -0
  100. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_frames.py +0 -0
  101. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_queue.py +0 -0
  102. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_recording.py +0 -0
  103. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_serve.py +0 -0
  104. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_computer_spaces.py +0 -0
  105. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_control_token.py +0 -0
  106. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_data_objects.py +0 -0
  107. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_errors.py +0 -0
  108. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_extension_methods.py +0 -0
  109. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_intake.py +0 -0
  110. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_manifest.py +0 -0
  111. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_preflight.py +0 -0
  112. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_tool_access.py +0 -0
  113. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_tool_annotations.py +0 -0
  114. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_tool_client.py +0 -0
  115. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_tool_context.py +0 -0
  116. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/tests/test_tool_context_fastmcp.py +0 -0
  117. {agentenv_framework_protocol-0.1.432 → agentenv_framework_protocol-0.1.434}/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.432
3
+ Version: 0.1.434
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,12 @@ 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`; and `geolocation` and
817
+ `notifications`, `deny` (the default), which answers a page's prompt for it, or `ask`.
818
+ - **The flags agentenv owns**, after the subclass's command and before the start tabs: its profile, its
819
+ HTTP cache under `~/.cache` (`--disk-cache-dir`), `--remote-debugging-pipe`, `--no-first-run`,
820
+ `--no-default-browser-check`, `--lang` and, when `/dev/shm` holds under 1 GiB (Docker gives 64 MiB),
821
+ `--disable-dev-shm-usage`, so it keeps its shared memory in files. With
814
822
  `AGENTENV_SITE_PROXY` set, it goes out through that proxy, with WebRTC's non-proxied UDP off.
815
823
  Alone it bypasses nothing, loopback included; in a computer it bypasses loopback (`127.0.0.1`,
816
824
  `[::1]`, `localhost`), so the apps beside it stay reachable while `*.localhost` names still go
@@ -818,6 +826,9 @@ are the profile's template. The base class adds:
818
826
  except on the open web (`AGENTENV_BROWSER_OPEN_WEB=1`), and refuses to start with no proxy.
819
827
  `declaration()` refuses a subclass whose command carries any of these, or `--remote-allow-origins`, or
820
828
  any other way to open a debugging port.
829
+ - **Before each launch** it empties its cache, so no launch inherits another's, and writes the deploy's
830
+ prompt answers into its profile's `Default/Preferences` (`profile.default_content_setting_values`,
831
+ `2` to deny, left out to ask), which it reads as it starts.
821
832
  - **Alone, no desktop**: its launch matches and waits on no window, its server adds no launcher and,
822
833
  without `DISPLAY`, waits for no display, and its ready checks say when it's ready.
823
834
  - **`write_policies`**, an install step that writes agentenv's unattended-use policies (`POLICIES`)
@@ -828,7 +839,9 @@ are the profile's template. The base class adds:
828
839
  the result or raises `DevToolsError`, and `ConnectionError` with no browser connected. No port
829
840
  listens and no `DevToolsActivePort` is written, so nothing else in the computer can reach it over
830
841
  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.
842
+ launches it again, and a launch while it runs, from its menu launcher say, opens another window over
843
+ the pipe rather than a second copy, which would hand its window to the first and exit. The ready check
844
+ `devtools_answers` asks `Browser.getVersion` over the pipe.
832
845
  - **`browser/v1`** on its card, params only: `chromium` (its `version`), `clock: "real"` and, alone,
833
846
  `window`.
834
847
  - **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
@@ -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,12 @@ 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`; and `geolocation` and
761
+ `notifications`, `deny` (the default), which answers a page's prompt for it, or `ask`.
762
+ - **The flags agentenv owns**, after the subclass's command and before the start tabs: its profile, its
763
+ HTTP cache under `~/.cache` (`--disk-cache-dir`), `--remote-debugging-pipe`, `--no-first-run`,
764
+ `--no-default-browser-check`, `--lang` and, when `/dev/shm` holds under 1 GiB (Docker gives 64 MiB),
765
+ `--disable-dev-shm-usage`, so it keeps its shared memory in files. With
758
766
  `AGENTENV_SITE_PROXY` set, it goes out through that proxy, with WebRTC's non-proxied UDP off.
759
767
  Alone it bypasses nothing, loopback included; in a computer it bypasses loopback (`127.0.0.1`,
760
768
  `[::1]`, `localhost`), so the apps beside it stay reachable while `*.localhost` names still go
@@ -762,6 +770,9 @@ are the profile's template. The base class adds:
762
770
  except on the open web (`AGENTENV_BROWSER_OPEN_WEB=1`), and refuses to start with no proxy.
763
771
  `declaration()` refuses a subclass whose command carries any of these, or `--remote-allow-origins`, or
764
772
  any other way to open a debugging port.
773
+ - **Before each launch** it empties its cache, so no launch inherits another's, and writes the deploy's
774
+ prompt answers into its profile's `Default/Preferences` (`profile.default_content_setting_values`,
775
+ `2` to deny, left out to ask), which it reads as it starts.
765
776
  - **Alone, no desktop**: its launch matches and waits on no window, its server adds no launcher and,
766
777
  without `DISPLAY`, waits for no display, and its ready checks say when it's ready.
767
778
  - **`write_policies`**, an install step that writes agentenv's unattended-use policies (`POLICIES`)
@@ -772,7 +783,9 @@ are the profile's template. The base class adds:
772
783
  the result or raises `DevToolsError`, and `ConnectionError` with no browser connected. No port
773
784
  listens and no `DevToolsActivePort` is written, so nothing else in the computer can reach it over
774
785
  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.
786
+ launches it again, and a launch while it runs, from its menu launcher say, opens another window over
787
+ the pipe rather than a second copy, which would hand its window to the first and exit. The ready check
788
+ `devtools_answers` asks `Browser.getVersion` over the pipe.
776
789
  - **`browser/v1`** on its card, params only: `chromium` (its `version`), `clock: "real"` and, alone,
777
790
  `window`.
778
791
  - **Its skill**: sites by their real names, files to upload in `~/Uploads` and downloads in
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.432"
3
+ version = "0.1.434"
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."""
@@ -50,13 +50,15 @@ class AppRuntime:
50
50
 
51
51
  def __init__(self, launch: Optional[Launch], ready_checks: dict[str, Callable[[], Any]], timeout_s: float, *,
52
52
  desktop: bool = True, launch_fds: Optional[Callable[[], Awaitable[dict[int, int]]]] = None,
53
- relaunch_on_start: bool = False) -> None:
53
+ relaunch_on_start: bool = False,
54
+ launch_running: Optional[Callable[[], Awaitable[None]]] = None) -> None:
54
55
  self.launch = launch
55
56
  self.ready_checks = ready_checks
56
57
  self.timeout_s = timeout_s
57
58
  self.desktop = desktop
58
59
  self.launch_fds = launch_fds
59
60
  self.relaunch_on_start = relaunch_on_start
61
+ self.launch_running = launch_running
60
62
  self.started = time.time()
61
63
  self.launch_failed: Optional[str] = None
62
64
  self.checking: dict[str, asyncio.Future] = {}
@@ -84,7 +86,11 @@ class AppRuntime:
84
86
 
85
87
  async def launch_app(self) -> int:
86
88
  """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."""
89
+ closed once it has them; its pid. With ``launch_running``, an app this server launched that still runs does
90
+ that instead of starting a second copy, as a browser opens another window."""
91
+ if self.launch_running is not None and (running := _running()) is not None:
92
+ await self.launch_running()
93
+ return running
88
94
  fds = await self.launch_fds() if self.launch_fds is not None else {}
89
95
  try:
90
96
  pid = await session.launch(self.launch.command, Path.cwd(),
@@ -156,6 +162,15 @@ def _launched_at() -> Optional[float]:
156
162
  return None
157
163
 
158
164
 
165
+ def _running() -> Optional[int]:
166
+ """The pid of the app this server launched last, while it runs."""
167
+ try:
168
+ pid = int(PID.read_text())
169
+ except (FileNotFoundError, ValueError):
170
+ return None
171
+ return pid if session.alive(pid) else None
172
+
173
+
159
174
  async def _bounded(name: str, check: Awaitable[Optional[str]]) -> Optional[str]:
160
175
  """What ``check`` finds wrong, within ``CHECK_TIMEOUT_S``; a check that runs past it or raises says so."""
161
176
  try:
@@ -1,16 +1,19 @@
1
1
  """WebsiteBrowserEnvironment: a website browser as a computer app, run inside a computer or alone. A subclass says how
2
2
  its build installs and starts; this class adds what agentenv owns, the same in both: its flags after the subclass's
3
3
  command, the unattended-use policies, a CDP connection over the browser's debugging pipe, the settings every browser
4
- takes, its skill and ``browser/v1`` on its card. Whether it runs alone, on the open web and through which site proxy
5
- come from variables agentenv sets, never from its settings."""
4
+ takes, a fresh cache and the deploy's prompt answers at every launch, another window for a launch while it runs, its
5
+ skill and ``browser/v1`` on its card. Whether it runs alone, on the open web and through which site proxy come from
6
+ variables agentenv sets, never from its settings."""
6
7
  from __future__ import annotations
7
8
 
8
9
  import asyncio
9
10
  import itertools
10
11
  import json
12
+ import logging
11
13
  import os
14
+ import shutil
12
15
  from pathlib import Path
13
- from typing import Any, ClassVar, Optional
16
+ from typing import Any, Awaitable, Callable, ClassVar, Literal, Optional
14
17
 
15
18
  from pydantic import BaseModel, Field, field_validator
16
19
 
@@ -18,6 +21,8 @@ from .agent_env_environment import agent_skill
18
21
  from .computer import AppDeclaration, ComputerAppEnvironment, Launch, install_step, ready_check
19
22
  from .types import EnvironmentExtension
20
23
 
24
+ logger = logging.getLogger(__name__)
25
+
21
26
  ALONE_ENV = "AGENTENV_BROWSER_ALONE"
22
27
  OPEN_WEB_ENV = "AGENTENV_BROWSER_OPEN_WEB"
23
28
  SITE_PROXY_ENV = "AGENTENV_SITE_PROXY"
@@ -25,7 +30,12 @@ BROWSER = "urn:agentenv:browser/v1"
25
30
  #: The flags agentenv sets, by prefix, which a subclass's command may not carry: among them every way to open a debugging
26
31
  #: port, since the browser is reached only over its pipe.
27
32
  OWNED_FLAGS = ("--user-data-dir", "--remote-debugging-", "--remote-allow-origins", "--proxy-", "--no-proxy-server",
28
- "--headless", "--window-size", "--lang", "--force-webrtc-ip-handling-policy")
33
+ "--headless", "--window-size", "--lang", "--force-webrtc-ip-handling-policy", "--disk-cache-dir",
34
+ "--disable-dev-shm-usage")
35
+ #: Below this much /dev/shm, as Docker's 64 MiB default, the browser keeps its shared memory in files instead.
36
+ SHM_MIN_BYTES = 1 << 30
37
+ #: Chromium's content setting that denies a permission without a prompt.
38
+ BLOCK = 2
29
39
  #: Alone, loopback goes through the proxy too. In a computer, the apps on loopback beside it stay reachable; ``<-loopback>``
30
40
  #: removes Chromium's own rule, so ``*.localhost`` names still don't.
31
41
  ALONE_BYPASS = "<-loopback>"
@@ -147,6 +157,9 @@ class BrowserSettings(BaseModel):
147
157
  locale: str = "en-US"
148
158
  #: The IANA time zone its pages see.
149
159
  time_zone: str = "UTC"
160
+ #: Whether a page asking for the location, or to show notifications, is denied without a prompt or asks.
161
+ geolocation: Literal["deny", "ask"] = "deny"
162
+ notifications: Literal["deny", "ask"] = "deny"
150
163
 
151
164
  @field_validator("start_urls")
152
165
  @classmethod
@@ -189,12 +202,20 @@ class WebsiteBrowserEnvironment(ComputerAppEnvironment):
189
202
  def profile_dir(self) -> Path:
190
203
  return Path.home() / self.profile
191
204
 
205
+ def cache_dir(self) -> Path:
206
+ """Its HTTP cache, kept out of its profile, which holds only its data."""
207
+ return Path.home() / ".cache" / Path(self.profile).name
208
+
192
209
  def agentenv_flags(self) -> list[str]:
193
210
  """The flags agentenv owns. A browser alone goes out only through its site proxy, so it won't launch without
194
211
  one; in a computer it goes through one when the computer has one, loopback aside."""
195
212
  settings, alone, proxy = self.settings, _alone(), os.environ.get(SITE_PROXY_ENV)
196
- flags = [f"--user-data-dir={self.profile_dir()}", "--remote-debugging-pipe", "--no-first-run",
197
- "--no-default-browser-check", f"--lang={settings.locale}"]
213
+ flags = [f"--user-data-dir={self.profile_dir()}", f"--disk-cache-dir={self.cache_dir()}",
214
+ "--remote-debugging-pipe", "--no-first-run", "--no-default-browser-check", f"--lang={settings.locale}"]
215
+ if (shm := _shm_bytes()) < SHM_MIN_BYTES:
216
+ logger.info(f"/dev/shm holds {shm // 2**20} MiB, under {SHM_MIN_BYTES // 2**20} MiB: the browser keeps its "
217
+ "shared memory in files")
218
+ flags.append("--disable-dev-shm-usage")
198
219
  if proxy:
199
220
  flags += [f"--proxy-server={proxy}", f"--proxy-bypass-list={ALONE_BYPASS if alone else COMPUTER_BYPASS}",
200
221
  "--force-webrtc-ip-handling-policy=disable_non_proxied_udp"]
@@ -228,8 +249,11 @@ class WebsiteBrowserEnvironment(ComputerAppEnvironment):
228
249
  return self.settings.start_urls[:1] if _headless() else self.settings.start_urls
229
250
 
230
251
  async def _launch_fds(self) -> dict[int, int]:
231
- """A new debugging pipe for this launch: the browser's ends as its fds 3 and 4, this server's ends its CDP
232
- connection, over which the start tabs its command line can't open are opened."""
252
+ """Before each launch, an empty cache and the deploy's prompt answers in its profile; and a new debugging pipe:
253
+ the browser's ends as its fds 3 and 4, this server's ends its CDP connection, over which the start tabs its
254
+ command line can't open are opened."""
255
+ shutil.rmtree(self.cache_dir(), ignore_errors=True)
256
+ self._write_prompt_answers()
233
257
  to_browser_read, to_browser_write = os.pipe()
234
258
  from_browser_read, from_browser_write = os.pipe()
235
259
  await self.devtools.connect(to_browser_write, from_browser_read)
@@ -239,6 +263,24 @@ class WebsiteBrowserEnvironment(ComputerAppEnvironment):
239
263
  self._start_tabs.add_done_callback(lambda done: done.cancelled() or done.exception())
240
264
  return {PIPE_IN: to_browser_read, PIPE_OUT: from_browser_write}
241
265
 
266
+ def _write_prompt_answers(self) -> None:
267
+ """Its profile's preferences, which it reads as it starts, denying location and notification prompts or
268
+ leaving them to ask, as the deploy's settings say."""
269
+ preferences = self.profile_dir() / "Default" / "Preferences"
270
+ try:
271
+ current = json.loads(preferences.read_text(encoding="utf-8"))
272
+ except (OSError, ValueError):
273
+ current = {}
274
+ current = current if isinstance(current, dict) else {}
275
+ defaults = current.setdefault("profile", {}).setdefault("default_content_setting_values", {})
276
+ for name in ("geolocation", "notifications"):
277
+ if getattr(self.settings, name) == "deny":
278
+ defaults[name] = BLOCK
279
+ else:
280
+ defaults.pop(name, None)
281
+ preferences.parent.mkdir(parents=True, exist_ok=True)
282
+ preferences.write_text(json.dumps(current), encoding="utf-8")
283
+
242
284
  async def _open_tabs(self, urls: list[str]) -> None:
243
285
  """Each of ``urls`` in a tab behind the active one, in order."""
244
286
  for url in urls:
@@ -247,6 +289,14 @@ class WebsiteBrowserEnvironment(ComputerAppEnvironment):
247
289
  def _relaunch_on_start(self) -> bool:
248
290
  return True
249
291
 
292
+ def _launch_running(self) -> Callable[[], Awaitable[None]]:
293
+ return self._open_window
294
+
295
+ async def _open_window(self) -> None:
296
+ """Another window, which a launch of the browser while it runs opens over its pipe: a second copy would only
297
+ hand its window to the first and exit, taking its new pipe with it."""
298
+ await self.devtools.call("Target.createTarget", {"url": "about:blank", "newWindow": True})
299
+
250
300
  @install_step(scope="system")
251
301
  def write_policies(self) -> None:
252
302
  """agentenv's unattended-use policies, with the subclass's over them, where its build reads them."""
@@ -286,6 +336,14 @@ def _open_web() -> bool:
286
336
  return os.environ.get(OPEN_WEB_ENV) == "1"
287
337
 
288
338
 
339
+ def _shm_bytes(path: str = "/dev/shm") -> int:
340
+ try:
341
+ stats = os.statvfs(path)
342
+ except OSError:
343
+ return 0
344
+ return stats.f_frsize * stats.f_blocks
345
+
346
+
289
347
  def _headless() -> bool:
290
348
  """Alone it runs headless, but headful on Xvfb for the open web, whose sites block headless browsers."""
291
349
  return _alone() and not _open_web()
@@ -1,17 +1,20 @@
1
1
  """WebsiteBrowserEnvironment: agentenv's flags after a subclass's command, in a computer and alone, and the proxy rule
2
2
  for each; a subclass refused for carrying one of them; the start tabs, the time zone and the window from its settings;
3
3
  its launch run with no desktop when alone; a CDP connection over a new debugging pipe at each launch, which its ready
4
- check asks, and a browser launched again when its server restarts; its policies, card params and skill; and an app that
5
- isn't a browser launched and declared as before."""
4
+ check asks, and a browser launched again when its server restarts; its shared memory in files only on a small
5
+ /dev/shm; an empty cache and the deploy's prompt answers in its profile at each launch; its policies, card params and
6
+ skill; and an app that isn't a browser launched and declared as before."""
6
7
  from __future__ import annotations
7
8
 
8
9
  import asyncio
9
10
  import json
11
+ import os
10
12
  import sys
11
13
 
12
14
  import pytest
13
15
 
14
16
  from agentenv_protocol.computer import APP_SETTINGS_ENV, ComputerAppEnvironment, Launch, Ready, app, linux, session
17
+ from agentenv_protocol import website_browser
15
18
  from agentenv_protocol.computer import app_runtime
16
19
  from agentenv_protocol.computer.platform import Platform
17
20
  from agentenv_protocol.website_browser import (ALONE_BYPASS, ALONE_ENV, BROWSER, COMPUTER_BYPASS, OPEN_WEB_ENV,
@@ -19,9 +22,11 @@ from agentenv_protocol.website_browser import (ALONE_BYPASS, ALONE_ENV, BROWSER,
19
22
  WebsiteBrowserEnvironment)
20
23
 
21
24
  PROXY = "http://127.0.0.1:3128"
22
- OWNED = ["--user-data-dir", "--remote-debugging-pipe", "--no-first-run", "--no-default-browser-check", "--lang=en-US"]
23
- # A browser that answers CDP on its debugging pipe, fds 3 and 4, as a Chromium does: Browser.getVersion with its product,
24
- # Target.createTarget by writing the tab's URL to tabs.txt beside it, anything else with an error.
25
+ OWNED = ["--user-data-dir", "--disk-cache-dir", "--remote-debugging-pipe", "--no-first-run",
26
+ "--no-default-browser-check", "--lang=en-US"]
27
+ # A browser that answers CDP on its debugging pipe, fds 3 and 4, as a Chromium does: Browser.getVersion with its
28
+ # product, Target.createTarget by writing the tab's URL to tabs.txt beside it, a new window's after "window ", anything
29
+ # else with an error.
25
30
  FAKE_BROWSER = """import json, os
26
31
  buffer = b""
27
32
  while chunk := os.read(3, 65536):
@@ -31,9 +36,9 @@ while chunk := os.read(3, 65536):
31
36
  message = json.loads(raw)
32
37
  if message["method"] == "Browser.getVersion":
33
38
  answer = {"result": {"product": "Fake/1"}}
34
- elif message["method"] == "Target.createTarget" and message["params"]["background"]:
39
+ elif message["method"] == "Target.createTarget":
35
40
  with open("tabs.txt", "a") as tabs:
36
- tabs.write(message["params"]["url"] + "\\n")
41
+ tabs.write(("window " if message["params"].get("newWindow") else "") + message["params"]["url"] + "\\n")
37
42
  answer = {"result": {"targetId": message["params"]["url"]}}
38
43
  else:
39
44
  answer = {"error": {"code": -32601, "message": "unknown method"}}
@@ -60,8 +65,10 @@ class OnDemandBrowser(WebsiteBrowserEnvironment):
60
65
 
61
66
  @pytest.fixture(autouse=True)
62
67
  def browser_home(monkeypatch, tmp_path):
63
- """A browser on Linux, its session user's home under ``tmp_path``, inside a computer with no site proxy."""
68
+ """A browser on Linux, its session user's home under ``tmp_path``, inside a computer with no site proxy and 1 GiB
69
+ of /dev/shm."""
64
70
  monkeypatch.setattr(app, "detect", lambda: Platform(os="linux"))
71
+ monkeypatch.setattr(website_browser, "_shm_bytes", lambda path="/dev/shm": 1 << 30)
65
72
  monkeypatch.setenv("HOME", str(tmp_path / "home"))
66
73
  (tmp_path / "home").mkdir()
67
74
  monkeypatch.setenv("ENVIRONMENT_NAME", "chromium")
@@ -78,7 +85,8 @@ def test_in_a_computer_its_flags_follow_its_command_then_its_start_tabs_and_its_
78
85
  launch = Browser().launch
79
86
 
80
87
  profile = f"--user-data-dir={browser_home / '.config/chromium'}"
81
- assert launch.command == ["chromium/chrome", "--no-sandbox", profile, *OWNED[1:], "about:blank"]
88
+ cache = f"--disk-cache-dir={browser_home / '.cache/chromium'}"
89
+ assert launch.command == ["chromium/chrome", "--no-sandbox", profile, cache, *OWNED[2:], "about:blank"]
82
90
  assert launch.env == {"TZ": "UTC"}
83
91
  assert launch.window == {"class": "Chromium"} and launch.ready == Ready(window=True, timeout_s=30)
84
92
 
@@ -89,7 +97,7 @@ def test_in_a_computer_with_a_site_proxy_only_loopback_bypasses_it(monkeypatch):
89
97
  command = Browser().launch.command
90
98
 
91
99
  assert [f"--proxy-server={PROXY}", f"--proxy-bypass-list={COMPUTER_BYPASS}",
92
- "--force-webrtc-ip-handling-policy=disable_non_proxied_udp"] == command[7:10]
100
+ "--force-webrtc-ip-handling-policy=disable_non_proxied_udp"] == command[8:11]
93
101
  assert not any(flag.startswith(("--headless", "--window-size")) for flag in command)
94
102
 
95
103
 
@@ -142,7 +150,8 @@ def test_alone_it_refuses_to_launch_without_a_site_proxy(monkeypatch):
142
150
 
143
151
 
144
152
  @pytest.mark.parametrize("flag", ["--remote-debugging-port=9222", "--remote-allow-origins=*", "--headless",
145
- "--proxy-server=http://elsewhere:3128", "--user-data-dir=/tmp/profile"])
153
+ "--proxy-server=http://elsewhere:3128", "--user-data-dir=/tmp/profile",
154
+ "--disk-cache-dir=/tmp/cache", "--disable-dev-shm-usage"])
146
155
  def test_a_subclass_whose_command_carries_a_flag_agentenv_owns_is_refused_when_declared(flag):
147
156
  @linux(launch=Launch(command=["chromium/chrome", flag]))
148
157
  class Widened(WebsiteBrowserEnvironment):
@@ -152,6 +161,37 @@ def test_a_subclass_whose_command_carries_a_flag_agentenv_owns_is_refused_when_d
152
161
  Widened.declaration()
153
162
 
154
163
 
164
+ @pytest.mark.parametrize("mib, in_files", [(64, True), (1024, False)], ids=["docker-default", "sized"])
165
+ def test_it_keeps_its_shared_memory_in_files_only_when_dev_shm_is_under_1_gib(monkeypatch, mib, in_files):
166
+ monkeypatch.setattr(website_browser, "_shm_bytes", lambda path="/dev/shm": mib * 2**20)
167
+
168
+ assert ("--disable-dev-shm-usage" in Browser().launch.command) is in_files
169
+
170
+
171
+ @pytest.mark.asyncio
172
+ @pytest.mark.parametrize("settings, prompts", [
173
+ ({}, {"geolocation": 2, "notifications": 2}),
174
+ ({"notifications": "ask"}, {"geolocation": 2}),
175
+ ], ids=["deny", "ask"])
176
+ async def test_each_launch_starts_with_an_empty_cache_and_the_deploys_prompt_answers_in_its_profile(
177
+ browser_home, monkeypatch, settings, prompts):
178
+ _settings(monkeypatch, **settings)
179
+ preferences = browser_home / ".config/chromium/Default/Preferences"
180
+ preferences.parent.mkdir(parents=True)
181
+ preferences.write_text(json.dumps({"homepage": "about:blank", "profile": {"default_content_setting_values": {
182
+ "notifications": 1, "cookies": 1}}}))
183
+ (browser_home / ".cache/chromium/Cache").mkdir(parents=True)
184
+ browser = Browser()
185
+
186
+ for fd in (await browser._launch_fds()).values():
187
+ os.close(fd)
188
+ browser.devtools.close()
189
+
190
+ assert json.loads(preferences.read_text()) == {"homepage": "about:blank", "profile": {
191
+ "default_content_setting_values": {"cookies": 1, **prompts}}}
192
+ assert not (browser_home / ".cache/chromium").exists()
193
+
194
+
155
195
  def test_its_settings_give_its_start_tabs_language_and_time_zone_and_no_start_url_is_a_flag(monkeypatch):
156
196
  _settings(monkeypatch, start_urls=["https://slack.com", "https://linear.app"], locale="fr-FR",
157
197
  time_zone="Europe/Paris")
@@ -242,6 +282,26 @@ async def test_each_launch_gets_a_new_pipe_its_server_talks_cdp_over_and_its_rea
242
282
  assert "devtools_answers" in runtime.ready_checks
243
283
 
244
284
 
285
+ @pytest.mark.asyncio
286
+ async def test_a_launch_while_it_runs_opens_a_window_over_its_pipe_and_starts_no_second_copy(monkeypatch, tmp_path,
287
+ browser_home):
288
+ monkeypatch.chdir(tmp_path)
289
+ monkeypatch.setattr(app_runtime, "LAUNCHED", tmp_path / "launched")
290
+ browser = FakeBrowser()
291
+ runtime = browser._computer_app
292
+
293
+ first = await runtime.launch_app()
294
+ (browser_home / ".cache/chromium").mkdir(parents=True, exist_ok=True)
295
+ (browser_home / ".cache/chromium/kept").touch()
296
+ second = await runtime.launch_app()
297
+ answers = await browser.devtools_answers()
298
+ await runtime.stop_app()
299
+ browser.devtools.close()
300
+
301
+ assert second == first and answers is True and (browser_home / ".cache/chromium/kept").exists()
302
+ assert (tmp_path / "tabs.txt").read_text().splitlines() == ["window about:blank"]
303
+
304
+
245
305
  @pytest.mark.asyncio
246
306
  async def test_headless_it_opens_one_start_tab_from_its_command_line_and_the_rest_over_its_pipe(monkeypatch, tmp_path):
247
307
  monkeypatch.chdir(tmp_path)
@@ -356,6 +416,7 @@ def test_an_app_that_isnt_a_browser_launches_and_declares_as_before():
356
416
  stored = Mail.declaration().model_dump(mode="json", exclude_none=True)
357
417
 
358
418
  assert mail.launch is Mail._installs["linux"].launch and mail._computer_app.desktop is True
419
+ assert mail._computer_app.launch_running is None
359
420
  assert stored["os"]["linux"]["launch"] == {"at": "boot", "command": ["mail"], "window": {"class": "Mail"},
360
421
  "ready": {"window": True, "timeout_s": 60.0}}
361
422