agentvalet 0.2.2__tar.gz → 0.3.0__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.
- {agentvalet-0.2.2 → agentvalet-0.3.0}/PKG-INFO +14 -1
- {agentvalet-0.2.2 → agentvalet-0.3.0}/README.md +13 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/pyproject.toml +1 -1
- {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/__init__.py +1 -1
- {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/_constants.py +7 -1
- {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/aio.py +11 -3
- {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/client.py +11 -3
- agentvalet-0.3.0/tests/test_action_timeout.py +71 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/.gitignore +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/01_quickstart.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/02_approvals.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/03_langchain_tools.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/04_support_triage_agent.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/05_langgraph_subagents.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/README.md +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/_protocol.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/_signing.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/child.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/env.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/errors.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/py.typed +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/register.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/tests/conftest.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/tests/test_bearer_identity.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/tests/test_client.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/tests/test_constants.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/tests/test_env.py +0 -0
- {agentvalet-0.2.2 → agentvalet-0.3.0}/tests/test_register.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentvalet
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Call approved SaaS platforms from any Python agent. Your agent never holds the downstream credential.
|
|
5
5
|
Project-URL: Homepage, https://agentvalet.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.agentvalet.ai
|
|
@@ -164,6 +164,19 @@ result = av.wait_for_approval(approval_id)
|
|
|
164
164
|
|
|
165
165
|
Pass `approval_timeout_s=0` if you never want to block.
|
|
166
166
|
|
|
167
|
+
### Network timeouts
|
|
168
|
+
|
|
169
|
+
`call()` waits up to `action_timeout_s` (default 130s) for the action request,
|
|
170
|
+
because it waits on the platform itself and some tools run for a minute or
|
|
171
|
+
more. Every other request uses `timeout_s` (default 15s). Waiting on an owner
|
|
172
|
+
approval is separate and set by `approval_timeout_s`. AgentValet's edge ends any
|
|
173
|
+
single response at 100s, so for a tool that runs longer, use its
|
|
174
|
+
start-and-poll variant if it has one.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
av = AgentValet.from_env(action_timeout_s=60, timeout_s=10)
|
|
178
|
+
```
|
|
179
|
+
|
|
167
180
|
## Errors you can branch on
|
|
168
181
|
|
|
169
182
|
Every exception is typed, so you never string-match an error envelope:
|
|
@@ -132,6 +132,19 @@ result = av.wait_for_approval(approval_id)
|
|
|
132
132
|
|
|
133
133
|
Pass `approval_timeout_s=0` if you never want to block.
|
|
134
134
|
|
|
135
|
+
### Network timeouts
|
|
136
|
+
|
|
137
|
+
`call()` waits up to `action_timeout_s` (default 130s) for the action request,
|
|
138
|
+
because it waits on the platform itself and some tools run for a minute or
|
|
139
|
+
more. Every other request uses `timeout_s` (default 15s). Waiting on an owner
|
|
140
|
+
approval is separate and set by `approval_timeout_s`. AgentValet's edge ends any
|
|
141
|
+
single response at 100s, so for a tool that runs longer, use its
|
|
142
|
+
start-and-poll variant if it has one.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
av = AgentValet.from_env(action_timeout_s=60, timeout_s=10)
|
|
146
|
+
```
|
|
147
|
+
|
|
135
148
|
## Errors you can branch on
|
|
136
149
|
|
|
137
150
|
Every exception is typed, so you never string-match an error envelope:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentvalet"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Call approved SaaS platforms from any Python agent. Your agent never holds the downstream credential."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -21,9 +21,15 @@ APPROVAL_TIMEOUT_S = 50.0
|
|
|
21
21
|
#: How often we ask the proxy whether the owner has decided.
|
|
22
22
|
APPROVAL_POLL_S = 2.0
|
|
23
23
|
|
|
24
|
-
#:
|
|
24
|
+
#: Network timeout for every request except the action call.
|
|
25
25
|
REQUEST_TIMEOUT_S = 15.0
|
|
26
26
|
|
|
27
|
+
#: Network timeout for ``POST /v1/actions`` only. That call waits on the
|
|
28
|
+
#: upstream platform, and the proxy gives a slow MCP tool up to its platform's
|
|
29
|
+
#: ``mcp_call_timeout_ms`` (up to 120s today). Matches the Node SDK's
|
|
30
|
+
#: ``actionTimeoutMs`` default and the mcp-server's ACTION_REQUEST_TIMEOUT_MS.
|
|
31
|
+
ACTION_TIMEOUT_S = 130.0
|
|
32
|
+
|
|
27
33
|
#: Agent assertions are deliberately short-lived; the proxy rejects long-dated
|
|
28
34
|
#: ones. Seconds.
|
|
29
35
|
JWT_LIFETIME_S = 60
|
|
@@ -16,6 +16,7 @@ from typing import Any, Callable, Union
|
|
|
16
16
|
import httpx
|
|
17
17
|
|
|
18
18
|
from ._constants import (
|
|
19
|
+
ACTION_TIMEOUT_S,
|
|
19
20
|
APPROVAL_POLL_S,
|
|
20
21
|
APPROVAL_TIMEOUT_S,
|
|
21
22
|
DEFAULT_PROXY_URL,
|
|
@@ -61,6 +62,7 @@ class AsyncAgentValet:
|
|
|
61
62
|
session_id: str | None = None,
|
|
62
63
|
*,
|
|
63
64
|
bearer_token: str | None = None,
|
|
65
|
+
action_timeout_s: float = ACTION_TIMEOUT_S,
|
|
64
66
|
) -> None:
|
|
65
67
|
self._bearer_token = bearer_token
|
|
66
68
|
if bearer_token is None:
|
|
@@ -77,6 +79,7 @@ class AsyncAgentValet:
|
|
|
77
79
|
self._private_key = private_key or ""
|
|
78
80
|
self.session_id = validate_session_id(session_id)
|
|
79
81
|
self._timeout_s = timeout_s
|
|
82
|
+
self._action_timeout_s = action_timeout_s
|
|
80
83
|
self._approval_timeout_s = approval_timeout_s
|
|
81
84
|
self._approval_poll_s = approval_poll_s
|
|
82
85
|
self._on_approval_pending = on_approval_pending
|
|
@@ -145,7 +148,9 @@ class AsyncAgentValet:
|
|
|
145
148
|
connection_id=connection_id,
|
|
146
149
|
reason=reason,
|
|
147
150
|
)
|
|
148
|
-
response = await self._authed_request(
|
|
151
|
+
response = await self._authed_request(
|
|
152
|
+
"POST", "/v1/actions", timeout_s=self._action_timeout_s, json=body
|
|
153
|
+
)
|
|
149
154
|
text = response.text
|
|
150
155
|
|
|
151
156
|
if response.status_code == 202:
|
|
@@ -296,8 +301,11 @@ class AsyncAgentValet:
|
|
|
296
301
|
headers["Authorization"] = f"Bearer {self.sign_jwt()}"
|
|
297
302
|
return headers
|
|
298
303
|
|
|
299
|
-
async def _authed_request(
|
|
304
|
+
async def _authed_request(
|
|
305
|
+
self, method: str, path: str, *, timeout_s: float | None = None, **kwargs: Any
|
|
306
|
+
) -> httpx.Response:
|
|
300
307
|
url = f"{self.proxy_url}{path}"
|
|
308
|
+
timeout = self._timeout_s if timeout_s is None else timeout_s
|
|
301
309
|
|
|
302
310
|
async def once() -> httpx.Response:
|
|
303
311
|
try:
|
|
@@ -305,7 +313,7 @@ class AsyncAgentValet:
|
|
|
305
313
|
method,
|
|
306
314
|
url,
|
|
307
315
|
headers=self._auth_headers(),
|
|
308
|
-
timeout=
|
|
316
|
+
timeout=timeout,
|
|
309
317
|
**kwargs,
|
|
310
318
|
)
|
|
311
319
|
except httpx.HTTPError as err:
|
|
@@ -12,6 +12,7 @@ from typing import Any, Callable
|
|
|
12
12
|
import httpx
|
|
13
13
|
|
|
14
14
|
from ._constants import (
|
|
15
|
+
ACTION_TIMEOUT_S,
|
|
15
16
|
APPROVAL_POLL_S,
|
|
16
17
|
APPROVAL_TIMEOUT_S,
|
|
17
18
|
DEFAULT_PROXY_URL,
|
|
@@ -63,6 +64,7 @@ class AgentValet:
|
|
|
63
64
|
session_id: str | None = None,
|
|
64
65
|
*,
|
|
65
66
|
bearer_token: str | None = None,
|
|
67
|
+
action_timeout_s: float = ACTION_TIMEOUT_S,
|
|
66
68
|
) -> None:
|
|
67
69
|
self._bearer_token = bearer_token
|
|
68
70
|
if bearer_token is None:
|
|
@@ -79,6 +81,7 @@ class AgentValet:
|
|
|
79
81
|
self._private_key = private_key or ""
|
|
80
82
|
self.session_id = validate_session_id(session_id)
|
|
81
83
|
self._timeout_s = timeout_s
|
|
84
|
+
self._action_timeout_s = action_timeout_s
|
|
82
85
|
self._approval_timeout_s = approval_timeout_s
|
|
83
86
|
self._approval_poll_s = approval_poll_s
|
|
84
87
|
self._on_approval_pending = on_approval_pending
|
|
@@ -181,7 +184,9 @@ class AgentValet:
|
|
|
181
184
|
connection_id=connection_id,
|
|
182
185
|
reason=reason,
|
|
183
186
|
)
|
|
184
|
-
response = self._authed_request(
|
|
187
|
+
response = self._authed_request(
|
|
188
|
+
"POST", "/v1/actions", timeout_s=self._action_timeout_s, json=body
|
|
189
|
+
)
|
|
185
190
|
text = response.text
|
|
186
191
|
|
|
187
192
|
if response.status_code == 202:
|
|
@@ -379,8 +384,11 @@ class AgentValet:
|
|
|
379
384
|
headers["Authorization"] = f"Bearer {self.sign_jwt()}"
|
|
380
385
|
return headers
|
|
381
386
|
|
|
382
|
-
def _authed_request(
|
|
387
|
+
def _authed_request(
|
|
388
|
+
self, method: str, path: str, *, timeout_s: float | None = None, **kwargs: Any
|
|
389
|
+
) -> httpx.Response:
|
|
383
390
|
url = f"{self.proxy_url}{path}"
|
|
391
|
+
timeout = self._timeout_s if timeout_s is None else timeout_s
|
|
384
392
|
|
|
385
393
|
def once() -> httpx.Response:
|
|
386
394
|
try:
|
|
@@ -388,7 +396,7 @@ class AgentValet:
|
|
|
388
396
|
method,
|
|
389
397
|
url,
|
|
390
398
|
headers=self._auth_headers(),
|
|
391
|
-
timeout=
|
|
399
|
+
timeout=timeout,
|
|
392
400
|
**kwargs,
|
|
393
401
|
)
|
|
394
402
|
except httpx.HTTPError as err:
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""``call()``'s ``POST /v1/actions`` waits up to ``action_timeout_s`` (130s);
|
|
2
|
+
every other request keeps ``timeout_s`` (15s).
|
|
3
|
+
|
|
4
|
+
The proxy gives a slow MCP tool up to its platform's ``mcp_call_timeout_ms``
|
|
5
|
+
(up to 120s today), so a 15s action timeout gave up on calls the proxy would
|
|
6
|
+
have completed. Each test runs against both the sync and async client.
|
|
7
|
+
The timeout asserted is the one httpx actually attached to the request.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import pytest
|
|
12
|
+
import respx
|
|
13
|
+
from conftest import PROXY, invoke
|
|
14
|
+
|
|
15
|
+
from agentvalet import AgentValet, NetworkError
|
|
16
|
+
from agentvalet import _constants as c
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_timeout(request: httpx.Request) -> float:
|
|
20
|
+
return request.extensions["timeout"]["read"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_constants():
|
|
24
|
+
assert c.ACTION_TIMEOUT_S == 130.0
|
|
25
|
+
assert c.REQUEST_TIMEOUT_S == 15.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@respx.mock
|
|
29
|
+
async def test_call_uses_the_action_timeout(client):
|
|
30
|
+
route = respx.post(f"{PROXY}/v1/actions").mock(return_value=httpx.Response(200, json={"ok": True}))
|
|
31
|
+
await invoke(client, "call", "mcp-treg", "check", "mcp-treg:check", method="POST")
|
|
32
|
+
assert read_timeout(route.calls.last.request) == 130.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@respx.mock
|
|
36
|
+
async def test_other_requests_keep_the_request_timeout(client):
|
|
37
|
+
route = respx.get(f"{PROXY}/v1/agent/permissions").mock(return_value=httpx.Response(200, json={"platforms": []}))
|
|
38
|
+
await invoke(client, "list_platforms")
|
|
39
|
+
assert read_timeout(route.calls.last.request) == 15.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@respx.mock
|
|
43
|
+
async def test_the_two_timeouts_are_independent(make_client):
|
|
44
|
+
client = make_client(action_timeout_s=40.0, timeout_s=5.0)
|
|
45
|
+
actions = respx.post(f"{PROXY}/v1/actions").mock(return_value=httpx.Response(200, json={}))
|
|
46
|
+
perms = respx.get(f"{PROXY}/v1/agent/permissions").mock(return_value=httpx.Response(200, json={}))
|
|
47
|
+
await invoke(client, "call", "p", "/x", "s")
|
|
48
|
+
await invoke(client, "list_platforms")
|
|
49
|
+
assert read_timeout(actions.calls.last.request) == 40.0
|
|
50
|
+
assert read_timeout(perms.calls.last.request) == 5.0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@respx.mock
|
|
54
|
+
async def test_the_401_retry_keeps_the_action_timeout(client):
|
|
55
|
+
route = respx.post(f"{PROXY}/v1/actions").mock(
|
|
56
|
+
side_effect=[httpx.Response(401, json={"error": "skew"}), httpx.Response(200, json={})]
|
|
57
|
+
)
|
|
58
|
+
await invoke(client, "call", "p", "/x", "s")
|
|
59
|
+
assert [read_timeout(call.request) for call in route.calls] == [130.0, 130.0]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@respx.mock
|
|
63
|
+
async def test_an_action_timeout_is_a_network_error(client):
|
|
64
|
+
respx.post(f"{PROXY}/v1/actions").mock(side_effect=httpx.ReadTimeout("timed out"))
|
|
65
|
+
with pytest.raises(NetworkError):
|
|
66
|
+
await invoke(client, "call", "p", "/x", "s")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_from_bearer_accepts_an_action_timeout():
|
|
70
|
+
child = AgentValet.from_bearer("h.e30.s", action_timeout_s=90.0)
|
|
71
|
+
assert child._action_timeout_s == 90.0
|
|
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
|