File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -288,7 +288,7 @@ This agent CANNOT call:
288288 ✗ stripe.charge
289289 ✗ delete_user
290290
291- If the agent tries to call a blocked tool, the API will return a 403 .
291+ If the agent tries to call a blocked tool via `ai.run_tool`, you'll get `allowed=False` with a `policy_reason` .
292292```
293293
294294** When to use:**
@@ -917,7 +917,7 @@ max_spend_usd_per_day = 500.00
917917## 📚 Additional Resources
918918
919919- ** Main README:** [ ` ../README.md ` ] ( ../README.md )
920- - ** API Docs:** https://docs.onceonly.tech/api
920+ - ** API Docs:** https://docs.onceonly.tech/reference/idempotency/
921921- ** SDK Reference:** https://docs.onceonly.tech/sdk/python
922922- ** Dashboard:** https://onceonly.tech/dashboard
923923- ** Governance Guide:** https://docs.onceonly.tech/governance
@@ -932,7 +932,7 @@ max_spend_usd_per_day = 500.00
932932| 200 | — | Success | ✅ Proceed |
933933| 401 | ` UnauthorizedError ` | Invalid API key | Check ` ONCEONLY_API_KEY ` |
934934| 402 | ` OverLimitError ` | Usage/budget limit | Upgrade plan or increase budget |
935- | 403 | ` PolicyBlockedError ` | Agent blocked by policy | Check governance logs |
935+ | 403 | ` ApiError ` | Forbidden / feature gating | Check plan + ` error=feature_not_available ` |
936936| 422 | ` ValidationError ` | Bad request | Fix parameters |
937937| 429 | ` RateLimitError ` | Rate limit | Enable retries |
938938| 500+ | ` ApiError ` | Server error | Retry or fail-open |
Original file line number Diff line number Diff line change 1212import os
1313import random
1414import time
15- import requests
15+ import httpx
1616
1717LLM_API_KEY = os .getenv ("LLM_API_KEY" )
1818TOOL_ENDPOINT = os .getenv ("TOOL_ENDPOINT" , "https://example.com/tools/charge" )
@@ -23,9 +23,10 @@ def llm_decide() -> dict:
2323
2424def call_tool (payload : dict ) -> dict :
2525 # No idempotency key. A retry repeats the charge.
26- resp = requests .post (TOOL_ENDPOINT , json = payload , timeout = 10 )
27- resp .raise_for_status ()
28- return resp .json ()
26+ with httpx .Client (timeout = 10.0 ) as c :
27+ resp = c .post (TOOL_ENDPOINT , json = payload )
28+ resp .raise_for_status ()
29+ return resp .json ()
2930
3031def main () -> None :
3132 decision = llm_decide ()
Original file line number Diff line number Diff line change 4747st1 = client .gov .disable_agent (agent_id , reason = "Manual safety stop (example)" )
4848print ("Status after disable:" , st1 )
4949
50- print ("Agent disabled. All tool calls should now return 403 until enabled." )
50+ print ("Agent disabled. Tool calls should now be blocked (ai.run_tool -> allowed=False) until enabled." )
5151
5252print ("Re-enabling agent..." )
5353st2 = client .gov .enable_agent (agent_id , reason = "Resume operations (example)" )
Original file line number Diff line number Diff line change 2727print (" ✗ stripe.charge" )
2828print (" ✗ delete_user" )
2929
30- print ("\n If the agent tries to call a blocked tool, the API will return a 403 ." )
30+ print ("\n If the agent tries to call a blocked tool via ai.run_tool(), you'll get allowed=False with a policy_reason ." )
3131
3232# Optional: execute a tool (requires the tool to be registered in your account)
3333# res = client.ai.run_tool(
Original file line number Diff line number Diff line change @@ -42,7 +42,18 @@ def _parse_retry_after(resp: httpx.Response) -> Optional[float]:
4242
4343def parse_json_or_raise (resp : httpx .Response ) -> Dict [str , Any ]:
4444 # typed errors
45- if resp .status_code in (401 , 403 ):
45+ if resp .status_code == 401 :
46+ raise UnauthorizedError (error_text (resp , "Invalid API Key (Unauthorized)." ))
47+
48+ if resp .status_code == 403 :
49+ d = try_extract_detail (resp )
50+ # Backend uses 403 both for invalid/disabled API keys and for feature gating.
51+ if isinstance (d , dict ) and d .get ("error" ) == "feature_not_available" :
52+ raise ApiError (
53+ error_text (resp , "Feature not available for this plan." ),
54+ status_code = 403 ,
55+ detail = d ,
56+ )
4657 raise UnauthorizedError (error_text (resp , "Invalid API Key (Unauthorized)." ))
4758
4859 if resp .status_code == 402 :
Original file line number Diff line number Diff line change 11from __future__ import annotations
22
33import logging
4+ from urllib .parse import urlparse
45from typing import Optional , Dict , Any
56
67import httpx
@@ -52,7 +53,16 @@ def __init__(
5253 async_transport : Optional [httpx .AsyncBaseTransport ] = None ,
5354 ):
5455 self .api_key = api_key
55- self .base_url = base_url .rstrip ("/" )
56+ base_url = base_url .rstrip ("/" )
57+ # Accept both "https://api.onceonly.tech" and ".../v1" (and keep custom paths intact).
58+ try :
59+ p = urlparse (base_url )
60+ if (p .path or "" ) in ("" , "/" ):
61+ base_url = base_url + "/v1"
62+ except Exception :
63+ pass
64+
65+ self .base_url = base_url
5666 self .timeout = timeout
5767 self .fail_open = fail_open
5868
You can’t perform that action at this time.
0 commit comments