agentenv-framework-protocol 0.1.375__tar.gz → 0.1.377__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 (88) hide show
  1. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/PKG-INFO +174 -1
  2. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/README.md +168 -0
  3. agentenv_framework_protocol-0.1.377/examples/computer_x11/Dockerfile +19 -0
  4. agentenv_framework_protocol-0.1.377/examples/computer_x11/README.md +60 -0
  5. agentenv_framework_protocol-0.1.377/examples/computer_x11/driver.py +374 -0
  6. agentenv_framework_protocol-0.1.377/examples/computer_x11/fixture.py +173 -0
  7. agentenv_framework_protocol-0.1.377/examples/computer_x11/start.sh +9 -0
  8. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/pyproject.toml +8 -1
  9. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/__init__.py +5 -0
  10. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/agent_env_environment.py +390 -50
  11. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/client.py +26 -2
  12. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/__init__.py +67 -0
  13. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/card.py +105 -0
  14. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/checks.py +42 -0
  15. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/dataplane.py +116 -0
  16. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/driver.py +120 -0
  17. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/errors.py +71 -0
  18. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/extensions.py +131 -0
  19. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/frames.py +163 -0
  20. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/keys.py +66 -0
  21. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/linux.py +100 -0
  22. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/operations.py +183 -0
  23. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/platform.py +76 -0
  24. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/queue.py +51 -0
  25. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/results.py +94 -0
  26. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/runtime.py +226 -0
  27. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/settings.py +43 -0
  28. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/spaces.py +49 -0
  29. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/computer/tools.py +423 -0
  30. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/errors.py +29 -0
  31. agentenv_framework_protocol-0.1.377/src/agentenv_protocol/tool_access.py +98 -0
  32. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/types.py +3 -3
  33. agentenv_framework_protocol-0.1.377/tests/computer_fake.py +218 -0
  34. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_agent_env_environment.py +22 -0
  35. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_client.py +26 -1
  36. agentenv_framework_protocol-0.1.377/tests/test_computer_dataplane.py +153 -0
  37. agentenv_framework_protocol-0.1.377/tests/test_computer_driver.py +386 -0
  38. agentenv_framework_protocol-0.1.377/tests/test_computer_extensions.py +196 -0
  39. agentenv_framework_protocol-0.1.377/tests/test_computer_frames.py +157 -0
  40. agentenv_framework_protocol-0.1.377/tests/test_computer_queue.py +477 -0
  41. agentenv_framework_protocol-0.1.377/tests/test_computer_spaces.py +119 -0
  42. agentenv_framework_protocol-0.1.377/tests/test_computer_x11_example.py +290 -0
  43. agentenv_framework_protocol-0.1.377/tests/test_errors.py +158 -0
  44. agentenv_framework_protocol-0.1.377/tests/test_extension_methods.py +236 -0
  45. agentenv_framework_protocol-0.1.377/tests/test_tool_access.py +195 -0
  46. agentenv_framework_protocol-0.1.377/tests/test_tool_annotations.py +211 -0
  47. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/.gitignore +0 -0
  48. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/LICENSE +0 -0
  49. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/NOTICE +0 -0
  50. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/THIRD_PARTY_NOTICES.md +0 -0
  51. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/examples/__init__.py +0 -0
  52. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/examples/advanced_agent.py +0 -0
  53. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/examples/basic_agent.py +0 -0
  54. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/examples/custom_extensions_agent.py +0 -0
  55. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/examples/multimodal_agent.py +0 -0
  56. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/examples/reference_model.py +0 -0
  57. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/examples/streaming_agent.py +0 -0
  58. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  59. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  60. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  61. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  62. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  63. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  64. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  65. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  66. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  67. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  68. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  69. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/manifest.py +0 -0
  70. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/preflight.py +0 -0
  71. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/tool_client.py +0 -0
  72. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/tool_context.py +0 -0
  73. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/src/agentenv_protocol/transfers.py +0 -0
  74. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_a2a_agent.py +0 -0
  75. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_a2a_agent_examples.py +0 -0
  76. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_a2a_object_transfers.py +0 -0
  77. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_a2a_peer_agents.py +0 -0
  78. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_a2a_proxy.py +0 -0
  79. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_a2a_staging.py +0 -0
  80. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_a2a_trajectory_log.py +0 -0
  81. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_data_objects.py +0 -0
  82. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_intake.py +0 -0
  83. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_manifest.py +0 -0
  84. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_preflight.py +0 -0
  85. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_tool_client.py +0 -0
  86. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_tool_context.py +0 -0
  87. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/tests/test_tool_context_fastmcp.py +0 -0
  88. {agentenv_framework_protocol-0.1.375 → agentenv_framework_protocol-0.1.377}/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.375
3
+ Version: 0.1.377
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
@@ -34,10 +34,15 @@ Requires-Dist: mcp<2,>=1.25; extra == 'agent'
34
34
  Requires-Dist: python-multipart>=0.0.13; extra == 'agent'
35
35
  Requires-Dist: regex>=2023.12.25; extra == 'agent'
36
36
  Requires-Dist: uvicorn>=0.30; extra == 'agent'
37
+ Provides-Extra: computer
38
+ Requires-Dist: mcp<2,>=1.25; extra == 'computer'
39
+ Requires-Dist: pillow>=10.4; extra == 'computer'
40
+ Requires-Dist: uvicorn>=0.30; extra == 'computer'
37
41
  Provides-Extra: dev
38
42
  Requires-Dist: a2a-sdk[http-server]<0.4,>=0.3.26; extra == 'dev'
39
43
  Requires-Dist: fastmcp<4,>=3.4; extra == 'dev'
40
44
  Requires-Dist: mcp<2,>=1.25; extra == 'dev'
45
+ Requires-Dist: pillow>=10.4; extra == 'dev'
41
46
  Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
42
47
  Requires-Dist: pytest>=9.0; extra == 'dev'
43
48
  Requires-Dist: python-multipart>=0.0.13; extra == 'dev'
@@ -136,10 +141,34 @@ step names it. Steps that name it show and hide it as they do any tool. Steps fo
136
141
  that role's earlier named steps, and a full reset (`role: "*", tools: "*"`) clears all of them, so
137
142
  grant a hidden tool after such steps. Trigger tool actions call it as usual.
138
143
 
144
+ `@tool(..., annotations={"readOnlyHint": True}, structured_output=True, meta={...})` lists the tool
145
+ with MCP `ToolAnnotations`, an output schema and `_meta`, on mcp's FastMCP and on fastmcp alike.
146
+ `structured_output` follows mcp's FastMCP (None derives an output schema from the return annotation
147
+ where it can, False lists none); a tool returning `Annotated[CallToolResult, Model]` sends its
148
+ result as is, image blocks included, and is listed with `Model`'s schema. An environment chooses
149
+ at mount which of its `@tool` methods it serves, and with which descriptions, by overriding
150
+ `served_tools(tools)`. After the mount, `env.mounted_card()` is the card the application serves, and
151
+ `await env.update_tool(name, description=...)` changes a served tool's description on it and in
152
+ `tools/list` alike, on both FastMCPs.
153
+
139
154
  `AgentEnvStarletteApplication` mounts the same handler onto a Starlette/FastAPI app instead of
140
155
  FastMCP; since those apps have no MCP tool registry, constructing one with `@tool` methods
141
156
  raises.
142
157
 
158
+ ### Errors
159
+
160
+ Raise `AgentEnvError(code, message, *, retryable=False, applied="no", hint=None, status=400)` to
161
+ answer a caller with a code rather than a crash. From an `@extension` method it answers with
162
+ `status` and the body `{"ok": false, "error": {code, message, retryable, applied, hint?}}`; from a
163
+ `@tool` it answers `isError` with `"<code>: <message>"` as the text and the same fields as the
164
+ structured content. `applied` says whether the call's effect happened (`yes`, `no`, or `unknown`
165
+ when it may have been delivered before something failed), and callers retry on their own only when
166
+ `retryable` is true and `applied` is `no`. Any other exception in an extension still answers
167
+ `extension_failed` with status 500, now with `applied: "unknown"`. On the client side,
168
+ `invoke_extension` raises `ExtensionCallError`, an `httpx.HTTPStatusError` carrying `code`,
169
+ `detail`, `retryable`, `applied` and `hint`; a body without those fields, such as a proxy's error
170
+ page, leaves `code` None and `applied` `unknown`.
171
+
143
172
  ### Who is calling
144
173
 
145
174
  A tool that needs the caller declares a parameter annotated `ToolContext` (any name) and receives
@@ -226,6 +255,40 @@ extension's, and `client.invoke_extension(base_url, card, uri, params, method=..
226
255
  its HTTP verb. Without `method`, `invoke_extension` calls the first method listed; an extension
227
256
  with several methods, such as a gateway's `urn:agentenv:clock/v1`, should always be called by name.
228
257
 
258
+ An extension with several methods is served by giving each handler an `op`. Handlers sharing a URI
259
+ merge into one card entry, each method under `params.methods.<op>` with its own verb and endpoint,
260
+ by default `/agentenv/ext/<extension>/<op>`, where `<extension>` is the URN's name without
261
+ `urn:agentenv:` and the version. Extension-level params, such as features and limits, come from the
262
+ environment's `extension_params(uri)`, merged beside `methods`. A declaration-only extension, a
263
+ promise with params and no handler like `intake/v1`, is returned from `declared_extensions()`.
264
+ Two handlers for one method, or a handler without `op` on a URI another handler also serves, are
265
+ refused at mount.
266
+
267
+ An environment chooses at mount which of its `@extension` handlers it serves by overriding
268
+ `served_extensions(extensions)`: an extension it leaves out is not served, and of one served with
269
+ `op`, only the methods its returned descriptor still lists under `params.methods`.
270
+ `served_operations(operations)` chooses the data-plane methods (`data/add` and the others it
271
+ marks) the same way. Either hook returning something with no handler is refused at mount.
272
+
273
+ ```python
274
+ @extension("urn:agentenv:computer-display/v1", op="get", method="GET")
275
+ async def display_get(self) -> dict: ...
276
+
277
+ @extension("urn:agentenv:computer-display/v1", op="set")
278
+ async def display_set(self, width: int, height: int) -> dict: ...
279
+ ```
280
+
281
+ ### Tool access without a gateway
282
+
283
+ An environment agents reach directly, with no gateway in front, can enforce per-role tool access
284
+ itself: set `tool_access = True` on the class. It then filters `tools/list` and refuses `tools/call`
285
+ (`permission_denied`) by the caller's role, read from `AgentEnv-Role` or a gateway-stamped `_meta`
286
+ role, and serves `urn:agentenv:disable-tool/v1` and `urn:agentenv:enable-tool/v1` at the gateway's
287
+ paths (`POST /tools/disable`, `POST /tools/enable`) with its bodies (`{role, tools: [...] | "*"}` to
288
+ `{role, disabled, allowed}`) and its rules, hidden-by-default tools included. So agent-env's
289
+ `modify_env_tool_access` step works on such an env unchanged. A call with no role is `default`, as
290
+ at the gateway.
291
+
229
292
  Where the binding lives depends on the app. On mcp's own FastMCP it wraps the tool manager's dispatch; on the standalone `fastmcp` package it joins the app's middleware chain as the outermost layer, so a handler's own middleware runs under it too. An app with neither seam mounts unbound, with a warning, unless a tool declares a `ToolContext` parameter or the handler defines `on_tool_call`: then the mount is refused rather than serving an empty caller.
230
293
 
231
294
  ### Uploading an export through a grant
@@ -300,6 +363,116 @@ async with ToolSession(mcp_url=mcp_url, role="alice@example.com") as session:
300
363
  - **Tools:** `list_tools()` returns the tools the role may use as `EnvironmentTool`, the type a card's
301
364
  `capabilities.tools` holds. Each `inputSchema` is the JSON Schema a model API takes as the tool's parameters.
302
365
 
366
+ ## Computer drivers
367
+
368
+ A computer driver is what runs inside a computer an agent uses through screenshots and input. Write
369
+ one as a `ComputerDriverEnvironment` subclass from `agentenv_protocol.computer`, which needs the
370
+ `computer` extra (`pip install 'agentenv-framework-protocol[computer]'`: mcp, uvicorn and Pillow).
371
+ The driver marks the methods that capture the screen and send input; the base class serves the
372
+ computer contract from them, so the tools, results and errors can't drift from it. A complete
373
+ driver for an X11 desktop, with an image to run it in, is in
374
+ [`examples/computer_x11/`](https://github.com/scaleapi/agentenv-framework/tree/main/packages/agentenv-protocol/examples/computer_x11).
375
+
376
+ ```python
377
+ from agentenv_protocol.computer import (
378
+ ComputerDriverEnvironment, Display, computer_driver,
379
+ capture, click, display, drag, press, scroll, type_text,
380
+ )
381
+
382
+
383
+ @computer_driver(name="my-driver", supports=[{"os": {"type": "linux", "display": ["x11"]}, "arch": ["amd64"]}])
384
+ class MyDriver(ComputerDriverEnvironment):
385
+ buttons = ("left", "right") # features, on the card
386
+ max_click_count = 2
387
+
388
+ @display
389
+ async def size(self) -> Display: ... # width, height, scale
390
+ @capture(display="x11")
391
+ async def grab(self): ... # the whole display, native resolution
392
+ @click
393
+ async def click_at(self, x, y, button, count, modifiers): ...
394
+ @drag
395
+ async def drag_along(self, path, button, duration_ms, modifiers): ...
396
+ @scroll
397
+ async def scroll_at(self, x, y, direction, amount, modifiers): ...
398
+ @type_text
399
+ async def type_in(self, text): ...
400
+ @press
401
+ async def press_keys(self, keys): ...
402
+
403
+
404
+ if __name__ == "__main__":
405
+ MyDriver().serve()
406
+ ```
407
+
408
+ **Operations.** Operations get points in screen pixels, after the base class has mapped, checked
409
+ and queued the call, and may be sync or async. `@capture` returns a Pillow image or an
410
+ `Image(width, height, data, mode)` (PNG or JPEG bytes, or raw `RGB`, `RGBA`, `BGRA` or `BGRX`
411
+ pixels), at the display's size times its scale. Input operations may return an `Effect`.
412
+
413
+ | Decorator | Required | Gives |
414
+ |---|---|---|
415
+ | `@display`, `@capture`, `@click`, `@drag`, `@scroll`, `@type_text`, `@press` | yes | the seven `computer_*` tools and `computer/v1` |
416
+ | `@set_display` | no, with `display_modes` | `computer-display/v1` `set` (`get` is always served) |
417
+ | `@pointer_move`, `@pointer_down`, `@pointer_up`, `@pointer_position` | all or none | `computer-pointer/v1` and its five tools; click and scroll at the pointer |
418
+ | `@key_down`, `@key_up` | both or none | `computer-keyboard/v1` and its two tools |
419
+ | `@run_in_session` | no; a Linux default | `computer-exec/v1` |
420
+ | `@write_files` | no; a Linux default | `data/add` |
421
+
422
+ The class is checked when it is defined: a missing required operation, half a group, two methods
423
+ for one operation that some platform could both match, or features that don't match the
424
+ decorators raise `TypeError`. Predicates (`os`, `distribution`, `version`, `arch`, `display`)
425
+ choose a method per platform, read from `/etc/os-release`, the machine and `XDG_SESSION_TYPE` or
426
+ `DISPLAY`: `@capture(display="x11")` and `@capture(display="wayland")` can sit side by side. A
427
+ driver's own `@run_in_session` or `@write_files` replaces the base class's Linux default, which runs
428
+ as the driver's user inside the desktop session. The base class keeps its state in `self._computer`
429
+ and names its own members `_computer_*` and `computer_*`; a driver that redefines one of them, or
430
+ another private member of the base class, is refused when it is defined, so a helper of the
431
+ driver's can't silently replace one. What the optional operations give is served only when the
432
+ driver has them, through the `served_tools`, `served_extensions` and `served_operations` hooks.
433
+
434
+ **What the base class serves:**
435
+
436
+ - **Tools.** `computer_screenshot` (a frame: `frame_id`, image size, display resolution and scale;
437
+ `region` crops a frame at native resolution), `computer_click`, `computer_drag`,
438
+ `computer_scroll`, `computer_type`, `computer_key` (W3C key names) and `computer_wait`, plus the
439
+ group tools. Every result is structured, with a one-line summary as its text block; input results
440
+ carry `applied`, `delivery` and `others_since_frame`, and `screenshot: true` returns a fresh frame
441
+ after acting. Click and scroll leave out `x` and `y` to act at the pointer on a computer with
442
+ `computer-pointer/v1`; elsewhere a call without them is refused `invalid_argument`.
443
+ - **Frames and spaces.** Points are pixels of a named frame (your latest by default), or a
444
+ 0..`norm_scale` grid over it with `space: "norm"`. A point outside the frame or the range is
445
+ refused (`out_of_bounds`), never clamped. The last 64 frames are kept, and a frame goes stale
446
+ (`stale_frame`) when the display geometry it was captured at changes.
447
+ - **The input queue.** Input calls run one at a time in arrival order; one that waits past
448
+ `queue_wait_s` is refused `input_busy`. Screenshots and waits never queue.
449
+ - **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
450
+ capture, plus the driver's own `ready_checks()`; tools refuse `device_unavailable` until they
451
+ pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
452
+ descriptions), `computer-keyboard/v1` and `computer-exec/v1` (`run {argv, cwd?, env?, timeout_s?,
453
+ request_id}`: output capped per stream, a repeated `request_id` answered with the first result).
454
+ - **The data plane.** `data/add` takes file parts that name their path, relative to the session
455
+ user's home, under the `data-objects/v1` marker (`{"path": ...}`), as inline bytes, an https URL,
456
+ or a `transfers.ReadObject` read grant under the marker's `read`, and writes them atomically.
457
+ `data/get` returns the files under the settings' `data_paths`, inline or uploaded under a
458
+ `write_namespace` grant.
459
+ - **The card** (the name from `ENVIRONMENT_NAME`, then `@computer_driver`'s), and per-role tool
460
+ access (`tool_access` is on).
461
+
462
+ **Errors.** A driver raises the errors table's exceptions, all `AgentEnvError`s:
463
+ `InvalidArgument`, `OutOfBounds`, `StaleFrame`, `InputBusy`, `PermissionDenied`, `Unsupported`,
464
+ `BackgroundUnavailable`, `TargetUnavailable`, `DeviceUnavailable` and `LimitExceeded` mean nothing
465
+ was sent (`applied: "no"`); `Timeout`, `DeliveryFailed` and `Internal` mean it may have been. Any
466
+ other exception from an input operation is reported `delivery_failed` with `applied: "unknown"`.
467
+
468
+ **Settings** reach the driver as one JSON value in `AGENTENV_COMPUTER_SETTINGS`
469
+ (`ComputerSettings`): `data_paths`, `default_space`, `default_norm_scale`, `queue_wait_s`,
470
+ `tap_hold_ms`, `type_delay_ms`, `max_hold_ms`, `layout`, `drag_duration_ms`, `exec_output_bytes`
471
+ and `exec_timeout_s`. A driver reads them as `self.settings`, for example its tap hold in `@press`.
472
+
473
+ Every caller is `default` until agents carry an identity of their own; `caller_identity(context)`
474
+ names the caller whose latest frame and others' input a call uses.
475
+
303
476
  ## A2A agent framework
304
477
 
305
478
  The distribution exposes two unrelated decorators named `extension`:
@@ -90,10 +90,34 @@ step names it. Steps that name it show and hide it as they do any tool. Steps fo
90
90
  that role's earlier named steps, and a full reset (`role: "*", tools: "*"`) clears all of them, so
91
91
  grant a hidden tool after such steps. Trigger tool actions call it as usual.
92
92
 
93
+ `@tool(..., annotations={"readOnlyHint": True}, structured_output=True, meta={...})` lists the tool
94
+ with MCP `ToolAnnotations`, an output schema and `_meta`, on mcp's FastMCP and on fastmcp alike.
95
+ `structured_output` follows mcp's FastMCP (None derives an output schema from the return annotation
96
+ where it can, False lists none); a tool returning `Annotated[CallToolResult, Model]` sends its
97
+ result as is, image blocks included, and is listed with `Model`'s schema. An environment chooses
98
+ at mount which of its `@tool` methods it serves, and with which descriptions, by overriding
99
+ `served_tools(tools)`. After the mount, `env.mounted_card()` is the card the application serves, and
100
+ `await env.update_tool(name, description=...)` changes a served tool's description on it and in
101
+ `tools/list` alike, on both FastMCPs.
102
+
93
103
  `AgentEnvStarletteApplication` mounts the same handler onto a Starlette/FastAPI app instead of
94
104
  FastMCP; since those apps have no MCP tool registry, constructing one with `@tool` methods
95
105
  raises.
96
106
 
107
+ ### Errors
108
+
109
+ Raise `AgentEnvError(code, message, *, retryable=False, applied="no", hint=None, status=400)` to
110
+ answer a caller with a code rather than a crash. From an `@extension` method it answers with
111
+ `status` and the body `{"ok": false, "error": {code, message, retryable, applied, hint?}}`; from a
112
+ `@tool` it answers `isError` with `"<code>: <message>"` as the text and the same fields as the
113
+ structured content. `applied` says whether the call's effect happened (`yes`, `no`, or `unknown`
114
+ when it may have been delivered before something failed), and callers retry on their own only when
115
+ `retryable` is true and `applied` is `no`. Any other exception in an extension still answers
116
+ `extension_failed` with status 500, now with `applied: "unknown"`. On the client side,
117
+ `invoke_extension` raises `ExtensionCallError`, an `httpx.HTTPStatusError` carrying `code`,
118
+ `detail`, `retryable`, `applied` and `hint`; a body without those fields, such as a proxy's error
119
+ page, leaves `code` None and `applied` `unknown`.
120
+
97
121
  ### Who is calling
98
122
 
99
123
  A tool that needs the caller declares a parameter annotated `ToolContext` (any name) and receives
@@ -180,6 +204,40 @@ extension's, and `client.invoke_extension(base_url, card, uri, params, method=..
180
204
  its HTTP verb. Without `method`, `invoke_extension` calls the first method listed; an extension
181
205
  with several methods, such as a gateway's `urn:agentenv:clock/v1`, should always be called by name.
182
206
 
207
+ An extension with several methods is served by giving each handler an `op`. Handlers sharing a URI
208
+ merge into one card entry, each method under `params.methods.<op>` with its own verb and endpoint,
209
+ by default `/agentenv/ext/<extension>/<op>`, where `<extension>` is the URN's name without
210
+ `urn:agentenv:` and the version. Extension-level params, such as features and limits, come from the
211
+ environment's `extension_params(uri)`, merged beside `methods`. A declaration-only extension, a
212
+ promise with params and no handler like `intake/v1`, is returned from `declared_extensions()`.
213
+ Two handlers for one method, or a handler without `op` on a URI another handler also serves, are
214
+ refused at mount.
215
+
216
+ An environment chooses at mount which of its `@extension` handlers it serves by overriding
217
+ `served_extensions(extensions)`: an extension it leaves out is not served, and of one served with
218
+ `op`, only the methods its returned descriptor still lists under `params.methods`.
219
+ `served_operations(operations)` chooses the data-plane methods (`data/add` and the others it
220
+ marks) the same way. Either hook returning something with no handler is refused at mount.
221
+
222
+ ```python
223
+ @extension("urn:agentenv:computer-display/v1", op="get", method="GET")
224
+ async def display_get(self) -> dict: ...
225
+
226
+ @extension("urn:agentenv:computer-display/v1", op="set")
227
+ async def display_set(self, width: int, height: int) -> dict: ...
228
+ ```
229
+
230
+ ### Tool access without a gateway
231
+
232
+ An environment agents reach directly, with no gateway in front, can enforce per-role tool access
233
+ itself: set `tool_access = True` on the class. It then filters `tools/list` and refuses `tools/call`
234
+ (`permission_denied`) by the caller's role, read from `AgentEnv-Role` or a gateway-stamped `_meta`
235
+ role, and serves `urn:agentenv:disable-tool/v1` and `urn:agentenv:enable-tool/v1` at the gateway's
236
+ paths (`POST /tools/disable`, `POST /tools/enable`) with its bodies (`{role, tools: [...] | "*"}` to
237
+ `{role, disabled, allowed}`) and its rules, hidden-by-default tools included. So agent-env's
238
+ `modify_env_tool_access` step works on such an env unchanged. A call with no role is `default`, as
239
+ at the gateway.
240
+
183
241
  Where the binding lives depends on the app. On mcp's own FastMCP it wraps the tool manager's dispatch; on the standalone `fastmcp` package it joins the app's middleware chain as the outermost layer, so a handler's own middleware runs under it too. An app with neither seam mounts unbound, with a warning, unless a tool declares a `ToolContext` parameter or the handler defines `on_tool_call`: then the mount is refused rather than serving an empty caller.
184
242
 
185
243
  ### Uploading an export through a grant
@@ -254,6 +312,116 @@ async with ToolSession(mcp_url=mcp_url, role="alice@example.com") as session:
254
312
  - **Tools:** `list_tools()` returns the tools the role may use as `EnvironmentTool`, the type a card's
255
313
  `capabilities.tools` holds. Each `inputSchema` is the JSON Schema a model API takes as the tool's parameters.
256
314
 
315
+ ## Computer drivers
316
+
317
+ A computer driver is what runs inside a computer an agent uses through screenshots and input. Write
318
+ one as a `ComputerDriverEnvironment` subclass from `agentenv_protocol.computer`, which needs the
319
+ `computer` extra (`pip install 'agentenv-framework-protocol[computer]'`: mcp, uvicorn and Pillow).
320
+ The driver marks the methods that capture the screen and send input; the base class serves the
321
+ computer contract from them, so the tools, results and errors can't drift from it. A complete
322
+ driver for an X11 desktop, with an image to run it in, is in
323
+ [`examples/computer_x11/`](https://github.com/scaleapi/agentenv-framework/tree/main/packages/agentenv-protocol/examples/computer_x11).
324
+
325
+ ```python
326
+ from agentenv_protocol.computer import (
327
+ ComputerDriverEnvironment, Display, computer_driver,
328
+ capture, click, display, drag, press, scroll, type_text,
329
+ )
330
+
331
+
332
+ @computer_driver(name="my-driver", supports=[{"os": {"type": "linux", "display": ["x11"]}, "arch": ["amd64"]}])
333
+ class MyDriver(ComputerDriverEnvironment):
334
+ buttons = ("left", "right") # features, on the card
335
+ max_click_count = 2
336
+
337
+ @display
338
+ async def size(self) -> Display: ... # width, height, scale
339
+ @capture(display="x11")
340
+ async def grab(self): ... # the whole display, native resolution
341
+ @click
342
+ async def click_at(self, x, y, button, count, modifiers): ...
343
+ @drag
344
+ async def drag_along(self, path, button, duration_ms, modifiers): ...
345
+ @scroll
346
+ async def scroll_at(self, x, y, direction, amount, modifiers): ...
347
+ @type_text
348
+ async def type_in(self, text): ...
349
+ @press
350
+ async def press_keys(self, keys): ...
351
+
352
+
353
+ if __name__ == "__main__":
354
+ MyDriver().serve()
355
+ ```
356
+
357
+ **Operations.** Operations get points in screen pixels, after the base class has mapped, checked
358
+ and queued the call, and may be sync or async. `@capture` returns a Pillow image or an
359
+ `Image(width, height, data, mode)` (PNG or JPEG bytes, or raw `RGB`, `RGBA`, `BGRA` or `BGRX`
360
+ pixels), at the display's size times its scale. Input operations may return an `Effect`.
361
+
362
+ | Decorator | Required | Gives |
363
+ |---|---|---|
364
+ | `@display`, `@capture`, `@click`, `@drag`, `@scroll`, `@type_text`, `@press` | yes | the seven `computer_*` tools and `computer/v1` |
365
+ | `@set_display` | no, with `display_modes` | `computer-display/v1` `set` (`get` is always served) |
366
+ | `@pointer_move`, `@pointer_down`, `@pointer_up`, `@pointer_position` | all or none | `computer-pointer/v1` and its five tools; click and scroll at the pointer |
367
+ | `@key_down`, `@key_up` | both or none | `computer-keyboard/v1` and its two tools |
368
+ | `@run_in_session` | no; a Linux default | `computer-exec/v1` |
369
+ | `@write_files` | no; a Linux default | `data/add` |
370
+
371
+ The class is checked when it is defined: a missing required operation, half a group, two methods
372
+ for one operation that some platform could both match, or features that don't match the
373
+ decorators raise `TypeError`. Predicates (`os`, `distribution`, `version`, `arch`, `display`)
374
+ choose a method per platform, read from `/etc/os-release`, the machine and `XDG_SESSION_TYPE` or
375
+ `DISPLAY`: `@capture(display="x11")` and `@capture(display="wayland")` can sit side by side. A
376
+ driver's own `@run_in_session` or `@write_files` replaces the base class's Linux default, which runs
377
+ as the driver's user inside the desktop session. The base class keeps its state in `self._computer`
378
+ and names its own members `_computer_*` and `computer_*`; a driver that redefines one of them, or
379
+ another private member of the base class, is refused when it is defined, so a helper of the
380
+ driver's can't silently replace one. What the optional operations give is served only when the
381
+ driver has them, through the `served_tools`, `served_extensions` and `served_operations` hooks.
382
+
383
+ **What the base class serves:**
384
+
385
+ - **Tools.** `computer_screenshot` (a frame: `frame_id`, image size, display resolution and scale;
386
+ `region` crops a frame at native resolution), `computer_click`, `computer_drag`,
387
+ `computer_scroll`, `computer_type`, `computer_key` (W3C key names) and `computer_wait`, plus the
388
+ group tools. Every result is structured, with a one-line summary as its text block; input results
389
+ carry `applied`, `delivery` and `others_since_frame`, and `screenshot: true` returns a fresh frame
390
+ after acting. Click and scroll leave out `x` and `y` to act at the pointer on a computer with
391
+ `computer-pointer/v1`; elsewhere a call without them is refused `invalid_argument`.
392
+ - **Frames and spaces.** Points are pixels of a named frame (your latest by default), or a
393
+ 0..`norm_scale` grid over it with `space: "norm"`. A point outside the frame or the range is
394
+ refused (`out_of_bounds`), never clamped. The last 64 frames are kept, and a frame goes stale
395
+ (`stale_frame`) when the display geometry it was captured at changes.
396
+ - **The input queue.** Input calls run one at a time in arrival order; one that waits past
397
+ `queue_wait_s` is refused `input_busy`. Screenshots and waits never queue.
398
+ - **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
399
+ capture, plus the driver's own `ready_checks()`; tools refuse `device_unavailable` until they
400
+ pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
401
+ descriptions), `computer-keyboard/v1` and `computer-exec/v1` (`run {argv, cwd?, env?, timeout_s?,
402
+ request_id}`: output capped per stream, a repeated `request_id` answered with the first result).
403
+ - **The data plane.** `data/add` takes file parts that name their path, relative to the session
404
+ user's home, under the `data-objects/v1` marker (`{"path": ...}`), as inline bytes, an https URL,
405
+ or a `transfers.ReadObject` read grant under the marker's `read`, and writes them atomically.
406
+ `data/get` returns the files under the settings' `data_paths`, inline or uploaded under a
407
+ `write_namespace` grant.
408
+ - **The card** (the name from `ENVIRONMENT_NAME`, then `@computer_driver`'s), and per-role tool
409
+ access (`tool_access` is on).
410
+
411
+ **Errors.** A driver raises the errors table's exceptions, all `AgentEnvError`s:
412
+ `InvalidArgument`, `OutOfBounds`, `StaleFrame`, `InputBusy`, `PermissionDenied`, `Unsupported`,
413
+ `BackgroundUnavailable`, `TargetUnavailable`, `DeviceUnavailable` and `LimitExceeded` mean nothing
414
+ was sent (`applied: "no"`); `Timeout`, `DeliveryFailed` and `Internal` mean it may have been. Any
415
+ other exception from an input operation is reported `delivery_failed` with `applied: "unknown"`.
416
+
417
+ **Settings** reach the driver as one JSON value in `AGENTENV_COMPUTER_SETTINGS`
418
+ (`ComputerSettings`): `data_paths`, `default_space`, `default_norm_scale`, `queue_wait_s`,
419
+ `tap_hold_ms`, `type_delay_ms`, `max_hold_ms`, `layout`, `drag_duration_ms`, `exec_output_bytes`
420
+ and `exec_timeout_s`. A driver reads them as `self.settings`, for example its tap hold in `@press`.
421
+
422
+ Every caller is `default` until agents carry an identity of their own; `caller_identity(context)`
423
+ names the caller whose latest frame and others' input a call uses.
424
+
257
425
  ## A2A agent framework
258
426
 
259
427
  The distribution exposes two unrelated decorators named `extension`:
@@ -0,0 +1,19 @@
1
+ # Built from packages/agentenv-protocol, so the driver runs on this checkout's SDK:
2
+ # docker build -f examples/computer_x11/Dockerfile -t x11-desktop .
3
+ FROM public.ecr.aws/docker/library/ubuntu:24.04
4
+ ENV DEBIAN_FRONTEND=noninteractive
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ python3 python3-venv python3-tk xvfb x11-utils fonts-dejavu-core procps \
7
+ libxcb1 libxcb-render0 libxcb-shm0 libxcb-randr0 libxcb-xfixes0 libxcb-xinerama0 \
8
+ && rm -rf /var/lib/apt/lists/*
9
+ COPY pyproject.toml README.md LICENSE NOTICE THIRD_PARTY_NOTICES.md /opt/protocol/
10
+ COPY src /opt/protocol/src
11
+ RUN python3 -m venv /opt/driver \
12
+ && /opt/driver/bin/pip install --no-cache-dir "/opt/protocol[computer]" "mss>=9,<11" "python-xlib>=0.33,<0.34"
13
+ RUN useradd --create-home --uid 10001 user
14
+ COPY examples/computer_x11/start.sh examples/computer_x11/driver.py examples/computer_x11/fixture.py /opt/desktop/
15
+ USER user
16
+ WORKDIR /home/user
17
+ ENV DISPLAY=:99 XDG_SESSION_TYPE=x11
18
+ EXPOSE 18765
19
+ CMD ["/bin/sh", "/opt/desktop/start.sh"]
@@ -0,0 +1,60 @@
1
+ # An X11 computer driver
2
+
3
+ [`driver.py`](driver.py) is a complete computer driver for an X11 desktop, written on
4
+ `ComputerDriverEnvironment` from `agentenv_protocol.computer`: `mss` captures the screen and XTEST,
5
+ through `python-xlib`, sends the input. It implements the seven required operations and the pointer
6
+ and keyboard groups, declares five buttons, triple clicks and 50-point drags, and adds a ready check
7
+ of its own for XTEST. The base class serves everything else from those: the `computer_*` tools,
8
+ frames and coordinate spaces, the input queue, the framework extensions and the card, and its Linux
9
+ defaults give the driver `computer-exec/v1` and `data/add`.
10
+
11
+ The image runs the driver on Xvfb beside [`fixture.py`](fixture.py), the Fixture Desk: a Tk window
12
+ with a name field, a checkbox, a 100-item list, a grid of cells, a drawing pad and a Save button. It
13
+ records whatever reaches it in `~/fixture/state.json`, so what an agent did can be read back from
14
+ the app rather than from the driver's answers, and shows the text of `~/inbox/message.txt`, which
15
+ `data/add` can write. [`start.sh`](start.sh) restarts Xvfb and the app if either dies.
16
+
17
+ ## Run it
18
+
19
+ ```bash
20
+ cd packages/agentenv-protocol
21
+ docker build -f examples/computer_x11/Dockerfile -t x11-desktop .
22
+ docker run --rm -p 18765:18765 x11-desktop
23
+ ```
24
+
25
+ The card is at `http://localhost:18765/.well-known/agent-env.json`, readiness at
26
+ `/agentenv/ext/computer-ready/status`, and the tools at the MCP endpoint `/mcp`:
27
+
28
+ ```python
29
+ import asyncio
30
+
31
+ from mcp import ClientSession
32
+ from mcp.client.streamable_http import streamablehttp_client
33
+
34
+
35
+ async def main() -> None:
36
+ async with streamablehttp_client("http://localhost:18765/mcp") as (read, write, _):
37
+ async with ClientSession(read, write) as session:
38
+ await session.initialize()
39
+ shot = await session.call_tool("computer_screenshot", {})
40
+ print(shot.structuredContent["summary"])
41
+ await session.call_tool("computer_click", {"x": 300, "y": 52})
42
+ typed = await session.call_tool("computer_type", {"text": "Ada Lovelace", "screenshot": True})
43
+ print(typed.structuredContent["summary"])
44
+
45
+
46
+ asyncio.run(main())
47
+ ```
48
+
49
+ `AGENTENV_COMPUTER_SETTINGS` takes the driver's settings as one JSON value, for example
50
+ `-e AGENTENV_COMPUTER_SETTINGS='{"default_space": "norm", "data_paths": ["fixture"]}'`.
51
+
52
+ ## In agent-env
53
+
54
+ As a bundle env (`envs/<name>/Dockerfile`), the image is built from the env's own folder, so copy
55
+ the three files there and install `agentenv-framework-protocol[computer]` in place of this checkout.
56
+ Name the env with `environment_name` in its `env.toml`. A task then deploys it with `deploy_env`
57
+ and gives it to an agent with `deploy_agent`, as for any MCP env.
58
+
59
+ `tests/test_computer_x11_example.py` runs the driver against fake `mss` and `Xlib` modules, so it
60
+ keeps up with the SDK without an X server.