Skip to content

Commit 17dc9fa

Browse files
Release 3.0.1
1 parent 197bb6e commit 17dc9fa

6 files changed

Lines changed: 33 additions & 11 deletions

File tree

examples/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff 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 |

examples/ai/agent_full_flow_no_onceonly.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import os
1313
import random
1414
import time
15-
import requests
15+
import httpx
1616

1717
LLM_API_KEY = os.getenv("LLM_API_KEY")
1818
TOOL_ENDPOINT = os.getenv("TOOL_ENDPOINT", "https://example.com/tools/charge")
@@ -23,9 +23,10 @@ def llm_decide() -> dict:
2323

2424
def 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

3031
def main() -> None:
3132
decision = llm_decide()

examples/ai/governance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
st1 = client.gov.disable_agent(agent_id, reason="Manual safety stop (example)")
4848
print("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

5252
print("Re-enabling agent...")
5353
st2 = client.gov.enable_agent(agent_id, reason="Resume operations (example)")

examples/ai/tool_permissions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
print(" ✗ stripe.charge")
2828
print(" ✗ delete_user")
2929

30-
print("\nIf the agent tries to call a blocked tool, the API will return a 403.")
30+
print("\nIf 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(

onceonly/_http.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,18 @@ def _parse_retry_after(resp: httpx.Response) -> Optional[float]:
4242

4343
def 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:

onceonly/client.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4+
from urllib.parse import urlparse
45
from typing import Optional, Dict, Any
56

67
import 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

0 commit comments

Comments
 (0)