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.
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/PKG-INFO +39 -12
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/README.md +38 -11
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/pyproject.toml +1 -1
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/app.py +7 -2
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/app_data.py +4 -2
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/app_runtime.py +67 -4
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/dataplane.py +19 -10
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/session.py +28 -1
- agentenv_framework_protocol-0.1.435/src/agentenv_protocol/website_browser.py +592 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_dataplane.py +43 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_website_browser.py +246 -18
- agentenv_framework_protocol-0.1.433/src/agentenv_protocol/website_browser.py +0 -296
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/_declare.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/_linux.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/_load.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/_with_fds.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/apps.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/card.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/checks.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/driver.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/launch.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/recording.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/run_install_step.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/serve_app.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/settings.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/skill.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_cdp_config.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_app_data.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_app_server.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_declare.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_driver.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_recording.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_serve.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_control_token.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.433 → agentenv_framework_protocol-0.1.435}/tests/test_tool_context_fastmcp.py +0 -0
- {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.
|
|
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,
|
|
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
|
|
758
|
-
in `
|
|
759
|
-
|
|
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`;
|
|
812
|
-
|
|
813
|
-
|
|
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
|
|
832
|
-
|
|
833
|
-
`
|
|
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,
|
|
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
|
|
702
|
-
in `
|
|
703
|
-
|
|
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`;
|
|
756
|
-
|
|
757
|
-
|
|
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
|
|
776
|
-
|
|
777
|
-
`
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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:
|