Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions packages/agentenv-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -880,14 +880,14 @@ under the session user's home (`.config/chromium` by default), whose contents fr
are the profile's template. The base class adds:

- **Its settings**, `BrowserSettings`, which a subclass's `Settings` extends: `start_urls`, opened in
tabs at launch, the first active (`["about:blank"]` by default; none may start with `-`), headless
the first from its command line and the rest over its pipe, since headless Chromium takes one; `window`,
tabs at launch, the first active (`["about:blank"]` by default; none may start with `-`); `window`,
1280 × 800, alone only; `locale`; `time_zone`, set as `TZ` in its launch's `env`; `geolocation` and
`notifications`, `deny` (the default), which answers a page's prompt for it, or `ask`; and `dialogs`, who
answers a page's JavaScript dialogs: `agent` (the default), or agentenv, accepting (`accept`, a `prompt()`
with its default text) or dismissing (`dismiss`) each. Either way agentenv leaves a page that asks before
it's left (`beforeunload`), so none of its answers blocks a navigation.
- **The flags agentenv owns**, after the subclass's command and before the start tabs: its profile, its
- **The flags agentenv owns**, after the subclass's command and before `about:blank`, the one tab its command line
opens: its profile, its
HTTP cache under `~/.cache` (`--disk-cache-dir`), `--remote-debugging-pipe`, `--no-first-run`,
`--no-default-browser-check`, `--lang` and, when `/dev/shm` holds under 1 GiB (Docker gives 64 MiB),
`--disable-dev-shm-usage`, so it keeps its shared memory in files. With
Expand Down Expand Up @@ -946,6 +946,8 @@ are the profile's template. The base class adds:
- `files/choose {paths}`, files in `~/Uploads` for the file chooser open on it, which headless, and so
alone, it intercepts;
- `page/state`, the active tab's id, URL and title and the dialog or file chooser open on it.
- `sites/set {sites}`, the env's own sites, each `{name, domains}`, a `*.` domain matching one label more, which
the deploy hands it before it launches the browser and its server keeps for every launch after.
- `cookies/get {urls?}`, `cookies/set {cookies}` and `cookies/delete {name?, domain?, path?}`, on its default
context, each cookie as Playwright's `storageState` has it, CDP's `Network.Cookie` fields (`name`, `value`,
`domain`, `path`, `expires`, `httpOnly`, `secure`, `sameSite`), so a storage state exported from Playwright loads
Expand All @@ -968,9 +970,26 @@ are the profile's template. The base class adds:
under the `write_namespace` grant when there is one, else inline, as `data/get` returns files. Inline, the whole
file is read into memory and sent base64 in one answer, so a large one belongs under a grant.

**Start tabs.** At each launch its start tabs open over its pipe, in a computer and alone alike: the first in the
blank tab its command line opened, the rest in tabs it opens blank after it, each held to the window's size
(headless) before it goes to its URL, so its page sees that size from its first script; then the first is brought
to the front. They load at once, and each settles once it has loaded (its load event fired, its document's final
status read), once it failed (a network error, or on one of the env's sites, as `sites/set` named them, a 4xx or
5xx status, which settles it at once), or once it has been waited on: every one settles within the browser's
ready timeout of its launch, the launch's setup counted against it, a tab on one of the env's sites waited on what's
left of it and any other on 30 s at most, so `data/reset`, a load and a deploy, which wait that timeout and a margin,
always get its outcome. Its `start_tabs_loaded` ready check passes once all have settled, so `cdp` and a deploy wait
for them. `record`'s `start_tabs` keeps each launch's (`StartTabsLaunch`: `at` and its
`tabs`, each a `StartTab`: `tab`, `url`, `final_url`, `status`, `error`, `load_ms`, `site` — the env's site's
name, null for a live one — and `outcome`, `loaded`, `failed` or `timed_out`), and `data/get`'s state keeps its
latest launch's. A deploy of a computer reads them once it's ready, keeps them in its record and fails, naming
each, if one on the env's sites didn't load; `data/reset` and a load that opens the start tabs again fail
the same way, while a server's restart only records it.

The browser's data plane is a file tree, every file by its path under the session user's home:
- `data/get` returns its state as `browser-state.json` (`BrowserState`: `cookies` and `local_storage`
as above, `tabs`, `closed_tabs`, `downloads` and `clock`, `real` with its `now`); its profile's files,
as above, `tabs`, `closed_tabs`, `downloads`, `clock`, `real` with its `now`, and `start_tabs`); its
profile's files,
each database with its journal or log, copied in one pass with the browser paused once its state is
read, so the files are of one moment, without caches (`Cache`, `Code Cache`,
`GPUCache`, the shader and Dawn caches, `CacheStorage`, `ScriptCache`, `Crashpad`) or locks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ def __init__(self) -> None:
self._computer_app_data = AppDataPlane(
self._computer_app, {op: getattr(self, name) for op, name in cls._data_hooks.items()}, self.paths,
cls.data_timeout_s, live=cls.data_live, keeps_paths=cls.data_keeps_paths,
resets_failed_add=self._resets_failed_add)
resets_failed_add=self._resets_failed_add, relaunched=self._relaunched)

def _launch_for(self, launch: Launch) -> Launch:
"""The launch its server uses, from the one declared for its OS, whose ``ready`` timeout it keeps: as
Expand All @@ -473,6 +473,9 @@ def _resets_failed_add(self) -> bool:
"""Whether a load that fails is followed by a reset, so the app isn't left half loaded: always."""
return True

async def _relaunched(self, op: str) -> None:
"""What a data call that stopped the app, ``op``, awaits once it has launched it again: nothing."""

@classmethod
def settings_on(cls, os_type: Optional[str]) -> type[BaseModel]:
"""Its ``Settings`` on ``os_type``, with the fields its OS adds."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import logging
import shutil
import tempfile
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from pathlib import Path, PurePosixPath
from typing import TYPE_CHECKING, Any, Optional

Expand Down Expand Up @@ -41,14 +41,17 @@ class AppDataPlane:

def __init__(self, runtime: AppRuntime, hooks: dict[str, Callable], paths: list[str], timeout_s: float, *,
live: frozenset[str] = frozenset(), keeps_paths: bool = False,
resets_failed_add: Callable[[], bool] = lambda: True) -> None:
resets_failed_add: Callable[[], bool] = lambda: True,
relaunched: Optional[Callable[[str], Awaitable[None]]] = None) -> None:
self.runtime = runtime
self.hooks = hooks
self.paths = paths
self.timeout_s = timeout_s
#: The operations served on the running app; whether a hook's reset needs the paths as installed; and whether
#: a load that fails is followed by a reset now.
self.live, self.keeps_paths, self.resets_failed_add = live, keeps_paths, resets_failed_add
#: What a call that stopped the app awaits once it has launched it again, whose error fails the call.
self.relaunched = relaunched
self.turn = asyncio.Lock()
self.changing: set[asyncio.Task] = set()

Expand Down Expand Up @@ -99,6 +102,8 @@ async def _change(self, op: str, served: Callable, args: tuple, answer: asyncio.
finally:
if running:
await self.runtime.launch_app(_CAUSES[op])
if running and self.relaunched is not None:
await self.relaunched(op)
except Exception as e:
if not answer.done():
answer.set_exception(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@
skill and ``browser/v1`` on its card. It runs alone, headless, on a computer with no display, a browser computer's,
whose screen is its page; which site proxy it goes through comes from a variable agentenv sets, never from its
settings."""
from .control import (ACTIVE_CHECK_TIMEOUT_S, AGENT, AGENTENV, CAPTURE_TIMEOUT_S, DOWNLOADS, EXIT, HASH_CHUNK_BYTES,
PAGE_INPUT, PAGES, PIN_TIMEOUT_S, STOP, UNKNOWN, UNNAVIGATED_CAPTURE_TIMEOUT_S, UPLOADS,
BrowserControl)
from .control import (ACTIVE_CHECK_TIMEOUT_S, AGENT, AGENTENV, CAPTURE_TIMEOUT_S, DOWNLOADS, EXIT, FAILED,
HASH_CHUNK_BYTES, LIVE_START_TAB_TIMEOUT_S, LOADED, PAGE_INPUT, PAGES, PIN_TIMEOUT_S, STOP,
TIMED_OUT, UNKNOWN, UNNAVIGATED_CAPTURE_TIMEOUT_S, UPLOADS, BrowserControl)
from .devtools import MAX_MESSAGE_BYTES, PIPE_IN, PIPE_OUT, DevToolsError, DevToolsPipe
from .environment import (ALONE_BYPASS, BLOCK, BROWSER, COMPUTER_BYPASS, OWNED_FLAGS, POLICIES, READY_POLL_S,
SHM_MIN_BYTES, SITE_PROXY_ENV, SKILL, BrowserSettings, WebsiteBrowserEnvironment, Window)
SHM_MIN_BYTES, SITE_PROXY_ENV, SITES, SKILL, BrowserSettings, WebsiteBrowserEnvironment,
Window, start_tab_why)
from .shapes import (BrowserRecord, ClosedTab, Cookie, CookieToSet, Dialog, Download, LaunchEntry, OpenTab,
OriginStorage, PartitionKey, StorageItem, origin_of)
OriginStorage, PartitionKey, SiteNames, StartTab, StartTabsLaunch, StorageItem, origin_of)

__all__ = ["ACTIVE_CHECK_TIMEOUT_S", "AGENT", "AGENTENV", "ALONE_BYPASS", "BLOCK", "BROWSER", "CAPTURE_TIMEOUT_S",
"COMPUTER_BYPASS", "DOWNLOADS", "EXIT", "HASH_CHUNK_BYTES", "MAX_MESSAGE_BYTES", "OWNED_FLAGS", "PAGE_INPUT",
"PAGES", "PIN_TIMEOUT_S", "PIPE_IN", "PIPE_OUT", "POLICIES", "READY_POLL_S", "SHM_MIN_BYTES",
"SITE_PROXY_ENV", "SKILL", "STOP", "UNKNOWN", "UNNAVIGATED_CAPTURE_TIMEOUT_S", "UPLOADS", "BrowserControl",
"BrowserSettings", "DevToolsError", "DevToolsPipe", "WebsiteBrowserEnvironment", "Window",
"BrowserRecord", "ClosedTab", "Cookie", "CookieToSet", "Dialog", "Download", "LaunchEntry", "OpenTab",
"OriginStorage", "PartitionKey", "StorageItem", "origin_of"]
"COMPUTER_BYPASS", "DOWNLOADS", "EXIT", "FAILED", "HASH_CHUNK_BYTES", "LIVE_START_TAB_TIMEOUT_S", "LOADED",
"MAX_MESSAGE_BYTES", "OWNED_FLAGS", "PAGE_INPUT", "PAGES", "PIN_TIMEOUT_S", "PIPE_IN", "PIPE_OUT",
"POLICIES", "READY_POLL_S", "SHM_MIN_BYTES", "SITE_PROXY_ENV", "SITES", "SKILL", "STOP", "TIMED_OUT",
"UNKNOWN", "UNNAVIGATED_CAPTURE_TIMEOUT_S", "UPLOADS", "BrowserControl", "BrowserSettings", "DevToolsError",
"DevToolsPipe", "WebsiteBrowserEnvironment", "Window", "BrowserRecord", "ClosedTab", "Cookie", "CookieToSet",
"Dialog", "Download", "LaunchEntry", "OpenTab", "OriginStorage", "PartitionKey", "SiteNames", "StartTab",
"StartTabsLaunch", "StorageItem", "origin_of", "start_tab_why"]
Loading
Loading