From 32abd6a30b7563e1ead18449b676ff412b489cc3 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Sat, 7 Nov 2026 08:41:16 -0800 Subject: [PATCH 1/2] fix(computer): keep app_data.get's earlier call, so apps exported before 0.9.1457 still boot The browser data plane made `paused` get's second positional parameter, ahead of `write_namespace`. Computer apps exported earlier call `get(self.paths, write_namespace)`, so on 0.9.1457 and later their data/get fails and the first-boot probe fails with "get() missing 1 required positional argument". `paths` and `write_namespace` are positional again, and `paused` is a keyword the data plane passes. Co-Authored-By: Claude Opus 5.5 --- .../src/agentenv_protocol/computer/app_data.py | 8 ++++---- .../agentenv-protocol/tests/test_computer_app_data.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py b/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py index 397c97699..abcf3d232 100644 --- a/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py +++ b/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py @@ -131,7 +131,7 @@ def _served(self, op: str) -> Optional[Callable]: if not self.paths: return None return {OP_ADD_DATA: functools.partial(add, paths=self.paths), - OP_GET_DATA: functools.partial(get, self.paths, self.runtime.paused), + OP_GET_DATA: functools.partial(get, self.paths, paused=self.runtime.paused), OP_RESET_DATA: functools.partial(reset, self.paths)}[op] async def _within(self, op: str, served: Callable, args: tuple, answer: asyncio.Future) -> Any: @@ -177,10 +177,10 @@ async def add(parts: list, paths: list[str]) -> None: await in_thread(write_within, Path.home(), files, paths) -async def get(paths: list[str], paused: Callable[[], contextlib.AbstractContextManager], - write_namespace: Optional[WriteNamespaceGrant]) -> list: +async def get(paths: list[str], write_namespace: Optional[WriteNamespaceGrant] = None, *, + paused: Callable[[], contextlib.AbstractContextManager] = contextlib.nullcontext) -> list: """A part for each file under ``paths``, copied with the app ``paused``, so they are of one moment, and sent from - the copy once it runs again.""" + the copy once it runs again. ``paths`` and ``write_namespace`` stay positional, as apps exported earlier call it.""" INITIAL.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(dir=INITIAL.parent) as staging: with paused(): diff --git a/packages/agentenv-protocol/tests/test_computer_app_data.py b/packages/agentenv-protocol/tests/test_computer_app_data.py index 99e18ec0c..6fb171ae4 100644 --- a/packages/agentenv-protocol/tests/test_computer_app_data.py +++ b/packages/agentenv-protocol/tests/test_computer_app_data.py @@ -138,6 +138,16 @@ async def test_an_app_with_only_paths_loads_exports_and_resets_them_as_installed assert reset == {"Mail/inbox.txt": b"installed"} +@pytest.mark.asyncio +async def test_get_keeps_its_earlier_call_with_paths_and_a_write_namespace_as_exported_apps_make_it(home): + (home / "Mail").mkdir() + (home / "Mail/inbox.txt").write_text("kept") + + parts = await app_data.get(["Mail"], None) + + assert [await part_bytes(part) for part in parts] == [b"kept"] + + @pytest.mark.asyncio async def test_a_file_under_none_of_its_paths_or_through_a_link_out_of_them_is_refused_and_nothing_is_written(home): (home / "Documents").mkdir() From 32c19eb25a78af18184242a785fa8c4496fec07a Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Sat, 7 Nov 2026 09:20:45 -0800 Subject: [PATCH 2/2] fix(computer): get pauses the running app itself, so callers need no paused argument `get(paths, write_namespace)` takes back its original signature and freezes the app this server launched while it copies the files, through a module-level `paused_app()` that `AppRuntime.paused` now delegates to. Exported apps' own data/get hooks, which call get with two arguments, get the same one-moment copy as the built-in data plane. Co-Authored-By: Claude Opus 5.5 --- .../src/agentenv_protocol/computer/app_data.py | 12 ++++++------ .../src/agentenv_protocol/computer/app_runtime.py | 13 +++++++++---- .../tests/test_computer_app_data.py | 5 ++++- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py b/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py index abcf3d232..1898b4f80 100644 --- a/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py +++ b/packages/agentenv-protocol/src/agentenv_protocol/computer/app_data.py @@ -18,6 +18,7 @@ from ..agent_env_environment import OP_ADD_DATA, OP_GET_DATA, OP_RESET_DATA from ..transfers import WriteNamespaceGrant from ..types import METHOD_ADD, METHOD_RESET, FilePart, uploaded_object_path +from .app_runtime import paused_app from .dataplane import data_files, file_parts, part_bytes if TYPE_CHECKING: @@ -131,7 +132,7 @@ def _served(self, op: str) -> Optional[Callable]: if not self.paths: return None return {OP_ADD_DATA: functools.partial(add, paths=self.paths), - OP_GET_DATA: functools.partial(get, self.paths, paused=self.runtime.paused), + OP_GET_DATA: functools.partial(get, self.paths), OP_RESET_DATA: functools.partial(reset, self.paths)}[op] async def _within(self, op: str, served: Callable, args: tuple, answer: asyncio.Future) -> Any: @@ -177,13 +178,12 @@ async def add(parts: list, paths: list[str]) -> None: await in_thread(write_within, Path.home(), files, paths) -async def get(paths: list[str], write_namespace: Optional[WriteNamespaceGrant] = None, *, - paused: Callable[[], contextlib.AbstractContextManager] = contextlib.nullcontext) -> list: - """A part for each file under ``paths``, copied with the app ``paused``, so they are of one moment, and sent from - the copy once it runs again. ``paths`` and ``write_namespace`` stay positional, as apps exported earlier call it.""" +async def get(paths: list[str], write_namespace: Optional[WriteNamespaceGrant]) -> list: + """A part for each file under ``paths``, copied with the app this server launched paused (``paused_app``), so they + are of one moment, and sent from the copy once it runs again; an app's own data/get hook calls it so too.""" INITIAL.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(dir=INITIAL.parent) as staging: - with paused(): + with paused_app(): await in_thread(_stage, Path.home(), paths, Path(staging)) return await file_parts(Path(staging), paths, write_namespace) diff --git a/packages/agentenv-protocol/src/agentenv_protocol/computer/app_runtime.py b/packages/agentenv-protocol/src/agentenv_protocol/computer/app_runtime.py index e1beac1fa..bf125781c 100644 --- a/packages/agentenv-protocol/src/agentenv_protocol/computer/app_runtime.py +++ b/packages/agentenv-protocol/src/agentenv_protocol/computer/app_runtime.py @@ -139,10 +139,8 @@ def running(self) -> bool: return _running() is not None def paused(self) -> contextlib.AbstractContextManager: - """The app this server launched last, every process of it, paused while the block runs, so a copy of its files - taken meanwhile is of one moment.""" - pid = _running() - return session.paused(pid) if pid is not None else contextlib.nullcontext() + """The app this server launched last paused while the block runs (``paused_app``).""" + return paused_app() def exited(self) -> bool: """Log that the app this server launched last ended on its own, with its exit code or the signal that ended @@ -242,6 +240,13 @@ def _running() -> Optional[int]: return pid if pid is not None and session.alive(pid) else None +def paused_app() -> contextlib.AbstractContextManager: + """The app this server launched last, every process of it, paused while the block runs, so a copy of its files + taken meanwhile is of one moment; nothing when it isn't running.""" + pid = _running() + return session.paused(pid) if pid is not None else contextlib.nullcontext() + + async def _bounded(name: str, check: Awaitable[Optional[str]]) -> Optional[str]: """What ``check`` finds wrong, within ``CHECK_TIMEOUT_S``; a check that runs past it or raises says so.""" try: diff --git a/packages/agentenv-protocol/tests/test_computer_app_data.py b/packages/agentenv-protocol/tests/test_computer_app_data.py index 6fb171ae4..b4638b324 100644 --- a/packages/agentenv-protocol/tests/test_computer_app_data.py +++ b/packages/agentenv-protocol/tests/test_computer_app_data.py @@ -139,13 +139,16 @@ async def test_an_app_with_only_paths_loads_exports_and_resets_them_as_installed @pytest.mark.asyncio -async def test_get_keeps_its_earlier_call_with_paths_and_a_write_namespace_as_exported_apps_make_it(home): +async def test_get_takes_paths_and_a_write_namespace_and_pauses_the_running_app_as_exported_apps_call_it(home, + desktop): (home / "Mail").mkdir() (home / "Mail/inbox.txt").write_text("kept") + await PathsMail()._computer_app.launch_app() parts = await app_data.get(["Mail"], None) assert [await part_bytes(part) for part in parts] == [b"kept"] + assert desktop == [("launch", 4242), ("pause", 4242), ("continue", 4242)] @pytest.mark.asyncio