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
22 changes: 18 additions & 4 deletions packages/agentenv-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,8 +505,8 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
or a `transfers.ReadObject` read grant under the marker's `read`, and writes them atomically.
`data/get` returns the files under the `paths` it's given, relative to the home, or under the
card's `default_data_paths` (`Desktop`, `Documents`, `Downloads`) when it's given none, inline or
uploaded under a `write_namespace` grant. An environment whose `@get_data` hook takes no `paths`
refuses them.
uploaded under a `write_namespace` grant, leaving out the paths its apps keep their data in. An
environment whose `@get_data` hook takes no `paths` refuses them.
- **The card** (the name from `ENVIRONMENT_NAME`, then `@computer_driver`'s), and per-role tool
access (`tool_access` is on).

Expand Down Expand Up @@ -674,8 +674,10 @@ class MailApp(ComputerAppEnvironment):
naming one the app declares no install for is refused when the class is read. A step reads
`self.build_settings`; a deploy's `self.settings` aren't known at build, and reading one raises
`AttributeError` (`python -m agentenv_protocol.computer.run_install_step <file>:<Class> <method>`).
- **Its skill** is a file of its folder (`skill = "skill.md"`) or an `@agent_skill` method, not both;
its data hooks are the usual `@add_data`, `@get_data` and `@reset_data`.
- **Its skill** is a file of its folder (`skill = "skill.md"`) or an `@agent_skill` method, not both.
- **Its data** is under the `paths` its OS's decorator names, relative to the session user's home,
none inside another (`@linux(..., paths=["Mail", ".config/mail/accounts.json"])`), or what its own
`@add_data`, `@get_data` and `@reset_data` hooks serve instead (below).
- **`Settings`** is typed; a computer hands each app the values a deploy gives it, as one JSON value
in `APP_SETTINGS_ENV` (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads
its own, by `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.
Expand All @@ -701,6 +703,18 @@ folder: the computer is ready once every app's server answers and each `boot` ap
no launch, says it's ready; a `demand` app counts once something launches it. What the server does
in the session (`agentenv_protocol.computer.session`) is Linux's.

The server serves the app's data plane and its skill. With `paths` alone, `data/add` writes the files
it's given under them and refuses any other path, or one a link leads out of them; `data/get`
returns the files there, a file tree; and `data/reset` puts them back as the server found them when
it first started on this boot, which is the app as installed. Hooks serve what they mark instead.
A hook's `data/add` parts come as they were sent, inline or as a read grant, and
`part_bytes(part)` returns a part's bytes either way. Either way the base class takes data calls and
launches one at a time, runs a load and a reset with the app stopped and relaunches it after,
follows a failed load with a reset, and fails a call that runs past `data_timeout_s` (300 s); a sync
hook runs in a thread, which the reset and relaunch after it wait for. The driver fronts its apps:
its card lists each one's card as a child environment, rooted at `/svc/<app>/`, where it forwards
requests to the app's server.

`MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
import types
import uuid
from abc import ABC, abstractmethod
from typing import Annotated, Any, Callable, ClassVar, Literal, NamedTuple, Union, get_args, get_origin, get_type_hints
from typing import (Annotated, Any, Callable, ClassVar, Literal, NamedTuple, Optional, Union, get_args, get_origin,
get_type_hints)

from pydantic import TypeAdapter, ValidationError
from pydantic.fields import FieldInfo
Expand Down Expand Up @@ -376,7 +377,10 @@ def _card_for(self, app: Any) -> EnvironmentCard:
return self.environment_card

async def _serve_card(self, request: Request) -> Response:
return _json_response(self.environment_card.model_dump())
card = self.environment_card
if isinstance(self.handler, AgentEnvEnvironment) and (children := await self.handler.children_environments()):
card = card.model_copy(update={"children_environments": children})
return _json_response(card.model_dump())

async def update_tool(self, name: str, *, description: str) -> None:
"""Change a served tool's description after the mount: on the card and in tools/list alike."""
Expand Down Expand Up @@ -662,6 +666,11 @@ def declared_extensions(self) -> list[EnvironmentExtension]:
"""Declaration-only extensions to advertise, like ``intake/v1``: a promise with params and no handler."""
return []

async def children_environments(self) -> Optional[list[EnvironmentCard]]:
"""The environments this one fronts, as each serves its card with its paths under this one's, listed on the card
each time it's served; None for none."""
return None

def served_extensions(self, extensions: list[EnvironmentExtension]) -> list[EnvironmentExtension]:
"""The ``@extension`` handlers' extensions to serve, chosen at mount: one left out is not served, and of one served
with ``op``, only the methods its returned descriptor still lists."""
Expand Down
6 changes: 6 additions & 0 deletions packages/agentenv-protocol/src/agentenv_protocol/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ def intake_declaration(card: dict) -> dict | None:
return params if params else None


def operations(card: dict) -> list[str] | None:
"""The data-plane methods a card declares it serves (``capabilities.operations``), or None when it declares none,
as an older card doesn't."""
return (card.get("capabilities") or {}).get("operations")


def find_tool(card: dict, name: str) -> dict | None:
"""Return the MCP tool advertised under `name`, or None.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
type_text,
write_files,
)
from .dataplane import part_bytes
from .keys import PRINTABLE_KEYS
from .manifest import ManifestSegment, RecordingManifest
from .platform import Platform
Expand Down Expand Up @@ -107,7 +108,7 @@ def __getattr__(name: str) -> Any:
"ComputerAppEnvironment", "computer_app", "install", "linux", "install_step", "ready_check", "Linux", "Service",
"Launch", "Ready", "Packages", "AptSource", "Download", "RunScript",
"AppDeclaration", "AppOs",
"APP_SETTINGS_ENV",
"APP_SETTINGS_ENV", "part_bytes",
"Display", "Image", "Effect", "ExecResult", "ReadyCheck", "Platform", "ComputerSettings", "SETTINGS_ENV",
"PRINTABLE_KEYS",
"display", "set_display", "capture", "click", "drag", "scroll", "type_text", "press",
Expand Down
Loading
Loading