agentenv-framework-protocol 0.1.403__tar.gz → 0.1.404__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 (109) hide show
  1. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/PKG-INFO +72 -5
  2. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/README.md +71 -4
  3. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/pyproject.toml +1 -1
  4. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/__init__.py +6 -0
  5. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/agent_env_environment.py +206 -7
  6. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/__init__.py +2 -0
  7. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/driver.py +5 -0
  8. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/extensions.py +1 -1
  9. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/keys.py +2 -0
  10. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/skill.py +30 -7
  11. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_agent_env_environment.py +13 -0
  12. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_driver.py +18 -0
  13. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_tool_access.py +25 -3
  14. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_tool_annotations.py +155 -1
  15. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/.gitignore +0 -0
  16. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/LICENSE +0 -0
  17. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/NOTICE +0 -0
  18. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/THIRD_PARTY_NOTICES.md +0 -0
  19. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/__init__.py +0 -0
  20. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/advanced_agent.py +0 -0
  21. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/basic_agent.py +0 -0
  22. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/custom_extensions_agent.py +0 -0
  23. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/multimodal_agent.py +0 -0
  24. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/reference_model.py +0 -0
  25. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/streaming_agent.py +0 -0
  26. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
  27. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
  28. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
  29. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
  30. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
  31. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
  32. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
  33. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
  34. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
  35. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
  36. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
  37. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/client.py +0 -0
  38. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/_declare.py +0 -0
  39. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/_load.py +0 -0
  40. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/app.py +0 -0
  41. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/app_runtime.py +0 -0
  42. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/apps.py +0 -0
  43. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/card.py +0 -0
  44. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/checks.py +0 -0
  45. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
  46. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
  47. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
  48. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/client.py +0 -0
  49. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
  50. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
  51. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/report.py +0 -0
  52. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/dataplane.py +0 -0
  53. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/errors.py +0 -0
  54. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/frames.py +0 -0
  55. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/launch.py +0 -0
  56. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/linux.py +0 -0
  57. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/manifest.py +0 -0
  58. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/operations.py +0 -0
  59. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/platform.py +0 -0
  60. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/queue.py +0 -0
  61. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/recording.py +0 -0
  62. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/results.py +0 -0
  63. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/runtime.py +0 -0
  64. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/serve.py +0 -0
  65. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/serve_app.py +0 -0
  66. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/session.py +0 -0
  67. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/settings.py +0 -0
  68. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/spaces.py +0 -0
  69. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/tools.py +0 -0
  70. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/errors.py +0 -0
  71. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/manifest.py +0 -0
  72. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/preflight.py +0 -0
  73. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/tool_access.py +0 -0
  74. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/tool_client.py +0 -0
  75. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/tool_context.py +0 -0
  76. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/transfers.py +0 -0
  77. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/types.py +0 -0
  78. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/computer_fake.py +0 -0
  79. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/probe_fake.py +0 -0
  80. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_agent.py +0 -0
  81. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_agent_examples.py +0 -0
  82. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_object_transfers.py +0 -0
  83. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_peer_agents.py +0 -0
  84. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_proxy.py +0 -0
  85. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_staging.py +0 -0
  86. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_trajectory_log.py +0 -0
  87. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_client.py +0 -0
  88. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_app_server.py +0 -0
  89. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_conformance.py +0 -0
  90. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_conformance_checks.py +0 -0
  91. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_conformance_recording.py +0 -0
  92. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_dataplane.py +0 -0
  93. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_declare.py +0 -0
  94. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_extensions.py +0 -0
  95. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_frames.py +0 -0
  96. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_queue.py +0 -0
  97. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_recording.py +0 -0
  98. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_serve.py +0 -0
  99. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_spaces.py +0 -0
  100. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_data_objects.py +0 -0
  101. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_errors.py +0 -0
  102. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_extension_methods.py +0 -0
  103. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_intake.py +0 -0
  104. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_manifest.py +0 -0
  105. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_preflight.py +0 -0
  106. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_tool_client.py +0 -0
  107. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_tool_context.py +0 -0
  108. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_tool_context_fastmcp.py +0 -0
  109. {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/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.403
3
+ Version: 0.1.404
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
@@ -296,6 +296,69 @@ at the gateway.
296
296
 
297
297
  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.
298
298
 
299
+ ### Tools served on another server's behalf
300
+
301
+ An environment can serve tools another server defines, each with that server's own schema:
302
+ `forwarded_tools()` returns `ForwardedTool(tool, call, annotations, output_schema)` entries, where
303
+ `tool` is an `EnvironmentTool` carrying the other server's `inputSchema`, and `call` takes the
304
+ call's arguments as keywords, untouched, and returns that server's `CallToolResult`, which reaches
305
+ the caller as it is. An `output_schema` is listed as the tool's `outputSchema` and isn't checked
306
+ here, so `call` answers every result, its errors too, in a shape that schema admits. They're listed
307
+ on the card beside the `@tool` methods, chosen by `served_tools` and under tool access like them;
308
+ one named like another tool is refused. They need mcp's FastMCP. The cua-driver driver serves
309
+ cua-driver's own tools this way.
310
+
311
+ ### Resources
312
+
313
+ Mark a method that serves MCP resources with `@resource`; an env may mark several, and each read
314
+ calls the method, which returns text or bytes. The URI decides what's served:
315
+
316
+ ```python
317
+ @environment_card(name="orders")
318
+ class Orders(AgentEnvEnvironment):
319
+ @resource("docs://orders/refund-policy", mime_type="text/markdown")
320
+ def refund_policy(self) -> str:
321
+ """When a refund is allowed and who approves it."""
322
+ return POLICY.read_text()
323
+
324
+ @resource("orders://{order_id}")
325
+ async def order(self, order_id: str) -> str:
326
+ return json.dumps(await self.db.order(order_id))
327
+
328
+ @resource("skill://upstream/{file}", listed=lambda env: UPSTREAM["resources"])
329
+ async def upstream_file(self, file: str) -> str:
330
+ return await read_upstream(f"skill://upstream/{file}")
331
+ ```
332
+
333
+ - A fixed URI is one resource. Its name defaults to the method's name and its description to the
334
+ docstring.
335
+ - A template such as `orders://{order_id}` is an MCP resource template; the method takes its parts.
336
+ - A template with `listed` serves the resources `listed` returns, each as MCP lists one (`uri`,
337
+ `name`, `description`, `mimeType`), and only those. Each is read through the method with its
338
+ URI's parts. That's how an env serves another server's resources from a list: the cua-driver
339
+ driver serves cua-driver's skill files this way.
340
+
341
+ A URI served twice, a listed URI that doesn't match its template, and a method that doesn't take
342
+ exactly the template's parts are refused. Resources need mcp's FastMCP.
343
+
344
+ ### Instructions at connect
345
+
346
+ An MCP server may answer `initialize` with `instructions`: how to use it, which a client may put in
347
+ the agent's context, as Claude Code does. Mark the plain method that returns them with
348
+ `@server_instructions`:
349
+
350
+ ```python
351
+ @environment_card(name="orders")
352
+ class Orders(AgentEnvEnvironment):
353
+ @server_instructions
354
+ def how_to(self) -> str:
355
+ return "Look an order up with get_order before refunding it; refunds can't be undone."
356
+ ```
357
+
358
+ They're read once, when the SDK builds the app (`create_app`, `serve`, `create_fastmcp_app`), and
359
+ every session gets the same text. An env has at most one such method. An app you build yourself
360
+ and pass to `mount` keeps its own instructions: set them when you construct it.
361
+
299
362
  ### Getting part of the data
300
363
 
301
364
  A `@get_data` handler that takes a `paths` parameter returns only the data under the paths a caller
@@ -318,9 +381,11 @@ def guide(self):
318
381
  The card then declares `urn:agentenv:skill/v1` (`SKILL_EXTENSION_URI`), whose `get {name}`
319
382
  answers `{name, description, skill_md}`, the shape `skill-config/v1` takes, with the frontmatter
320
383
  written under the name the caller gives; a name that isn't lowercase letters, digits and single
321
- hyphens, up to 64, is refused `invalid_argument`. agent-env's `add_skills` step installs it on
322
- an agent through `env_ids`, named after the env's environment name; for an env behind a gateway,
323
- whose card nests its servers' cards, it installs each server's skill under that server's name.
384
+ hyphens, up to 64, is refused `invalid_argument`. A skill with other files beside its SKILL.md
385
+ returns them as a third value, `{relative path: text}`, and `get` answers them as `files`.
386
+ agent-env's `add_skills` step installs it on an agent through `env_ids`, named after the env's
387
+ environment name, a skill with files as a bundle of them; for an env behind a gateway, whose card
388
+ nests its servers' cards, it installs each server's skill under that server's name.
324
389
 
325
390
  ### Uploading an export through a grant
326
391
 
@@ -482,7 +547,9 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
482
547
  features. The driver serves its skill through `skill/v1` (an `@agent_skill` handler on the base
483
548
  class): a SKILL.md written from the computer as it booted (its platform, session user and home,
484
549
  the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
485
- class's `notes`, markdown for what its card can't say.
550
+ class's `notes`, markdown for what its card can't say. A driver whose software ships a skill of
551
+ its own returns it from `bundled_skill()` (its description, body and other files), and the
552
+ computer skill leads with it, the computer's part following.
486
553
  - **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
487
554
  capture, the driver's own `ready_checks()`, and one per app on the computer, asked of the app's
488
555
  own server (see Computer apps); tools refuse `device_unavailable` until they pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
@@ -240,6 +240,69 @@ at the gateway.
240
240
 
241
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.
242
242
 
243
+ ### Tools served on another server's behalf
244
+
245
+ An environment can serve tools another server defines, each with that server's own schema:
246
+ `forwarded_tools()` returns `ForwardedTool(tool, call, annotations, output_schema)` entries, where
247
+ `tool` is an `EnvironmentTool` carrying the other server's `inputSchema`, and `call` takes the
248
+ call's arguments as keywords, untouched, and returns that server's `CallToolResult`, which reaches
249
+ the caller as it is. An `output_schema` is listed as the tool's `outputSchema` and isn't checked
250
+ here, so `call` answers every result, its errors too, in a shape that schema admits. They're listed
251
+ on the card beside the `@tool` methods, chosen by `served_tools` and under tool access like them;
252
+ one named like another tool is refused. They need mcp's FastMCP. The cua-driver driver serves
253
+ cua-driver's own tools this way.
254
+
255
+ ### Resources
256
+
257
+ Mark a method that serves MCP resources with `@resource`; an env may mark several, and each read
258
+ calls the method, which returns text or bytes. The URI decides what's served:
259
+
260
+ ```python
261
+ @environment_card(name="orders")
262
+ class Orders(AgentEnvEnvironment):
263
+ @resource("docs://orders/refund-policy", mime_type="text/markdown")
264
+ def refund_policy(self) -> str:
265
+ """When a refund is allowed and who approves it."""
266
+ return POLICY.read_text()
267
+
268
+ @resource("orders://{order_id}")
269
+ async def order(self, order_id: str) -> str:
270
+ return json.dumps(await self.db.order(order_id))
271
+
272
+ @resource("skill://upstream/{file}", listed=lambda env: UPSTREAM["resources"])
273
+ async def upstream_file(self, file: str) -> str:
274
+ return await read_upstream(f"skill://upstream/{file}")
275
+ ```
276
+
277
+ - A fixed URI is one resource. Its name defaults to the method's name and its description to the
278
+ docstring.
279
+ - A template such as `orders://{order_id}` is an MCP resource template; the method takes its parts.
280
+ - A template with `listed` serves the resources `listed` returns, each as MCP lists one (`uri`,
281
+ `name`, `description`, `mimeType`), and only those. Each is read through the method with its
282
+ URI's parts. That's how an env serves another server's resources from a list: the cua-driver
283
+ driver serves cua-driver's skill files this way.
284
+
285
+ A URI served twice, a listed URI that doesn't match its template, and a method that doesn't take
286
+ exactly the template's parts are refused. Resources need mcp's FastMCP.
287
+
288
+ ### Instructions at connect
289
+
290
+ An MCP server may answer `initialize` with `instructions`: how to use it, which a client may put in
291
+ the agent's context, as Claude Code does. Mark the plain method that returns them with
292
+ `@server_instructions`:
293
+
294
+ ```python
295
+ @environment_card(name="orders")
296
+ class Orders(AgentEnvEnvironment):
297
+ @server_instructions
298
+ def how_to(self) -> str:
299
+ return "Look an order up with get_order before refunding it; refunds can't be undone."
300
+ ```
301
+
302
+ They're read once, when the SDK builds the app (`create_app`, `serve`, `create_fastmcp_app`), and
303
+ every session gets the same text. An env has at most one such method. An app you build yourself
304
+ and pass to `mount` keeps its own instructions: set them when you construct it.
305
+
243
306
  ### Getting part of the data
244
307
 
245
308
  A `@get_data` handler that takes a `paths` parameter returns only the data under the paths a caller
@@ -262,9 +325,11 @@ def guide(self):
262
325
  The card then declares `urn:agentenv:skill/v1` (`SKILL_EXTENSION_URI`), whose `get {name}`
263
326
  answers `{name, description, skill_md}`, the shape `skill-config/v1` takes, with the frontmatter
264
327
  written under the name the caller gives; a name that isn't lowercase letters, digits and single
265
- hyphens, up to 64, is refused `invalid_argument`. agent-env's `add_skills` step installs it on
266
- an agent through `env_ids`, named after the env's environment name; for an env behind a gateway,
267
- whose card nests its servers' cards, it installs each server's skill under that server's name.
328
+ hyphens, up to 64, is refused `invalid_argument`. A skill with other files beside its SKILL.md
329
+ returns them as a third value, `{relative path: text}`, and `get` answers them as `files`.
330
+ agent-env's `add_skills` step installs it on an agent through `env_ids`, named after the env's
331
+ environment name, a skill with files as a bundle of them; for an env behind a gateway, whose card
332
+ nests its servers' cards, it installs each server's skill under that server's name.
268
333
 
269
334
  ### Uploading an export through a grant
270
335
 
@@ -426,7 +491,9 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
426
491
  features. The driver serves its skill through `skill/v1` (an `@agent_skill` handler on the base
427
492
  class): a SKILL.md written from the computer as it booted (its platform, session user and home,
428
493
  the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
429
- class's `notes`, markdown for what its card can't say.
494
+ class's `notes`, markdown for what its card can't say. A driver whose software ships a skill of
495
+ its own returns it from `bundled_skill()` (its description, body and other files), and the
496
+ computer skill leads with it, the computer's part following.
430
497
  - **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
431
498
  capture, the driver's own `ready_checks()`, and one per app on the computer, asked of the app's
432
499
  own server (see Computer apps); tools refuse `device_unavailable` until they pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentenv-framework-protocol"
3
- version = "0.1.403"
3
+ version = "0.1.404"
4
4
  description = "Open data-plane protocol and server SDK for agent environments."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -11,8 +11,11 @@ from .agent_env_environment import (
11
11
  AgentEnvEnvironment,
12
12
  AgentEnvFastMCPApplication,
13
13
  AgentEnvStarletteApplication,
14
+ ForwardedTool,
14
15
  add_data,
15
16
  agent_skill,
17
+ resource,
18
+ server_instructions,
16
19
  create_fastmcp_app,
17
20
  environment_card,
18
21
  extension,
@@ -88,12 +91,15 @@ __all__ = [
88
91
  "AgentEnvEnvironment",
89
92
  "AgentEnvFastMCPApplication",
90
93
  "AgentEnvStarletteApplication",
94
+ "ForwardedTool",
91
95
  "create_fastmcp_app",
92
96
  "environment_card",
93
97
  "reset_data",
94
98
  "add_data",
95
99
  "get_data",
96
100
  "agent_skill",
101
+ "resource",
102
+ "server_instructions",
97
103
  "extension",
98
104
  "tool",
99
105
  "injecting",
@@ -38,6 +38,9 @@ _EXT_OP_ATTR = "_aee_ext_op"
38
38
  _TOOL_ATTR = "_aee_tool"
39
39
  _HIDDEN_BY_DEFAULT_ATTR = "_aee_hidden_by_default"
40
40
  _TOOL_OPTIONS_ATTR = "_aee_tool_options"
41
+ _FORWARDED_ATTR = "_aee_forwarded"
42
+ _INSTRUCTIONS_ATTR = "_aee_instructions"
43
+ _RESOURCE_ATTR = "_aee_resource"
41
44
  _CARD_CONFIG_ATTR = "_aee_card_config"
42
45
  #: Set on the tool dispatch installed by ``_bind_tool_calls``; a guard downstream checks it at boot.
43
46
  _TOOL_CONTEXT_BOUND_ATTR = "__agentenv_tool_context__"
@@ -128,21 +131,56 @@ def extension(uri: str, *, op: str | None = None, description: str | None = None
128
131
  return deco
129
132
 
130
133
 
134
+ def resource(uri: str, *, name: str | None = None, description: str | None = None, mime_type: str = "text/plain",
135
+ listed: Callable[[Any], list[dict]] | None = None) -> Callable:
136
+ """Mark a method that serves MCP resources, returning their text or bytes; an env may mark several, and each read
137
+ calls the method. A fixed ``uri`` is one resource. A template such as ``orders://{order_id}`` is an MCP resource
138
+ template, the method taking its parts, unless ``listed`` is given: it takes the env and returns the resources to
139
+ list, each as MCP lists one (``uri``, ``name``, ``description``, ``mimeType``), each read through the method with
140
+ its URI's parts. ``name`` defaults to the method's name and ``description`` to its docstring."""
141
+ parts = set(re.findall(r"{(\w+)}", uri))
142
+
143
+ def mark(fn: Callable) -> Callable:
144
+ params = set(list(inspect.signature(fn).parameters)[1:])
145
+ if params != parts:
146
+ raise ValueError(f"@resource {uri!r} needs a method taking {sorted(parts) or 'nothing'} besides self, "
147
+ f"not {sorted(params)}")
148
+ if listed is not None and not parts:
149
+ raise ValueError(f"@resource {uri!r} lists resources only from a template URI")
150
+ setattr(fn, _RESOURCE_ATTR, _ResourceMarker(uri, name, description, mime_type, listed))
151
+ return fn
152
+
153
+ return mark
154
+
155
+
156
+ def server_instructions(fn: Callable) -> Callable:
157
+ """Mark the plain method that returns what this env's MCP server tells an agent when it connects, the
158
+ ``instructions`` of its initialize result, which clients may put in the agent's context. It's read once, when the
159
+ SDK builds the app (``create_app``, ``serve``, ``create_fastmcp_app``); an app passed to ``mount`` keeps its own."""
160
+ if inspect.iscoroutinefunction(fn):
161
+ raise TypeError(f"@server_instructions marks a plain method, not the coroutine {fn.__name__!r}")
162
+ setattr(fn, _INSTRUCTIONS_ATTR, True)
163
+ return fn
164
+
165
+
131
166
  def agent_skill(fn: Callable) -> Callable:
132
- """Mark the method that writes this env's skill for agents, returning its description and its markdown body; it
133
- is served under ``SKILL_EXTENSION_URI`` as ``get {name}``, which writes the SKILL.md under the name asked for."""
167
+ """Mark the method that writes this env's skill for agents, returning its description, its markdown body and,
168
+ optionally, its other files by relative path; it is served under ``SKILL_EXTENSION_URI`` as ``get {name}``, which
169
+ writes the SKILL.md under the name asked for and returns the files beside it."""
134
170
 
135
171
  async def get(self, name: str) -> dict:
136
172
  if len(name) > 64 or not _SKILL_NAME.fullmatch(name):
137
173
  raise AgentEnvError("invalid_argument",
138
174
  f"a skill's name is lowercase letters, digits and single hyphens, up to 64, not {name!r}")
139
- description, body = await fn(self) if inspect.iscoroutinefunction(fn) else fn(self)
175
+ description, body, *files = await fn(self) if inspect.iscoroutinefunction(fn) else fn(self)
140
176
  frontmatter = f"---\nname: {name}\ndescription: {json.dumps(description)}\n---\n"
141
- return {"name": name, "description": description, "skill_md": f"{frontmatter}\n{body}"}
177
+ return {"name": name, "description": description, "skill_md": f"{frontmatter}\n{body}",
178
+ **({"files": files[0]} if files and files[0] else {})}
142
179
 
143
180
  get.__name__ = fn.__name__
144
181
  return extension(SKILL_EXTENSION_URI, op="get", method="GET",
145
- description="get returns a SKILL.md for agents under the name it's given.")(get)
182
+ description="get returns a SKILL.md for agents under the name it's given, and the skill's other "
183
+ "files when it has any.")(get)
146
184
 
147
185
 
148
186
  def _extension_slug(uri: str) -> str:
@@ -202,6 +240,36 @@ def tool(name: str | None = None, *, description: str | None = None, hidden_by_d
202
240
  return deco
203
241
 
204
242
 
243
+ class ForwardedTool(NamedTuple):
244
+ """A tool an environment serves on another server's behalf, as that server lists it: its descriptor, its own
245
+ schema included, its MCP ``annotations`` and its ``output_schema``; ``call`` takes the call's arguments as keywords
246
+ and returns the other server's ``CallToolResult``, which reaches the caller as it is."""
247
+
248
+ tool: EnvironmentTool
249
+ call: Callable[..., Any]
250
+ annotations: dict | None = None
251
+ output_schema: dict | None = None
252
+
253
+
254
+ class _ResourceMarker(NamedTuple):
255
+ uri: str
256
+ name: str | None
257
+ description: str | None
258
+ mime_type: str
259
+ listed: Callable[[Any], list[dict]] | None
260
+
261
+
262
+ class _ServedResource(NamedTuple):
263
+ """One resource, or a resource template when ``template``, as it is registered: ``read`` returns its content."""
264
+
265
+ uri: str
266
+ name: str
267
+ description: str | None
268
+ mime_type: str
269
+ read: Callable
270
+ template: bool
271
+
272
+
205
273
  class _ToolOptions(NamedTuple):
206
274
  annotations: dict | None
207
275
  structured_output: bool | None
@@ -239,9 +307,13 @@ class AgentEnvApplication(ABC):
239
307
  self._app: Any = None
240
308
  ext_handlers = _served_extensions(handler, _discover_extensions(handler))
241
309
  environment_card = _merge_extensions(environment_card, ext_handlers + _declared_extensions(handler, ext_handlers))
242
- tool_handlers = _served_tools(handler, _discover_tools(handler, environment_card.name))
310
+ tool_handlers = _served_tools(handler, _with_forwarded(_discover_tools(handler, environment_card.name), handler))
243
311
  if tool_handlers and not self.supports_tools:
244
312
  raise ValueError(f"@tool methods require a FastMCP-backed application; {type(self).__name__} cannot serve MCP tools")
313
+ self._resources = _discover_resources(handler)
314
+ if self._resources and not self.supports_tools:
315
+ raise ValueError(f"@resource methods require a FastMCP-backed application; {type(self).__name__} cannot "
316
+ "serve MCP resources")
245
317
  environment_card = _merge_tools(environment_card, tool_handlers)
246
318
  self._tool_access = (
247
319
  ToolAccess(descriptor.name for descriptor, fn in tool_handlers if getattr(fn, _HIDDEN_BY_DEFAULT_ATTR, False))
@@ -276,6 +348,8 @@ class AgentEnvApplication(ABC):
276
348
  self._add_route(app, path, [method], handler)
277
349
  for descriptor, fn in self._tools:
278
350
  self._register_tool(app, descriptor, fn)
351
+ for resource in self._resources:
352
+ self._register_resource(app, resource)
279
353
  # Last, so it is the outermost layer: a dispatch guard the handler installed before mounting
280
354
  # then runs under the bound ToolContext instead of outside it.
281
355
  self._bind_tool_calls(app)
@@ -287,6 +361,9 @@ class AgentEnvApplication(ABC):
287
361
  def _register_tool(self, app: Any, descriptor: EnvironmentTool, fn: Callable) -> None:
288
362
  raise NotImplementedError(f"{type(self).__name__} does not serve MCP tools")
289
363
 
364
+ def _register_resource(self, app: Any, resource: _ServedResource) -> None:
365
+ raise NotImplementedError(f"{type(self).__name__} does not serve MCP resources")
366
+
290
367
  def _check_tool_binding(self, app: Any) -> None:
291
368
  """Everything that can refuse the mount, before the app is touched; nothing to refuse without a tool dispatch."""
292
369
  return None
@@ -329,6 +406,8 @@ class AgentEnvFastMCPApplication(AgentEnvApplication):
329
406
  app.custom_route(path, methods=methods)(handler)
330
407
 
331
408
  def _register_tool(self, app: Any, descriptor: EnvironmentTool, fn: Callable) -> None:
409
+ if (forwarded := getattr(fn, _FORWARDED_ATTR, None)) is not None:
410
+ return self._register_forwarded(app, descriptor, fn, forwarded)
332
411
  # passed only when set, so a FastMCP-shaped target without the parameters still mounts plain tools
333
412
  options = getattr(fn, _TOOL_OPTIONS_ATTR, None) or _ToolOptions(None, None, None)
334
413
  fastmcp = self._binding == "middleware"
@@ -348,6 +427,38 @@ class AgentEnvFastMCPApplication(AgentEnvApplication):
348
427
  extra["output_schema"] = None if options.structured_output is False else schema
349
428
  app.tool(name=descriptor.name, description=descriptor.description, **extra)(registered)
350
429
 
430
+ def _register_forwarded(self, app: Any, descriptor: EnvironmentTool, fn: Callable, forwarded: ForwardedTool) -> None:
431
+ """Put a forwarded tool into mcp's FastMCP tool manager as it is, with its own schema and its arguments passed
432
+ through untouched, since FastMCP's ``tool`` builds a schema from a signature."""
433
+ from mcp.server.fastmcp.tools.base import Tool
434
+ from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata
435
+
436
+ if self._binding != "dispatch":
437
+ raise RuntimeError(f"the forwarded tool {descriptor.name!r} needs mcp's FastMCP, whose tool manager takes a "
438
+ "tool with its own schema")
439
+ schema = forwarded.output_schema
440
+ app._tool_manager._tools[descriptor.name] = Tool(
441
+ fn=fn, name=descriptor.name, description=descriptor.description or "",
442
+ parameters=descriptor.inputSchema or {"type": "object"},
443
+ fn_metadata=FuncMetadata(arg_model=_passed_through(), output_schema=schema,
444
+ output_model=_as_it_came() if schema is not None else None),
445
+ is_async=True,
446
+ annotations=_mcp_tool_annotations(forwarded.annotations) if forwarded.annotations is not None else None)
447
+
448
+ def _register_resource(self, app: Any, resource: _ServedResource) -> None:
449
+ """A resource as it is, its description absent when it has none, or a template through FastMCP's own
450
+ decorator."""
451
+ from mcp.server.fastmcp.resources import FunctionResource
452
+
453
+ if self._binding != "dispatch":
454
+ raise RuntimeError(f"the resource {resource.uri!r} needs mcp's FastMCP")
455
+ if resource.template:
456
+ app.resource(resource.uri, name=resource.name, description=resource.description,
457
+ mime_type=resource.mime_type)(resource.read)
458
+ return
459
+ app.add_resource(FunctionResource(uri=resource.uri, name=resource.name, description=resource.description,
460
+ mime_type=resource.mime_type, fn=resource.read))
461
+
351
462
  def _check_tool_binding(self, app: Any) -> None:
352
463
  """Decide, before any route or tool is added, how every tools/call on ``app`` gets its ToolContext, and
353
464
  refuse what cannot be honoured. The standalone ``fastmcp`` package (a ``middleware`` list with
@@ -480,7 +591,7 @@ def create_fastmcp_app(handler: Any, *, card: EnvironmentCard, **fastmcp_kwargs:
480
591
  from mcp.server.fastmcp import FastMCP
481
592
  except ImportError as e:
482
593
  raise ImportError("create_fastmcp_app requires the 'mcp' package: pip install 'mcp>=1.25,<2'") from e
483
- app = FastMCP(card.name, **fastmcp_kwargs)
594
+ app = FastMCP(card.name, **{"instructions": _instructions(handler), **fastmcp_kwargs})
484
595
  app.settings.host = os.environ.get("MCP_HOST", "0.0.0.0")
485
596
  app.settings.port = int(os.environ.get("MCP_PORT", "18765"))
486
597
  app.settings.transport_security.enable_dns_rebinding_protection = False
@@ -564,6 +675,11 @@ class AgentEnvEnvironment:
564
675
  """The ``@tool`` methods to serve, chosen at mount; a returned descriptor's description replaces the marked one."""
565
676
  return tools
566
677
 
678
+ def forwarded_tools(self) -> list[ForwardedTool]:
679
+ """Tools served on another server's behalf, each with its own schema, beside the ``@tool`` methods: listed on
680
+ the card, chosen by ``served_tools`` and under tool access like them. Needs mcp's FastMCP."""
681
+ return []
682
+
567
683
  def on_tool_call(self, context: ToolContext) -> Any:
568
684
  """Runs before every tools/call on this environment's app, under the bound ``context``.
569
685
 
@@ -932,6 +1048,47 @@ def _marked_members(handler: Any, marker_attr: str):
932
1048
  yield attr, marker
933
1049
 
934
1050
 
1051
+ def _discover_resources(handler: Any) -> list[_ServedResource]:
1052
+ """The resources and resource templates the handler's @resource methods serve; a URI served twice is refused."""
1053
+ served: list[_ServedResource] = []
1054
+ for method, marker in _marked_members(handler, _RESOURCE_ATTR):
1055
+ name, description = marker.name or method.__name__, marker.description or inspect.getdoc(method)
1056
+ if marker.listed is None:
1057
+ served.append(_ServedResource(marker.uri, name, description, marker.mime_type, method, "{" in marker.uri))
1058
+ continue
1059
+ for entry in marker.listed(handler):
1060
+ parts = _template_parts(marker.uri, entry["uri"])
1061
+ if parts is None:
1062
+ raise ValueError(f"the listed resource {entry['uri']!r} doesn't match {marker.uri!r}")
1063
+ served.append(_ServedResource(entry["uri"], entry["name"], entry.get("description"),
1064
+ entry.get("mimeType") or marker.mime_type, _with_parts(method, parts), False))
1065
+ uris = [resource.uri for resource in served]
1066
+ if duplicated := sorted({uri for uri in uris if uris.count(uri) > 1}):
1067
+ raise ValueError(f"resources served twice: {duplicated}")
1068
+ return served
1069
+
1070
+
1071
+ def _template_parts(template: str, uri: str) -> dict[str, str] | None:
1072
+ """The parts of ``uri`` that ``template``'s ``{name}`` slots match, each within one path segment."""
1073
+ match = re.fullmatch(re.sub(r"\\\{(\w+)\\\}", r"(?P<\1>[^/]+)", re.escape(template)), uri)
1074
+ return match.groupdict() if match else None
1075
+
1076
+
1077
+ def _with_parts(method: Callable, parts: dict[str, str]) -> Callable:
1078
+ def read() -> Any:
1079
+ return method(**parts)
1080
+
1081
+ return read
1082
+
1083
+
1084
+ def _instructions(handler: Any) -> str | None:
1085
+ """What the handler's @server_instructions method returns; None without one."""
1086
+ marked = [method for method, _marker in _marked_members(handler, _INSTRUCTIONS_ATTR)]
1087
+ if len(marked) > 1:
1088
+ raise ValueError(f"an environment has one @server_instructions method, not {sorted(m.__name__ for m in marked)}")
1089
+ return marked[0]() if marked else None
1090
+
1091
+
935
1092
  def _discover_methods(handler: Any) -> dict:
936
1093
  found: dict[str, Callable] = {}
937
1094
  for attr, op in _marked_members(handler, _OP_ATTR):
@@ -1080,6 +1237,48 @@ def _served_operations(handler: Any, methods: dict) -> dict:
1080
1237
  return {name: entry for name, entry in methods.items() if name in chosen}
1081
1238
 
1082
1239
 
1240
+ def _with_forwarded(tool_handlers: list, handler: Any) -> list:
1241
+ """The ``@tool`` handlers and then the handler's forwarded tools, each as (descriptor, call), the call marked so it
1242
+ registers with its own schema; a forwarded tool named like another tool is refused."""
1243
+ hook = getattr(handler, "forwarded_tools", None)
1244
+ forwarded = [(entry.tool, _forwarding(entry)) for entry in (hook() if hook is not None else [])]
1245
+ names = [descriptor.name for descriptor, _fn in tool_handlers + forwarded]
1246
+ if len(set(names)) != len(names):
1247
+ raise ValueError(f"forwarded tools named like another tool: {sorted({n for n in names if names.count(n) > 1})}")
1248
+ return tool_handlers + forwarded
1249
+
1250
+
1251
+ def _forwarding(forwarded: ForwardedTool) -> Callable:
1252
+ async def call(**arguments: Any) -> Any:
1253
+ return await forwarded.call(**arguments)
1254
+
1255
+ setattr(call, _FORWARDED_ATTR, forwarded)
1256
+ return call
1257
+
1258
+
1259
+ @functools.cache
1260
+ def _as_it_came() -> type:
1261
+ """mcp's FastMCP output model for a forwarded tool with an output schema: the other server's result, unchecked."""
1262
+ from pydantic import RootModel
1263
+
1264
+ return RootModel[Any]
1265
+
1266
+
1267
+ @functools.cache
1268
+ def _passed_through() -> type:
1269
+ """mcp's FastMCP argument model for a forwarded tool: any arguments, each passed to the call as it came."""
1270
+ from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase
1271
+ from pydantic import ConfigDict
1272
+
1273
+ class PassedThrough(ArgModelBase):
1274
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
1275
+
1276
+ def model_dump_one_level(self) -> dict[str, Any]:
1277
+ return dict(self.__pydantic_extra__ or {})
1278
+
1279
+ return PassedThrough
1280
+
1281
+
1083
1282
  def _served_tools(handler: Any, tool_handlers: list) -> list:
1084
1283
  """The tools the handler's ``served_tools`` keeps, in its order and with its descriptors; all of them without one."""
1085
1284
  hook = getattr(handler, "served_tools", None)
@@ -60,6 +60,7 @@ from .operations import (
60
60
  type_text,
61
61
  write_files,
62
62
  )
63
+ from .keys import PRINTABLE_KEYS
63
64
  from .manifest import ManifestSegment, RecordingManifest
64
65
  from .platform import Platform
65
66
  from .settings import SETTINGS_ENV, ComputerSettings
@@ -100,6 +101,7 @@ __all__ = [
100
101
  "AppDeclaration",
101
102
  "APP_SETTINGS_ENV",
102
103
  "Display", "Image", "Effect", "ExecResult", "ReadyCheck", "Platform", "ComputerSettings", "SETTINGS_ENV",
104
+ "PRINTABLE_KEYS",
103
105
  "display", "set_display", "capture", "click", "drag", "scroll", "type_text", "press",
104
106
  "pointer_move", "pointer_down", "pointer_up", "pointer_position", "key_down", "key_up",
105
107
  "run_in_session", "write_files", "record", "Recording", "RecordHints", "RecordedFrame", "RecordedSegment",
@@ -111,6 +111,11 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
111
111
  """Checks of the driver's own, run with the base class's by computer-ready/v1; never sends input."""
112
112
  return []
113
113
 
114
+ async def bundled_skill(self) -> tuple[str, str, dict[str, str]] | None:
115
+ """The skill the software this driver drives ships for agents, as its description, its SKILL.md body without
116
+ frontmatter, and its other files by relative path; the computer skill leads with it. None by default."""
117
+ return None
118
+
114
119
  def create_app(self) -> Any:
115
120
  self._computer_require_init()
116
121
  return super().create_app()
@@ -38,7 +38,7 @@ class Extensions:
38
38
  mounted_card: Callable[[], EnvironmentCard]
39
39
 
40
40
  @agent_skill
41
- async def _computer_skill(self) -> tuple[str, str]:
41
+ async def _computer_skill(self) -> tuple[str, str, dict[str, str]]:
42
42
  return await skill(self)
43
43
 
44
44
  @extension(COMPUTER_READY, op="status", method="GET",
@@ -6,6 +6,8 @@ import difflib
6
6
 
7
7
  from .errors import InvalidArgument, Unsupported
8
8
 
9
+ #: The printable ASCII characters but the space, each the key value it types.
10
+ PRINTABLE_KEYS = tuple(chr(code) for code in range(0x21, 0x7F))
9
11
  NAMED = (
10
12
  "Alt", "AltGraph", "CapsLock", "Control", "Fn", "FnLock", "Hyper", "Meta", "NumLock", "ScrollLock", "Shift",
11
13
  "Super", "Symbol", "SymbolLock",