agentenv-framework-protocol 0.1.420__tar.gz → 0.1.422__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.
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/PKG-INFO +22 -2
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/README.md +21 -1
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/pyproject.toml +1 -1
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/__init__.py +2 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/agent_env_environment.py +24 -3
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/client.py +62 -14
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/conformance/__init__.py +5 -3
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/conformance/__main__.py +8 -2
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/conformance/checks.py +20 -1
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/conformance/client.py +7 -5
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/driver.py +2 -2
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/serve_app.py +8 -4
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/types.py +3 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_app_server.py +27 -2
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_conformance.py +16 -2
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_serve.py +6 -2
- agentenv_framework_protocol-0.1.422/tests/test_control_token.py +148 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/_declare.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/_linux.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/_load.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/app.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/app_data.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/app_runtime.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/apps.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/card.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/checks.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/keys.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/launch.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/recording.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/run_install_step.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/session.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/settings.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/skill.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_agent_env_environment.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_app_data.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_declare.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_driver.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_recording.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_tool_annotations.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/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.
|
|
3
|
+
Version: 0.1.422
|
|
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
|
|
@@ -283,6 +283,23 @@ async def display_get(self) -> dict: ...
|
|
|
283
283
|
async def display_set(self, width: int, height: int) -> dict: ...
|
|
284
284
|
```
|
|
285
285
|
|
|
286
|
+
### Framework-only routes and the control token
|
|
287
|
+
|
|
288
|
+
Everything an environment serves but its MCP endpoint and its card is agent-env's own: the data
|
|
289
|
+
plane, every `@extension` route and the tool-access routes. When agent-env deploys an environment
|
|
290
|
+
that takes one, it boots it with a per-deploy token in `AGENTENV_CONTROL_TOKEN`
|
|
291
|
+
(`CONTROL_TOKEN_ENV`), and the SDK then refuses a call to those routes that doesn't carry it as
|
|
292
|
+
`Authorization: Bearer <token>`, answering 401 with the code `unauthorized` before the route runs.
|
|
293
|
+
The MCP endpoint and the card stay open, so an agent reaches the environment's tools as before and
|
|
294
|
+
nothing else. Without the variable, as when the environment runs on its own, every route is open.
|
|
295
|
+
`control_guard()` wraps a route an environment adds itself the same way, as a computer driver does
|
|
296
|
+
its `/svc/<app>/` routes.
|
|
297
|
+
|
|
298
|
+
On the client side each call takes `headers`, and `client.control_headers(token)` builds them.
|
|
299
|
+
Code that names none runs under `client.authorized({env_url: client.control_headers(token)})`:
|
|
300
|
+
within it, a call to an address under `env_url` sends those headers. agent-env runs every task
|
|
301
|
+
step, a verifier's `verify` included, under such a grant for each environment it deployed.
|
|
302
|
+
|
|
286
303
|
### Tool access without a gateway
|
|
287
304
|
|
|
288
305
|
An environment agents reach directly, with no gateway in front, can enforce per-role tool access
|
|
@@ -644,7 +661,10 @@ report = await run_probe("http://localhost:18765", "full") # a ProbeReport
|
|
|
644
661
|
failed = [c for c in report.checks if c.status == "failed"]
|
|
645
662
|
```
|
|
646
663
|
|
|
647
|
-
The CLI exits 0 when no check failed, 1 when one did and 2 when it can't reach the driver.
|
|
664
|
+
The CLI exits 0 when no check failed, 1 when one did and 2 when it can't reach the driver. A
|
|
665
|
+
driver booted with a control token is probed with it: `--token`, by default
|
|
666
|
+
`$AGENTENV_CONTROL_TOKEN`, or `run_probe(..., token=...)`. Given one, the probe also checks that
|
|
667
|
+
`computer-ready`'s status refuses a call without it and with a wrong one.
|
|
648
668
|
|
|
649
669
|
| Level | Checks | Needs on the computer |
|
|
650
670
|
|---|---|---|
|
|
@@ -227,6 +227,23 @@ async def display_get(self) -> dict: ...
|
|
|
227
227
|
async def display_set(self, width: int, height: int) -> dict: ...
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
+
### Framework-only routes and the control token
|
|
231
|
+
|
|
232
|
+
Everything an environment serves but its MCP endpoint and its card is agent-env's own: the data
|
|
233
|
+
plane, every `@extension` route and the tool-access routes. When agent-env deploys an environment
|
|
234
|
+
that takes one, it boots it with a per-deploy token in `AGENTENV_CONTROL_TOKEN`
|
|
235
|
+
(`CONTROL_TOKEN_ENV`), and the SDK then refuses a call to those routes that doesn't carry it as
|
|
236
|
+
`Authorization: Bearer <token>`, answering 401 with the code `unauthorized` before the route runs.
|
|
237
|
+
The MCP endpoint and the card stay open, so an agent reaches the environment's tools as before and
|
|
238
|
+
nothing else. Without the variable, as when the environment runs on its own, every route is open.
|
|
239
|
+
`control_guard()` wraps a route an environment adds itself the same way, as a computer driver does
|
|
240
|
+
its `/svc/<app>/` routes.
|
|
241
|
+
|
|
242
|
+
On the client side each call takes `headers`, and `client.control_headers(token)` builds them.
|
|
243
|
+
Code that names none runs under `client.authorized({env_url: client.control_headers(token)})`:
|
|
244
|
+
within it, a call to an address under `env_url` sends those headers. agent-env runs every task
|
|
245
|
+
step, a verifier's `verify` included, under such a grant for each environment it deployed.
|
|
246
|
+
|
|
230
247
|
### Tool access without a gateway
|
|
231
248
|
|
|
232
249
|
An environment agents reach directly, with no gateway in front, can enforce per-role tool access
|
|
@@ -588,7 +605,10 @@ report = await run_probe("http://localhost:18765", "full") # a ProbeReport
|
|
|
588
605
|
failed = [c for c in report.checks if c.status == "failed"]
|
|
589
606
|
```
|
|
590
607
|
|
|
591
|
-
The CLI exits 0 when no check failed, 1 when one did and 2 when it can't reach the driver.
|
|
608
|
+
The CLI exits 0 when no check failed, 1 when one did and 2 when it can't reach the driver. A
|
|
609
|
+
driver booted with a control token is probed with it: `--token`, by default
|
|
610
|
+
`$AGENTENV_CONTROL_TOKEN`, or `run_probe(..., token=...)`. Given one, the probe also checks that
|
|
611
|
+
`computer-ready`'s status refuses a call without it and with a wrong one.
|
|
592
612
|
|
|
593
613
|
| Level | Checks | Needs on the computer |
|
|
594
614
|
|---|---|---|
|
|
@@ -53,6 +53,7 @@ from .manifest import (
|
|
|
53
53
|
manifest_compatible,
|
|
54
54
|
)
|
|
55
55
|
from .types import (
|
|
56
|
+
CONTROL_TOKEN_ENV,
|
|
56
57
|
DATA_OBJECTS_EXTENSION_URI,
|
|
57
58
|
INTAKE_EXTENSION_URI,
|
|
58
59
|
MCP_PATH,
|
|
@@ -161,6 +162,7 @@ __all__ = [
|
|
|
161
162
|
"ExtensionCallError",
|
|
162
163
|
"PROTOCOL_VERSION",
|
|
163
164
|
"RPC_PATH",
|
|
165
|
+
"CONTROL_TOKEN_ENV",
|
|
164
166
|
"WELL_KNOWN_PATH",
|
|
165
167
|
"METHOD_RESET",
|
|
166
168
|
"METHOD_ADD",
|
|
@@ -5,6 +5,7 @@ import datetime
|
|
|
5
5
|
import decimal
|
|
6
6
|
import enum
|
|
7
7
|
import functools
|
|
8
|
+
import hmac
|
|
8
9
|
import inspect
|
|
9
10
|
import json
|
|
10
11
|
import logging
|
|
@@ -28,7 +29,7 @@ from .tool_access import DISABLE as TOOL_DISABLE, ENABLE as TOOL_ENABLE, PATHS a
|
|
|
28
29
|
from .tool_access import extensions as tool_access_extensions
|
|
29
30
|
from .tool_context import EMPTY, ToolContext, bound, context_from_http, context_from_mcp
|
|
30
31
|
from .transfers import WriteNamespaceGrant
|
|
31
|
-
from .types import DATA_OBJECTS_EXTENSION_URI, MCP_TRANSPORT, SKILL_EXTENSION_URI, METHOD_ADD, METHOD_GET, METHOD_RESET, RPC_PATH, WELL_KNOWN_PATH, AddDataRequest, AddDataResponse, EnvironmentCapabilities, EnvironmentCard, EnvironmentExtension, EnvironmentInterface, EnvironmentTool, GetDataResponse, ResetDataResponse, error_body
|
|
32
|
+
from .types import CONTROL_TOKEN_ENV, DATA_OBJECTS_EXTENSION_URI, MCP_TRANSPORT, SKILL_EXTENSION_URI, METHOD_ADD, METHOD_GET, METHOD_RESET, RPC_PATH, WELL_KNOWN_PATH, AddDataRequest, AddDataResponse, EnvironmentCapabilities, EnvironmentCard, EnvironmentExtension, EnvironmentInterface, EnvironmentTool, GetDataResponse, ResetDataResponse, error_body
|
|
32
33
|
|
|
33
34
|
logger = logging.getLogger(__name__)
|
|
34
35
|
|
|
@@ -343,10 +344,11 @@ class AgentEnvApplication(ABC):
|
|
|
343
344
|
if isinstance(self.handler, AgentEnvEnvironment):
|
|
344
345
|
self.handler._application = self
|
|
345
346
|
self.environment_card = self._card_for(app)
|
|
346
|
-
|
|
347
|
+
guard = control_guard()
|
|
348
|
+
self._add_route(app, rpc_url, ["POST"], guard(self._dispatch))
|
|
347
349
|
self._add_route(app, card_url, ["GET"], self._serve_card)
|
|
348
350
|
for path, method, handler in self._ext_routes:
|
|
349
|
-
self._add_route(app, path, [method], handler)
|
|
351
|
+
self._add_route(app, path, [method], guard(handler))
|
|
350
352
|
for descriptor, fn in self._tools:
|
|
351
353
|
self._register_tool(app, descriptor, fn)
|
|
352
354
|
for resource in self._resources:
|
|
@@ -1544,6 +1546,25 @@ def _error_response(error: AgentEnvError) -> Response:
|
|
|
1544
1546
|
return _json_response(error_body(**error.fields()), error.status)
|
|
1545
1547
|
|
|
1546
1548
|
|
|
1549
|
+
def control_guard() -> Callable[[Callable], Callable]:
|
|
1550
|
+
"""What wraps a framework-only route, everything an env serves but its MCP endpoint and its card: with
|
|
1551
|
+
``CONTROL_TOKEN_ENV`` set, a call that doesn't carry that token as its bearer is refused before the route runs;
|
|
1552
|
+
without it, nothing."""
|
|
1553
|
+
token = os.environ.get(CONTROL_TOKEN_ENV)
|
|
1554
|
+
if not token:
|
|
1555
|
+
return lambda handler: handler
|
|
1556
|
+
expected = f"Bearer {token}".encode()
|
|
1557
|
+
|
|
1558
|
+
def guard(handler: Callable) -> Callable:
|
|
1559
|
+
async def guarded(request: Request) -> Response:
|
|
1560
|
+
if not hmac.compare_digest(request.headers.get("authorization", "").encode(), expected):
|
|
1561
|
+
return _error_response(AgentEnvError(
|
|
1562
|
+
"unauthorized", "this route is agent-env's own: it takes the env's control token", status=401))
|
|
1563
|
+
return await handler(request)
|
|
1564
|
+
return guarded
|
|
1565
|
+
return guard
|
|
1566
|
+
|
|
1567
|
+
|
|
1547
1568
|
def _tool_access_handler(access: ToolAccess, action: str) -> Callable:
|
|
1548
1569
|
async def handler(request: Request) -> Response:
|
|
1549
1570
|
raw = await request.body()
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
"""Data-plane wire protocol client (JSON-RPC) for servers implementing the AgentEnv protocol.
|
|
1
|
+
"""Data-plane wire protocol client (JSON-RPC) for servers implementing the AgentEnv protocol. A call to an env's
|
|
2
|
+
framework-only routes carries its control token: as the call's ``headers`` (``control_headers``), or, for code that
|
|
3
|
+
names none, from the grant it runs under (``authorized``)."""
|
|
2
4
|
from __future__ import annotations
|
|
3
5
|
|
|
6
|
+
import contextlib
|
|
4
7
|
import logging
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from contextvars import ContextVar
|
|
5
10
|
|
|
6
11
|
import httpx
|
|
7
12
|
|
|
@@ -11,6 +16,9 @@ from .types import INTAKE_EXTENSION_URI, MCP_PATH, MCP_TRANSPORT, METHOD_ADD, ME
|
|
|
11
16
|
|
|
12
17
|
logger = logging.getLogger(__name__)
|
|
13
18
|
|
|
19
|
+
#: The headers that carry the control token of each env address the running code is granted, by address.
|
|
20
|
+
_GRANTS: ContextVar[dict[str, dict[str, str]]] = ContextVar("agentenv_control_grants", default={})
|
|
21
|
+
|
|
14
22
|
|
|
15
23
|
class ExtensionCallError(httpx.HTTPStatusError):
|
|
16
24
|
"""A non-2xx answer from an extension method, with the error fields the server sent. A body without them
|
|
@@ -35,10 +43,45 @@ def _error_fields(response: httpx.Response) -> dict:
|
|
|
35
43
|
return error if isinstance(error, dict) else {}
|
|
36
44
|
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
def control_headers(token: str | None) -> dict[str, str]:
|
|
47
|
+
"""The headers that carry an env's control token to its framework-only routes; none without one."""
|
|
48
|
+
return {"Authorization": f"Bearer {token}"} if token else {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@contextlib.contextmanager
|
|
52
|
+
def authorized(grants: dict[str | None, dict[str, str]]) -> Iterator[None]:
|
|
53
|
+
"""Within it, a call to an address under one of ``grants``, an env's address and the headers that carry its
|
|
54
|
+
control token (``control_headers``), sends those headers when the call names none of its own: how the steps
|
|
55
|
+
agent-env runs, and the verifiers they call, reach an env's framework-only routes as written."""
|
|
56
|
+
granted = _GRANTS.set({**_GRANTS.get(),
|
|
57
|
+
**{url.rstrip("/"): sent for url, sent in grants.items() if url and sent}})
|
|
58
|
+
try:
|
|
59
|
+
yield
|
|
60
|
+
finally:
|
|
61
|
+
_GRANTS.reset(granted)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def request_headers(url: str, headers: dict[str, str] | None = None) -> dict[str, str]:
|
|
65
|
+
"""What a call to ``url`` sends: ``headers`` when it names them, else the headers its grant gives
|
|
66
|
+
(``authorized``), if any."""
|
|
67
|
+
if headers is not None:
|
|
68
|
+
return headers
|
|
69
|
+
return granted_headers(url, _GRANTS.get())
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def granted_headers(url: str, grants: dict[str | None, dict[str, str]]) -> dict[str, str]:
|
|
73
|
+
"""What ``grants``, env addresses and the headers that carry their control tokens, give a call to ``url``: those of
|
|
74
|
+
the address it is or is under, and none anywhere else."""
|
|
75
|
+
return next((sent for base, sent in grants.items()
|
|
76
|
+
if base and (url == base.rstrip("/") or url.startswith(f"{base.rstrip('/')}/"))), {})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def _rpc(base_url: str, method: str, params: dict, timeout: int, verify: bool,
|
|
80
|
+
headers: dict[str, str] | None) -> dict:
|
|
39
81
|
request = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
|
|
82
|
+
url = f"{base_url}{RPC_PATH}"
|
|
40
83
|
async with httpx.AsyncClient(verify=verify) as client:
|
|
41
|
-
response = await client.post(
|
|
84
|
+
response = await client.post(url, json=request, timeout=timeout, headers=request_headers(url, headers))
|
|
42
85
|
response.raise_for_status()
|
|
43
86
|
body = response.json()
|
|
44
87
|
if "error" in body:
|
|
@@ -47,18 +90,20 @@ async def _rpc(base_url: str, method: str, params: dict, timeout: int, verify: b
|
|
|
47
90
|
return body["result"]
|
|
48
91
|
|
|
49
92
|
|
|
50
|
-
async def reset_data(base_url: str, timeout: int = 30, verify: bool = True
|
|
51
|
-
|
|
93
|
+
async def reset_data(base_url: str, timeout: int = 30, verify: bool = True, *,
|
|
94
|
+
headers: dict[str, str] | None = None) -> ResetDataResponse:
|
|
95
|
+
return ResetDataResponse.model_validate(await _rpc(base_url, METHOD_RESET, {}, timeout, verify, headers))
|
|
52
96
|
|
|
53
97
|
|
|
54
|
-
async def add_data(base_url: str, parts: list[Part], timeout: int = 120, verify: bool = True
|
|
98
|
+
async def add_data(base_url: str, parts: list[Part], timeout: int = 120, verify: bool = True, *,
|
|
99
|
+
headers: dict[str, str] | None = None) -> AddDataResponse:
|
|
55
100
|
params = {"parts": [p.model_dump(mode="json", exclude_none=True) for p in parts]}
|
|
56
|
-
return AddDataResponse.model_validate(await _rpc(base_url, METHOD_ADD, params, timeout, verify))
|
|
101
|
+
return AddDataResponse.model_validate(await _rpc(base_url, METHOD_ADD, params, timeout, verify, headers))
|
|
57
102
|
|
|
58
103
|
|
|
59
104
|
async def get_data(
|
|
60
105
|
base_url: str, timeout: int = 30, verify: bool = True, *, write_namespace: WriteNamespaceGrant | None = None,
|
|
61
|
-
paths: list[str] | None = None,
|
|
106
|
+
paths: list[str] | None = None, headers: dict[str, str] | None = None,
|
|
62
107
|
) -> GetDataResponse:
|
|
63
108
|
"""`data/get`. A server advertising `DATA_OBJECTS_EXTENSION_URI` may be handed `write_namespace` to upload
|
|
64
109
|
its export under; `uploaded_object_path` reads from the answer where it did. `paths` names what a computer
|
|
@@ -66,7 +111,7 @@ async def get_data(
|
|
|
66
111
|
params = {} if write_namespace is None else {"write_namespace": write_namespace.model_dump(mode="json", exclude_none=True)}
|
|
67
112
|
if paths is not None:
|
|
68
113
|
params["paths"] = paths
|
|
69
|
-
return GetDataResponse.model_validate(await _rpc(base_url, METHOD_GET, params, timeout, verify))
|
|
114
|
+
return GetDataResponse.model_validate(await _rpc(base_url, METHOD_GET, params, timeout, verify, headers))
|
|
70
115
|
|
|
71
116
|
|
|
72
117
|
async def get_card(base_url: str, timeout: int = 10, verify: bool = True) -> dict:
|
|
@@ -180,7 +225,8 @@ def find_child(card: dict, name: str) -> dict | None:
|
|
|
180
225
|
return None
|
|
181
226
|
|
|
182
227
|
|
|
183
|
-
async def get_interface_manifest(base_url: str, interface: str, endpoint: str = INTERFACE_MANIFEST_PATH, timeout: int = 30, verify: bool = True
|
|
228
|
+
async def get_interface_manifest(base_url: str, interface: str, endpoint: str = INTERFACE_MANIFEST_PATH, timeout: int = 30, verify: bool = True, *,
|
|
229
|
+
headers: dict[str, str] | None = None):
|
|
184
230
|
"""Fetch one interface's manifest from a server's manifest index endpoint.
|
|
185
231
|
|
|
186
232
|
``endpoint`` is the index path — pass the one advertised on the card under
|
|
@@ -191,12 +237,13 @@ async def get_interface_manifest(base_url: str, interface: str, endpoint: str =
|
|
|
191
237
|
"""
|
|
192
238
|
url = f"{base_url.rstrip('/')}{endpoint}/{interface}"
|
|
193
239
|
async with httpx.AsyncClient(verify=verify) as client:
|
|
194
|
-
response = await client.get(url, timeout=timeout)
|
|
240
|
+
response = await client.get(url, timeout=timeout, headers=request_headers(url, headers))
|
|
195
241
|
response.raise_for_status()
|
|
196
242
|
return response.json()
|
|
197
243
|
|
|
198
244
|
|
|
199
|
-
async def invoke_extension(base_url: str, card: dict, uri: str, params: dict | None = None, timeout: int = 30, verify: bool = True, *, method: str | None = None
|
|
245
|
+
async def invoke_extension(base_url: str, card: dict, uri: str, params: dict | None = None, timeout: int = 30, verify: bool = True, *, method: str | None = None,
|
|
246
|
+
headers: dict[str, str] | None = None):
|
|
200
247
|
"""Invoke a card extension via its advertised REST endpoint.
|
|
201
248
|
|
|
202
249
|
Reads the endpoint and HTTP verb of the extension method named `method` from the card, so the
|
|
@@ -215,11 +262,12 @@ async def invoke_extension(base_url: str, card: dict, uri: str, params: dict | N
|
|
|
215
262
|
if not op["endpoint"]:
|
|
216
263
|
raise ValueError(f"extension {uri} has no endpoint in params")
|
|
217
264
|
url = f"{base_url}{op['endpoint']}"
|
|
265
|
+
sent = request_headers(url, headers)
|
|
218
266
|
async with httpx.AsyncClient(verify=verify) as client:
|
|
219
267
|
if op["method"] == "GET":
|
|
220
|
-
response = await client.get(url, params=params or {}, timeout=timeout)
|
|
268
|
+
response = await client.get(url, params=params or {}, timeout=timeout, headers=sent)
|
|
221
269
|
else:
|
|
222
|
-
response = await client.request(op["method"], url, json=params or {}, timeout=timeout)
|
|
270
|
+
response = await client.request(op["method"], url, json=params or {}, timeout=timeout, headers=sent)
|
|
223
271
|
if response.status_code >= 400:
|
|
224
272
|
raise ExtensionCallError(response)
|
|
225
273
|
return response.json()
|
|
@@ -11,9 +11,11 @@ from .report import CheckResult, Level, ProbeReport, Status, Verdict
|
|
|
11
11
|
__all__ = ["run_probe", "ProbeReport", "CheckResult", "Level", "Status", "Verdict"]
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
async def run_probe(url: str, level: Level = "full", *, timeout_s: float = 60, ready_timeout_s: float = 60
|
|
15
|
-
|
|
16
|
-
driver
|
|
14
|
+
async def run_probe(url: str, level: Level = "full", *, timeout_s: float = 60, ready_timeout_s: float = 60,
|
|
15
|
+
token: str | None = None) -> ProbeReport:
|
|
16
|
+
"""Probe the driver serving its card at ``url``, sending ``token``, its control token, to its framework-only routes.
|
|
17
|
+
``ready_timeout_s`` is how long it may take to report ready."""
|
|
18
|
+
driver = await Driver.connect(url, timeout_s, token)
|
|
17
19
|
async with driver.session() as session:
|
|
18
20
|
probe = Probe(driver, session, ready_timeout_s)
|
|
19
21
|
await probe.basic()
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""``python -m agentenv_protocol.computer.conformance <driver url>``: probe a running driver, print each check, and
|
|
2
|
-
exit 1 when one failed or 2 when the driver couldn't be reached.
|
|
2
|
+
exit 1 when one failed or 2 when the driver couldn't be reached. ``--token`` is its control token, by default
|
|
3
|
+
``CONTROL_TOKEN_ENV``'s."""
|
|
3
4
|
from __future__ import annotations
|
|
4
5
|
|
|
5
6
|
import argparse
|
|
6
7
|
import asyncio
|
|
8
|
+
import os
|
|
7
9
|
import sys
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
|
|
12
|
+
from ...types import CONTROL_TOKEN_ENV
|
|
10
13
|
from . import run_probe
|
|
11
14
|
|
|
12
15
|
_MARK = {"passed": "PASS", "failed": "FAIL", "unverified": "----", "not_applicable": "n/a "}
|
|
@@ -19,9 +22,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
19
22
|
parser.add_argument("--level", choices=("basic", "full"), default="full")
|
|
20
23
|
parser.add_argument("--json", type=Path, help="also write the report to this file")
|
|
21
24
|
parser.add_argument("--timeout", type=float, default=60, help="seconds for each call, and to report ready")
|
|
25
|
+
parser.add_argument("--token", default=os.environ.get(CONTROL_TOKEN_ENV),
|
|
26
|
+
help=f"the driver's control token, if it was given one (default: ${CONTROL_TOKEN_ENV})")
|
|
22
27
|
args = parser.parse_args(argv)
|
|
23
28
|
try:
|
|
24
|
-
report = asyncio.run(run_probe(args.url, args.level, timeout_s=args.timeout, ready_timeout_s=args.timeout
|
|
29
|
+
report = asyncio.run(run_probe(args.url, args.level, timeout_s=args.timeout, ready_timeout_s=args.timeout,
|
|
30
|
+
token=args.token))
|
|
25
31
|
except Exception as e:
|
|
26
32
|
print(f"couldn't probe {args.url}: {type(e).__name__}: {e}", file=sys.stderr)
|
|
27
33
|
return 2
|
|
@@ -88,7 +88,8 @@ class Probe:
|
|
|
88
88
|
for check, section in (("P0", self._ready), ("P1", self._card), ("P2", self._frame), ("P5", self._refusals),
|
|
89
89
|
("X", self._display), ("X", self._spaces), ("X", self._keyboard_settings),
|
|
90
90
|
("X", self._exec), ("X", self._data_get), ("X", self._app_exports),
|
|
91
|
-
("X", self._tool_access), ("X", self._pointer), ("X", self._skill)
|
|
91
|
+
("X", self._tool_access), ("X", self._pointer), ("X", self._skill),
|
|
92
|
+
("X", self._control)):
|
|
92
93
|
await self._guarded(check, section)
|
|
93
94
|
self._grants()
|
|
94
95
|
|
|
@@ -422,6 +423,24 @@ class Probe:
|
|
|
422
423
|
await self.check("X", f"app {app.get('name')}'s data/get answers its parts through the driver", export,
|
|
423
424
|
*DATA_PLANE)
|
|
424
425
|
|
|
426
|
+
async def _control(self) -> None:
|
|
427
|
+
if not self.driver.headers:
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
async def refused() -> str | None:
|
|
431
|
+
for sent, without in (({}, "without"), (protocol.control_headers("not-its-token"), "with a wrong")):
|
|
432
|
+
try:
|
|
433
|
+
await protocol.invoke_extension(self.driver.url, self.driver.card, COMPUTER_READY, method="status",
|
|
434
|
+
timeout=int(self.driver.timeout_s), headers=sent)
|
|
435
|
+
except ExtensionCallError as e:
|
|
436
|
+
if e.response.status_code != 401:
|
|
437
|
+
return f"computer-ready status answered a call {without} control token {e.response.status_code}"
|
|
438
|
+
continue
|
|
439
|
+
return f"computer-ready status answered a call {without} control token"
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
await self.check("X", "a framework-only route refuses a call without the control token", refused)
|
|
443
|
+
|
|
425
444
|
async def _tool_access(self) -> None:
|
|
426
445
|
if not (self.driver.declares(DISABLE_TOOL_EXTENSION_URI) and self.driver.declares(ENABLE_TOOL_EXTENSION_URI)):
|
|
427
446
|
return
|
|
@@ -24,14 +24,15 @@ STREAM_READ_S = 300
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class Driver:
|
|
27
|
-
def __init__(self, url: str, card: dict, timeout_s: float) -> None:
|
|
27
|
+
def __init__(self, url: str, card: dict, timeout_s: float, token: str | None = None) -> None:
|
|
28
28
|
self.url = url.rstrip("/")
|
|
29
29
|
self.card = card
|
|
30
30
|
self.timeout_s = timeout_s
|
|
31
|
+
self.headers = protocol.control_headers(token) if token else None
|
|
31
32
|
|
|
32
33
|
@classmethod
|
|
33
|
-
async def connect(cls, url: str, timeout_s: float) -> Driver:
|
|
34
|
-
return cls(url, await protocol.get_card(url.rstrip("/"), timeout=int(timeout_s)), timeout_s)
|
|
34
|
+
async def connect(cls, url: str, timeout_s: float, token: str | None = None) -> Driver:
|
|
35
|
+
return cls(url, await protocol.get_card(url.rstrip("/"), timeout=int(timeout_s)), timeout_s, token)
|
|
35
36
|
|
|
36
37
|
@property
|
|
37
38
|
def name(self) -> str:
|
|
@@ -59,12 +60,13 @@ class Driver:
|
|
|
59
60
|
|
|
60
61
|
async def invoke(self, uri: str, method: str, params: dict | None = None) -> Any:
|
|
61
62
|
return await protocol.invoke_extension(self.url, self.card, uri, params or {}, method=method,
|
|
62
|
-
timeout=int(self.timeout_s))
|
|
63
|
+
timeout=int(self.timeout_s), headers=self.headers)
|
|
63
64
|
|
|
64
65
|
async def rpc(self, method: str, params: dict | None = None, card: dict | None = None) -> dict:
|
|
65
66
|
"""``method``'s answer from the data plane of ``card``, the driver's own or a child's it fronts."""
|
|
66
67
|
async with httpx.AsyncClient(timeout=self.timeout_s) as http:
|
|
67
|
-
|
|
68
|
+
url = self.url + ((card or self.card).get("url") or "/agentenv")
|
|
69
|
+
response = await http.post(url, headers=protocol.request_headers(url, self.headers),
|
|
68
70
|
json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}})
|
|
69
71
|
return response.json()
|
|
70
72
|
|
|
@@ -12,7 +12,7 @@ from starlette.requests import Request
|
|
|
12
12
|
from starlette.responses import Response
|
|
13
13
|
|
|
14
14
|
from ..agent_env_environment import (_CARD_CONFIG_ATTR, AgentEnvEnvironment, AgentEnvFastMCPApplication,
|
|
15
|
-
_error_response, environment_card)
|
|
15
|
+
_error_response, control_guard, environment_card)
|
|
16
16
|
from ..tool_context import ToolContext
|
|
17
17
|
from ..types import EnvironmentCard, EnvironmentExtension, EnvironmentTool
|
|
18
18
|
from . import _linux, apps, card
|
|
@@ -135,7 +135,7 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
|
|
|
135
135
|
return await apps.children(self._computer.apps) if self._computer.apps else None
|
|
136
136
|
|
|
137
137
|
def _computer_front_apps(self, app: Any) -> Any:
|
|
138
|
-
app.custom_route("/svc/{name}/{path:path}", methods=["GET", "POST"])(self._computer_forward)
|
|
138
|
+
app.custom_route("/svc/{name}/{path:path}", methods=["GET", "POST"])(control_guard()(self._computer_forward))
|
|
139
139
|
return app
|
|
140
140
|
|
|
141
141
|
async def _computer_forward(self, request: Request) -> Response:
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
"""Serve a computer app's class from its file, as its computer's program starts it:
|
|
2
2
|
``python -m agentenv_protocol.computer.serve_app <file>:<Class>``, run in the app's folder, serves the class's card and
|
|
3
|
-
``computer-app/v1`` on the Unix socket ``.agentenv/server.sock`` there
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
``computer-app/v1`` on the Unix socket ``.agentenv/server.sock`` there, which only the computer reaches, so it takes no
|
|
4
|
+
control token (``CONTROL_TOKEN_ENV``): the driver checks it in front of the app. Before it serves, it keeps a copy of
|
|
5
|
+
the app's data paths as installed, which a reset puts back; once it serves, it adds the app's launcher and launches a
|
|
6
|
+
boot app (``AppRuntime.start``). A server that can't start, or whose start fails once it serves, writes why to
|
|
7
|
+
``.agentenv/server.failure``, which its computer's driver reports, and exits. Needs the ``app`` extra."""
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
import asyncio
|
|
10
11
|
import contextlib
|
|
12
|
+
import os
|
|
11
13
|
import sys
|
|
12
14
|
import traceback
|
|
13
15
|
from pathlib import Path
|
|
14
16
|
|
|
15
17
|
import uvicorn
|
|
16
18
|
|
|
19
|
+
from ..types import CONTROL_TOKEN_ENV
|
|
17
20
|
from ._load import load_class
|
|
18
21
|
from .app import FAILURE, SOCKET, ComputerAppEnvironment
|
|
19
22
|
|
|
@@ -35,6 +38,7 @@ def _serve(argv: list[str]) -> None:
|
|
|
35
38
|
if len(argv) != 1 or ":" not in argv[0]:
|
|
36
39
|
raise SystemExit(USAGE)
|
|
37
40
|
file, _, name = argv[0].partition(":")
|
|
41
|
+
os.environ.pop(CONTROL_TOKEN_ENV, None)
|
|
38
42
|
app = load_class(Path(file), name, ComputerAppEnvironment)()
|
|
39
43
|
runtime = app._computer_app_runtime()
|
|
40
44
|
app._computer_app_data_plane().keep_initial()
|
|
@@ -16,6 +16,9 @@ PROTOCOL_VERSION = "1.0"
|
|
|
16
16
|
|
|
17
17
|
WELL_KNOWN_PATH = "/.well-known/agent-env.json"
|
|
18
18
|
RPC_PATH = "/agentenv"
|
|
19
|
+
#: The variable that hands an env the token its framework-only routes take as their bearer, everything it serves but
|
|
20
|
+
#: its MCP endpoint and its card; agent-env sets it when it deploys the env, and without it those routes are open.
|
|
21
|
+
CONTROL_TOKEN_ENV = "AGENTENV_CONTROL_TOKEN"
|
|
19
22
|
|
|
20
23
|
# A card declares its MCP endpoint as an `additionalInterfaces` entry with this transport; a card
|
|
21
24
|
# without one is reached at MCP_PATH by convention.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""A computer app's own server: it adds the app's launcher, launches a boot app once per boot and says whether the app
|
|
2
2
|
is ready (``computer-app/v1``); its computer's driver finds it on the socket in the app's folder and asks it, lists it
|
|
3
|
-
as a child environment and forwards its requests to it,
|
|
3
|
+
as a child environment and forwards its requests to it, only with the computer's control token when it has one, and
|
|
4
|
+
leaves the paths it owns out of its own data/get."""
|
|
4
5
|
from __future__ import annotations
|
|
5
6
|
|
|
6
7
|
import asyncio
|
|
@@ -31,7 +32,7 @@ from agentenv_protocol.computer import (ComputerAppEnvironment, Launch, ReadyChe
|
|
|
31
32
|
from agentenv_protocol.computer import app, app_runtime, apps, session # noqa: E402
|
|
32
33
|
from agentenv_protocol.computer.app import COMPUTER_APP, FAILURE, SOCKET # noqa: E402
|
|
33
34
|
from agentenv_protocol.computer.platform import Platform # noqa: E402
|
|
34
|
-
from agentenv_protocol.types import DATA_OBJECTS_EXTENSION_URI, WELL_KNOWN_PATH # noqa: E402
|
|
35
|
+
from agentenv_protocol.types import CONTROL_TOKEN_ENV, DATA_OBJECTS_EXTENSION_URI, WELL_KNOWN_PATH # noqa: E402
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
|
|
@@ -311,6 +312,30 @@ async def test_the_driver_lists_its_apps_as_children_forwards_each_ones_requests
|
|
|
311
312
|
"Documents/note.txt"]
|
|
312
313
|
|
|
313
314
|
|
|
315
|
+
@pytest.mark.asyncio
|
|
316
|
+
async def test_with_a_control_token_the_driver_lists_its_apps_and_forwards_to_one_only_with_it(tmp_path, monkeypatch):
|
|
317
|
+
from computer_fake import FakeDriver, http_client
|
|
318
|
+
|
|
319
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
320
|
+
(tmp_path / "Mail").mkdir()
|
|
321
|
+
(tmp_path / "Mail/inbox.txt").write_text("hi")
|
|
322
|
+
get = {"jsonrpc": "2.0", "id": 1, "method": "data/get", "params": {}}
|
|
323
|
+
with tempfile.TemporaryDirectory(dir="/tmp", prefix="apps-") as root:
|
|
324
|
+
monkeypatch.setattr(apps, "APPS", Path(root))
|
|
325
|
+
async with _serving(Inbox(), Path(root) / "mail", monkeypatch):
|
|
326
|
+
monkeypatch.setenv(CONTROL_TOKEN_ENV, "s3cret")
|
|
327
|
+
async with http_client(FakeDriver()) as http:
|
|
328
|
+
card = (await http.get(WELL_KNOWN_PATH)).json()
|
|
329
|
+
refused = [await http.post("/svc/mail/agentenv", json=get, headers=headers)
|
|
330
|
+
for headers in ({}, client.control_headers("guess"))]
|
|
331
|
+
forwarded = await http.post("/svc/mail/agentenv", json=get, headers=client.control_headers("s3cret"))
|
|
332
|
+
|
|
333
|
+
assert [child["name"] for child in card["children_environments"]] == ["mail"]
|
|
334
|
+
assert [answer.status_code for answer in refused] == [401, 401]
|
|
335
|
+
assert [part["metadata"][DATA_OBJECTS_EXTENSION_URI]["path"] for part in forwarded.json()["result"]["parts"]] == [
|
|
336
|
+
"Mail/inbox.txt"]
|
|
337
|
+
|
|
338
|
+
|
|
314
339
|
@pytest.mark.asyncio
|
|
315
340
|
async def test_the_session_launches_an_app_in_a_session_of_its_own_from_its_folder(tmp_path, monkeypatch):
|
|
316
341
|
monkeypatch.setenv("DISPLAY", ":99")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""The conformance probe against fake drivers on loopback: the fake driver with the fake fixture passes every check; a
|
|
2
|
-
driver
|
|
3
|
-
|
|
2
|
+
driver given a control token passes with it, refusing calls without it, and fails without it; a driver without what
|
|
3
|
+
full needs gets those checks unverified; a driver broken one way fails the check for that way; each app a driver
|
|
4
|
+
fronts that exports is asked for its export once; the report's verdicts and the CLI."""
|
|
4
5
|
from __future__ import annotations
|
|
5
6
|
|
|
6
7
|
import json
|
|
@@ -33,6 +34,7 @@ from agentenv_protocol.computer.conformance import CheckResult, ProbeReport, run
|
|
|
33
34
|
from agentenv_protocol.computer.conformance import checks # noqa: E402
|
|
34
35
|
from agentenv_protocol.computer.conformance.client import Driver # noqa: E402
|
|
35
36
|
from agentenv_protocol.computer.conformance.__main__ import main # noqa: E402
|
|
37
|
+
from agentenv_protocol import CONTROL_TOKEN_ENV # noqa: E402
|
|
36
38
|
from agentenv_protocol.tool_access import DISABLE_TOOL_EXTENSION_URI # noqa: E402
|
|
37
39
|
from agentenv_protocol.types import EnvironmentCard # noqa: E402
|
|
38
40
|
|
|
@@ -71,6 +73,18 @@ async def test_the_fake_driver_passes_every_check_at_full_and_cleans_up(home):
|
|
|
71
73
|
assert ["kill", "4242"] in driver.commands
|
|
72
74
|
|
|
73
75
|
|
|
76
|
+
@pytest.mark.asyncio
|
|
77
|
+
async def test_a_driver_given_a_control_token_passes_with_it_and_refuses_calls_without_it(monkeypatch):
|
|
78
|
+
monkeypatch.setenv(CONTROL_TOKEN_ENV, "s3cret")
|
|
79
|
+
|
|
80
|
+
report = await _probe(FixtureDriver(), "basic", token="s3cret")
|
|
81
|
+
unauthorized = await _probe(FixtureDriver(), "basic")
|
|
82
|
+
|
|
83
|
+
assert report.passed, _failed(report)
|
|
84
|
+
assert _status(report, "a framework-only route refuses a call without the control token") == "passed"
|
|
85
|
+
assert not unauthorized.passed and "401 unauthorized" in _failed(unauthorized)[0]
|
|
86
|
+
|
|
87
|
+
|
|
74
88
|
@pytest.mark.asyncio
|
|
75
89
|
async def test_a_driver_whose_default_space_is_norm_passes_at_full():
|
|
76
90
|
report = await _probe(FixtureDriver(settings=ComputerSettings(default_space="norm")))
|
|
@@ -3,6 +3,7 @@ that isn't such a class exits saying why, and an app's server that can't start s
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
@@ -11,6 +12,7 @@ pytest.importorskip("mcp.server.fastmcp")
|
|
|
11
12
|
pytest.importorskip("PIL")
|
|
12
13
|
pytest.importorskip("uvicorn")
|
|
13
14
|
|
|
15
|
+
from agentenv_protocol import CONTROL_TOKEN_ENV # noqa: E402
|
|
14
16
|
from agentenv_protocol.computer import ComputerDriverEnvironment, run_install_step, serve_app # noqa: E402
|
|
15
17
|
from agentenv_protocol.computer.app import BUILD_SETTINGS, FAILURE, SOCKET # noqa: E402
|
|
16
18
|
from agentenv_protocol.computer.app_runtime import AppRuntime # noqa: E402
|
|
@@ -164,10 +166,11 @@ def app_folder(tmp_path, monkeypatch, isolated):
|
|
|
164
166
|
return tmp_path, ran
|
|
165
167
|
|
|
166
168
|
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
+
def test_an_apps_server_serves_on_the_socket_in_its_folder_with_no_control_token_and_starts_the_app_once_it_serves(
|
|
170
|
+
app_folder, monkeypatch):
|
|
169
171
|
folder, ran = app_folder
|
|
170
172
|
started = []
|
|
173
|
+
monkeypatch.setenv(CONTROL_TOKEN_ENV, "s3cret")
|
|
171
174
|
|
|
172
175
|
async def start(runtime):
|
|
173
176
|
started.append(runtime)
|
|
@@ -177,6 +180,7 @@ def test_an_apps_server_serves_on_the_socket_in_its_folder_and_starts_the_app_on
|
|
|
177
180
|
serve_app.main(["app.py:Notes"])
|
|
178
181
|
|
|
179
182
|
(server,) = ran
|
|
183
|
+
assert CONTROL_TOKEN_ENV not in os.environ
|
|
180
184
|
assert server.config.uds == str(SOCKET) and not server.should_exit
|
|
181
185
|
assert not (folder / SOCKET).exists() and not (folder / FAILURE).exists()
|
|
182
186
|
assert len(started) == 1 and isinstance(started[0], AppRuntime)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""The control token: with CONTROL_TOKEN_ENV set when an env mounts, everything it serves but its MCP endpoint and its
|
|
2
|
+
card refuses a call without that token, or with another, before the route runs, and answers one with it; the MCP
|
|
3
|
+
endpoint and the card stay open, and without the variable every route is open. The client sends a call's own headers,
|
|
4
|
+
or else the headers a grant gives the address it calls."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
|
|
9
|
+
import anyio
|
|
10
|
+
import httpx
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
pytest.importorskip("mcp.server.fastmcp")
|
|
14
|
+
|
|
15
|
+
from mcp import ClientSession # noqa: E402
|
|
16
|
+
from mcp.client import streamable_http as _streamable_http # noqa: E402
|
|
17
|
+
from mcp.server.fastmcp import FastMCP # noqa: E402
|
|
18
|
+
|
|
19
|
+
from agentenv_protocol import ( # noqa: E402
|
|
20
|
+
CONTROL_TOKEN_ENV,
|
|
21
|
+
RPC_PATH,
|
|
22
|
+
WELL_KNOWN_PATH,
|
|
23
|
+
AgentEnvEnvironment,
|
|
24
|
+
add_data,
|
|
25
|
+
client,
|
|
26
|
+
environment_card,
|
|
27
|
+
extension,
|
|
28
|
+
tool,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
pytestmark = pytest.mark.skipif(not hasattr(_streamable_http, "streamable_http_client"),
|
|
32
|
+
reason="mcp without streamable_http_client")
|
|
33
|
+
|
|
34
|
+
_BASE = "http://127.0.0.1:8000"
|
|
35
|
+
RAN: list[str] = []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@environment_card(name="desk")
|
|
39
|
+
class _Desk(AgentEnvEnvironment):
|
|
40
|
+
tool_access = True
|
|
41
|
+
|
|
42
|
+
@tool()
|
|
43
|
+
def read(self) -> str:
|
|
44
|
+
return "read"
|
|
45
|
+
|
|
46
|
+
@add_data
|
|
47
|
+
async def load(self, parts: list) -> None:
|
|
48
|
+
RAN.append("data/add")
|
|
49
|
+
|
|
50
|
+
@extension("urn:agentenv:test-exec/v1", op="run")
|
|
51
|
+
async def run(self, argv: list) -> dict:
|
|
52
|
+
RAN.append("run")
|
|
53
|
+
return {"ran": argv}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@contextlib.asynccontextmanager
|
|
57
|
+
async def _served():
|
|
58
|
+
"""An ``_Desk`` mounted on mcp's FastMCP as the environment's variables stand, and its REST and MCP clients."""
|
|
59
|
+
app = FastMCP("desk", json_response=True)
|
|
60
|
+
_Desk().mount(app)
|
|
61
|
+
asgi = app.streamable_http_app()
|
|
62
|
+
async with app.session_manager.run():
|
|
63
|
+
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=asgi), base_url=_BASE) as rest:
|
|
64
|
+
async with _streamable_http.streamable_http_client(f"{_BASE}/mcp", http_client=rest) as (read, write, _):
|
|
65
|
+
async with ClientSession(read, write) as mcp:
|
|
66
|
+
await mcp.initialize()
|
|
67
|
+
yield rest, mcp
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def _calls(rest: httpx.AsyncClient, headers: dict) -> list[int]:
|
|
71
|
+
"""The status of a call to each framework-only route with ``headers``: the data plane, an extension, tool access."""
|
|
72
|
+
load = {"jsonrpc": "2.0", "id": 1, "method": "data/add", "params": {"parts": [{"kind": "data", "data": {}}]}}
|
|
73
|
+
return [(await rest.post(RPC_PATH, json=load, headers=headers)).status_code,
|
|
74
|
+
(await rest.post("/agentenv/ext/test-exec/run", json={"argv": ["ls"]}, headers=headers)).status_code,
|
|
75
|
+
(await rest.post("/tools/disable", json={"role": "agent", "tools": ["read"]}, headers=headers)).status_code]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
async def test_with_a_token_every_framework_only_route_refuses_a_call_without_it_before_it_runs(monkeypatch):
|
|
80
|
+
monkeypatch.setenv(CONTROL_TOKEN_ENV, "s3cret")
|
|
81
|
+
RAN.clear()
|
|
82
|
+
|
|
83
|
+
with anyio.fail_after(30):
|
|
84
|
+
async with _served() as (rest, mcp):
|
|
85
|
+
without = await _calls(rest, {})
|
|
86
|
+
wrong = await _calls(rest, client.control_headers("guess"))
|
|
87
|
+
refusal = (await rest.post("/agentenv/ext/test-exec/run", json={"argv": ["ls"]})).json()
|
|
88
|
+
ran_before = list(RAN)
|
|
89
|
+
right = await _calls(rest, client.control_headers("s3cret"))
|
|
90
|
+
card = await rest.get(WELL_KNOWN_PATH)
|
|
91
|
+
tools = [t.name for t in (await mcp.list_tools()).tools]
|
|
92
|
+
|
|
93
|
+
assert without == wrong == [401, 401, 401] and ran_before == []
|
|
94
|
+
assert refusal["error"]["code"] == "unauthorized" and refusal["error"]["applied"] == "no"
|
|
95
|
+
assert right == [200, 200, 200] and RAN == ["data/add", "run"]
|
|
96
|
+
assert card.status_code == 200 and tools == ["read"]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_without_a_token_every_route_is_open(monkeypatch):
|
|
101
|
+
monkeypatch.delenv(CONTROL_TOKEN_ENV, raising=False)
|
|
102
|
+
|
|
103
|
+
with anyio.fail_after(30):
|
|
104
|
+
async with _served() as (rest, _):
|
|
105
|
+
assert await _calls(rest, {}) == [200, 200, 200]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_a_call_sends_its_own_headers_or_else_the_ones_its_grant_gives_an_address_under_the_envs():
|
|
109
|
+
env = "http://desk:18765"
|
|
110
|
+
|
|
111
|
+
with client.authorized({env: client.control_headers("s3cret"), "http://other:1": {},
|
|
112
|
+
None: client.control_headers("orphan")}):
|
|
113
|
+
granted = [client.request_headers(url) for url in (env, f"{env}/svc/mail/agentenv", f"{env}0/x",
|
|
114
|
+
"http://other:1/x")]
|
|
115
|
+
own = client.request_headers(f"{env}/agentenv", {"X-Mine": "1"})
|
|
116
|
+
with client.authorized({env: client.control_headers("rotated")}):
|
|
117
|
+
inner = client.request_headers(f"{env}/agentenv")
|
|
118
|
+
outer = client.request_headers(f"{env}/agentenv")
|
|
119
|
+
after = client.request_headers(f"{env}/agentenv")
|
|
120
|
+
|
|
121
|
+
bearer = {"Authorization": "Bearer s3cret"}
|
|
122
|
+
assert granted == [bearer, bearer, {}, {}] and own == {"X-Mine": "1"}
|
|
123
|
+
assert inner == {"Authorization": "Bearer rotated"} and outer == bearer and after == {}
|
|
124
|
+
assert client.control_headers(None) == {} and client.control_headers("") == {}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_invoke_extension_and_the_data_plane_send_the_token(monkeypatch):
|
|
129
|
+
seen = []
|
|
130
|
+
|
|
131
|
+
def handle(request: httpx.Request) -> httpx.Response:
|
|
132
|
+
seen.append((request.url.path, request.headers.get("authorization")))
|
|
133
|
+
return httpx.Response(200, json={"jsonrpc": "2.0", "id": 1, "result": {"parts": []}})
|
|
134
|
+
|
|
135
|
+
real = httpx.AsyncClient
|
|
136
|
+
monkeypatch.setattr(httpx, "AsyncClient", lambda *a, **k: real(transport=httpx.MockTransport(handle)))
|
|
137
|
+
card = {"capabilities": {"extensions": [{"uri": "urn:agentenv:test-exec/v1", "params": {
|
|
138
|
+
"methods": {"run": {"endpoint": "/agentenv/ext/test-exec/run"}}}}]}}
|
|
139
|
+
|
|
140
|
+
await client.invoke_extension("http://desk", card, "urn:agentenv:test-exec/v1", {}, method="run",
|
|
141
|
+
headers=client.control_headers("own"))
|
|
142
|
+
await client.get_data("http://desk", headers=client.control_headers("own"))
|
|
143
|
+
with client.authorized({"http://desk": client.control_headers("granted")}):
|
|
144
|
+
await client.add_data("http://desk", [])
|
|
145
|
+
await client.reset_data("http://desk", headers={})
|
|
146
|
+
|
|
147
|
+
assert seen == [("/agentenv/ext/test-exec/run", "Bearer own"), ("/agentenv", "Bearer own"),
|
|
148
|
+
("/agentenv", "Bearer granted"), ("/agentenv", None)]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/THIRD_PARTY_NOTICES.md
RENAMED
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/examples/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/examples/basic_agent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/computer_fake.py
RENAMED
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/probe_fake.py
RENAMED
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_a2a_agent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_a2a_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_errors.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_intake.py
RENAMED
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_manifest.py
RENAMED
|
File without changes
|
{agentenv_framework_protocol-0.1.420 → agentenv_framework_protocol-0.1.422}/tests/test_preflight.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|