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.
Files changed (28) hide show
  1. {agentvalet-0.2.2 → agentvalet-0.3.0}/PKG-INFO +14 -1
  2. {agentvalet-0.2.2 → agentvalet-0.3.0}/README.md +13 -0
  3. {agentvalet-0.2.2 → agentvalet-0.3.0}/pyproject.toml +1 -1
  4. {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/__init__.py +1 -1
  5. {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/_constants.py +7 -1
  6. {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/aio.py +11 -3
  7. {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/client.py +11 -3
  8. agentvalet-0.3.0/tests/test_action_timeout.py +71 -0
  9. {agentvalet-0.2.2 → agentvalet-0.3.0}/.gitignore +0 -0
  10. {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/01_quickstart.py +0 -0
  11. {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/02_approvals.py +0 -0
  12. {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/03_langchain_tools.py +0 -0
  13. {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/04_support_triage_agent.py +0 -0
  14. {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/05_langgraph_subagents.py +0 -0
  15. {agentvalet-0.2.2 → agentvalet-0.3.0}/examples/README.md +0 -0
  16. {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/_protocol.py +0 -0
  17. {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/_signing.py +0 -0
  18. {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/child.py +0 -0
  19. {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/env.py +0 -0
  20. {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/errors.py +0 -0
  21. {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/py.typed +0 -0
  22. {agentvalet-0.2.2 → agentvalet-0.3.0}/src/agentvalet/register.py +0 -0
  23. {agentvalet-0.2.2 → agentvalet-0.3.0}/tests/conftest.py +0 -0
  24. {agentvalet-0.2.2 → agentvalet-0.3.0}/tests/test_bearer_identity.py +0 -0
  25. {agentvalet-0.2.2 → agentvalet-0.3.0}/tests/test_client.py +0 -0
  26. {agentvalet-0.2.2 → agentvalet-0.3.0}/tests/test_constants.py +0 -0
  27. {agentvalet-0.2.2 → agentvalet-0.3.0}/tests/test_env.py +0 -0
  28. {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.2.2
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.2.2"
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"
@@ -37,7 +37,7 @@ from .errors import (
37
37
  UpstreamError,
38
38
  )
39
39
 
40
- __version__ = "0.2.2"
40
+ __version__ = "0.3.0"
41
41
 
42
42
  __all__ = [
43
43
  "DEFAULT_PROXY_URL",
@@ -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
- #: Per-request network timeout.
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("POST", "/v1/actions", json=body)
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(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
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=self._timeout_s,
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("POST", "/v1/actions", json=body)
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(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
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=self._timeout_s,
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