oneclaw 0.1.0__py3-none-any.whl

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.
oneclaw/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """Official Python SDK for the 1Claw secrets management platform."""
2
+
3
+ from oneclaw.client import OneclawClient, create_client
4
+ from oneclaw.errors import (
5
+ ApprovalRequiredError,
6
+ AuthError,
7
+ NotFoundError,
8
+ OneclawError,
9
+ PaymentRequiredError,
10
+ RateLimitError,
11
+ ResourceLimitExceededError,
12
+ ServerError,
13
+ ValidationError,
14
+ )
15
+ from oneclaw.types import OneclawClientConfig, OneclawResponse
16
+
17
+ __version__ = "0.1.0"
18
+
19
+ __all__ = [
20
+ "OneclawClient",
21
+ "create_client",
22
+ "OneclawClientConfig",
23
+ "OneclawResponse",
24
+ "OneclawError",
25
+ "AuthError",
26
+ "NotFoundError",
27
+ "PaymentRequiredError",
28
+ "RateLimitError",
29
+ "ResourceLimitExceededError",
30
+ "ValidationError",
31
+ "ServerError",
32
+ "ApprovalRequiredError",
33
+ ]
oneclaw/client.py ADDED
@@ -0,0 +1,148 @@
1
+ """Main client for the 1Claw Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from oneclaw.http_client import HttpClient
6
+ from oneclaw.resources.agents import AgentsResource
7
+ from oneclaw.resources.api_keys import ApiKeysResource
8
+ from oneclaw.resources.approvals import ApprovalsResource
9
+ from oneclaw.resources.audit import AuditResource
10
+ from oneclaw.resources.auth import AuthResource
11
+ from oneclaw.resources.billing import BillingResource
12
+ from oneclaw.resources.chains import ChainsResource
13
+ from oneclaw.resources.org import OrgResource
14
+ from oneclaw.resources.platform import PlatformResource
15
+ from oneclaw.resources.policies import PoliciesResource
16
+ from oneclaw.resources.risk import RiskResource
17
+ from oneclaw.resources.secrets import SecretsResource
18
+ from oneclaw.resources.sharing import SharingResource
19
+ from oneclaw.resources.signing_keys import SigningKeysResource
20
+ from oneclaw.resources.treasury import TreasuryResource
21
+ from oneclaw.resources.treasury_wallets import TreasuryWalletsResource
22
+ from oneclaw.resources.vaults import VaultResource
23
+ from oneclaw.resources.webhooks import WebhooksResource
24
+ from oneclaw.types import OneclawClientConfig
25
+
26
+
27
+ class OneclawClient:
28
+ """The main 1Claw SDK client.
29
+
30
+ All API resources are exposed as namespaced attributes for
31
+ discoverability.
32
+
33
+ Examples
34
+ --------
35
+ >>> from oneclaw import create_client
36
+ >>>
37
+ >>> # Agent with API key (auto-exchanges for JWT)
38
+ >>> client = create_client(api_key="ocv_...")
39
+ >>>
40
+ >>> # Pre-authenticated with JWT
41
+ >>> client = create_client(token="eyJ...")
42
+ >>>
43
+ >>> # Use resources
44
+ >>> vaults = client.vaults.list()
45
+ >>> secret = client.secrets.get(vault_id, "my-key")
46
+ """
47
+
48
+ def __init__(self, config: OneclawClientConfig) -> None:
49
+ self._http = HttpClient(config)
50
+
51
+ if (
52
+ config.api_key
53
+ and not config.token
54
+ and not config.agent_id
55
+ and not config.api_key.startswith("ocv_")
56
+ ):
57
+ self._auto_authenticate_user_key(config.api_key)
58
+
59
+ self.auth = AuthResource(self._http)
60
+ self.vaults = VaultResource(self._http)
61
+ self.secrets = SecretsResource(self._http)
62
+ self.agents = AgentsResource(self._http)
63
+ self.policies = PoliciesResource(self._http)
64
+ self.chains = ChainsResource(self._http)
65
+ self.sharing = SharingResource(self._http)
66
+ self.billing = BillingResource(self._http)
67
+ self.audit = AuditResource(self._http)
68
+ self.org = OrgResource(self._http)
69
+ self.api_keys = ApiKeysResource(self._http)
70
+ self.signing_keys = SigningKeysResource(self._http)
71
+ self.treasury = TreasuryResource(self._http)
72
+ self.treasury_wallets = TreasuryWalletsResource(self._http)
73
+ self.platform = PlatformResource(self._http)
74
+ self.approvals = ApprovalsResource(self._http)
75
+ self.webhooks = WebhooksResource(self._http)
76
+ self.risk = RiskResource(self._http)
77
+
78
+ def _auto_authenticate_user_key(self, api_key: str) -> None:
79
+ """Exchange a ``1ck_`` user API key for a JWT (fire-and-forget)."""
80
+ try:
81
+ resp = self._http.request(
82
+ "POST", "/v1/auth/api-key-token",
83
+ body={"api_key": api_key},
84
+ skip_auth=True,
85
+ )
86
+ if resp.data and resp.data.get("access_token"):
87
+ self._http.set_token(resp.data["access_token"])
88
+ except Exception:
89
+ pass
90
+
91
+ @property
92
+ def resolved_agent_id(self) -> str | None:
93
+ """Agent ID resolved from the token exchange (for key-only auth)."""
94
+ return self._http.resolved_agent_id
95
+
96
+ def close(self) -> None:
97
+ """Close the underlying HTTP connection pool."""
98
+ self._http.close()
99
+
100
+ def __enter__(self) -> OneclawClient:
101
+ return self
102
+
103
+ def __exit__(self, *args: object) -> None:
104
+ self.close()
105
+
106
+
107
+ def create_client(
108
+ *,
109
+ base_url: str = "https://api.1claw.xyz",
110
+ token: str | None = None,
111
+ api_key: str | None = None,
112
+ agent_id: str | None = None,
113
+ timeout: float = 30.0,
114
+ ) -> OneclawClient:
115
+ """Create a new 1Claw SDK client.
116
+
117
+ Parameters
118
+ ----------
119
+ base_url : str
120
+ The 1Claw API base URL (default: ``https://api.1claw.xyz``).
121
+ token : str, optional
122
+ A pre-existing JWT to use for authentication.
123
+ api_key : str, optional
124
+ An API key (``ocv_`` for agents, ``1ck_`` for users).
125
+ Agent keys auto-exchange for JWTs and refresh before expiry.
126
+ agent_id : str, optional
127
+ The agent UUID (optional with ``ocv_`` keys — auto-discovered).
128
+ timeout : float
129
+ HTTP request timeout in seconds (default: 30).
130
+
131
+ Returns
132
+ -------
133
+ OneclawClient
134
+ A fully configured client instance.
135
+
136
+ Examples
137
+ --------
138
+ >>> client = create_client(api_key="ocv_abc123")
139
+ >>> vaults = client.vaults.list()
140
+ """
141
+ config = OneclawClientConfig(
142
+ base_url=base_url,
143
+ token=token,
144
+ api_key=api_key,
145
+ agent_id=agent_id,
146
+ timeout=timeout,
147
+ )
148
+ return OneclawClient(config)
oneclaw/errors.py ADDED
@@ -0,0 +1,124 @@
1
+ """Error types for the 1Claw Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ from typing import Any
7
+
8
+
9
+ class OneclawError(Exception):
10
+ """Base error for all 1Claw SDK errors."""
11
+
12
+ def __init__(
13
+ self,
14
+ message: str,
15
+ status: int,
16
+ error_type: str = "unknown",
17
+ detail: str | None = None,
18
+ ) -> None:
19
+ super().__init__(message)
20
+ self.status = status
21
+ self.error_type = error_type
22
+ self.detail = detail
23
+
24
+
25
+ class AuthError(OneclawError):
26
+ """Raised on 401 Unauthorized or 403 Forbidden responses."""
27
+
28
+ def __init__(self, message: str, status: int = 401) -> None:
29
+ super().__init__(message, status, "auth_error")
30
+
31
+
32
+ class ResourceLimitExceededError(OneclawError):
33
+ """Raised when a resource limit is exceeded (403 with type resource_limit_exceeded)."""
34
+
35
+ def __init__(self, message: str) -> None:
36
+ super().__init__(message, 403, "resource_limit_exceeded")
37
+
38
+
39
+ class PaymentRequiredError(OneclawError):
40
+ """Raised on 402 Payment Required responses."""
41
+
42
+ def __init__(self, message: str, requirement: dict[str, Any] | None = None) -> None:
43
+ super().__init__(message, 402, "payment_required")
44
+ self.payment_requirement = requirement
45
+
46
+
47
+ class ApprovalRequiredError(OneclawError):
48
+ """Raised when human approval is needed to access a resource."""
49
+
50
+ def __init__(self, approval_request_id: str, message: str | None = None) -> None:
51
+ super().__init__(
52
+ message or "Human approval is required to access this secret",
53
+ 403,
54
+ "approval_required",
55
+ )
56
+ self.approval_request_id = approval_request_id
57
+
58
+
59
+ class NotFoundError(OneclawError):
60
+ """Raised on 404 Not Found responses."""
61
+
62
+ def __init__(self, message: str = "Resource not found") -> None:
63
+ super().__init__(message, 404, "not_found")
64
+
65
+
66
+ class RateLimitError(OneclawError):
67
+ """Raised on 429 Too Many Requests. Includes retry timing when available."""
68
+
69
+ def __init__(
70
+ self,
71
+ message: str = "Rate limit exceeded",
72
+ retry_after_ms: int | None = None,
73
+ ) -> None:
74
+ super().__init__(message, 429, "rate_limit")
75
+ self.retry_after_ms = retry_after_ms
76
+
77
+
78
+ class ValidationError(OneclawError):
79
+ """Raised on 400 Bad Request responses for validation failures."""
80
+
81
+ def __init__(self, message: str, fields: dict[str, str] | None = None) -> None:
82
+ super().__init__(message, 400, "validation_error")
83
+ self.fields = fields
84
+
85
+
86
+ class ServerError(OneclawError):
87
+ """Raised on 500+ server-side errors."""
88
+
89
+ def __init__(self, message: str = "Internal server error", status: int = 500) -> None:
90
+ super().__init__(message, status, "server_error")
91
+
92
+
93
+ def error_from_response(
94
+ status: int,
95
+ body: dict[str, Any],
96
+ headers: dict[str, str] | None = None,
97
+ ) -> OneclawError:
98
+ """Parse an HTTP response into the appropriate typed error."""
99
+ message = body.get("detail") or body.get("message") or body.get("error") or f"HTTP {status}"
100
+
101
+ if status == 400:
102
+ return ValidationError(message, body.get("fields"))
103
+ if status == 401:
104
+ return AuthError(message, 401)
105
+ if status == 403:
106
+ error_type = body.get("type", "")
107
+ if error_type == "resource_limit_exceeded":
108
+ return ResourceLimitExceededError(message)
109
+ if error_type == "approval_required":
110
+ return ApprovalRequiredError(body.get("approval_request_id", ""), message)
111
+ return AuthError(message, 403)
112
+ if status == 402:
113
+ return PaymentRequiredError(message, body)
114
+ if status == 404:
115
+ return NotFoundError(message)
116
+ if status == 429:
117
+ retry_after_ms = None
118
+ if headers and "retry-after" in headers:
119
+ with contextlib.suppress(ValueError):
120
+ retry_after_ms = int(headers["retry-after"]) * 1000
121
+ return RateLimitError(message, retry_after_ms)
122
+ if status >= 500:
123
+ return ServerError(message, status)
124
+ return OneclawError(message, status, "unknown")
oneclaw/http_client.py ADDED
@@ -0,0 +1,242 @@
1
+ """Internal HTTP transport for the 1Claw Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import threading
8
+ import time
9
+ from typing import Any, TypeVar
10
+
11
+ import httpx
12
+
13
+ from oneclaw.errors import OneclawError, error_from_response
14
+ from oneclaw.types import ErrorInfo, OneclawClientConfig, OneclawResponse, ResponseMeta
15
+
16
+ T = TypeVar("T")
17
+
18
+ _REFRESH_BUFFER_SECS = 60
19
+
20
+
21
+ class HttpClient:
22
+ """Low-level HTTP client that handles authentication and error mapping.
23
+
24
+ All resource modules delegate requests through this class.
25
+ Agent ``ocv_`` API keys are automatically exchanged for JWTs and
26
+ refreshed 60 seconds before expiry.
27
+ """
28
+
29
+ def __init__(self, config: OneclawClientConfig) -> None:
30
+ self._base_url = config.base_url.rstrip("/")
31
+ self._token = config.token
32
+ self._token_expires_at: float = 0
33
+ self._timeout = config.timeout
34
+
35
+ self._agent_credentials: dict[str, str | None] | None = None
36
+ self._resolved_agent_id: str | None = None
37
+ self._refresh_lock = threading.Lock()
38
+
39
+ is_agent_key = (config.api_key or "").startswith("ocv_") or config.agent_id is not None
40
+ if config.api_key and is_agent_key:
41
+ self._agent_credentials = {
42
+ "agent_id": config.agent_id,
43
+ "api_key": config.api_key,
44
+ }
45
+
46
+ self._client = httpx.Client(
47
+ base_url=self._base_url,
48
+ timeout=self._timeout,
49
+ headers={"Content-Type": "application/json"},
50
+ )
51
+
52
+ # -- public helpers --------------------------------------------------------
53
+
54
+ @property
55
+ def resolved_agent_id(self) -> str | None:
56
+ if self._resolved_agent_id:
57
+ return self._resolved_agent_id
58
+ if self._agent_credentials:
59
+ return self._agent_credentials.get("agent_id")
60
+ return None
61
+
62
+ def set_token(self, token: str) -> None:
63
+ self._token = token
64
+ self._token_expires_at = self._decode_expiry(token)
65
+
66
+ def get_token(self) -> str | None:
67
+ return self._token
68
+
69
+ @property
70
+ def base_url(self) -> str:
71
+ return self._base_url
72
+
73
+ # -- request methods -------------------------------------------------------
74
+
75
+ def request(
76
+ self,
77
+ method: str,
78
+ path: str,
79
+ *,
80
+ body: Any | None = None,
81
+ query: dict[str, Any] | None = None,
82
+ headers: dict[str, str] | None = None,
83
+ skip_auth: bool = False,
84
+ ) -> OneclawResponse[Any]:
85
+ """Perform a typed request and return an :class:`OneclawResponse` envelope."""
86
+ if not skip_auth:
87
+ self._ensure_token()
88
+
89
+ req_headers: dict[str, str] = {}
90
+ if headers:
91
+ req_headers.update(headers)
92
+ if not skip_auth and self._token:
93
+ req_headers["Authorization"] = f"Bearer {self._token}"
94
+
95
+ cleaned_query = None
96
+ if query:
97
+ cleaned_query = {k: str(v) for k, v in query.items() if v is not None}
98
+
99
+ try:
100
+ response = self._client.request(
101
+ method,
102
+ path,
103
+ json=body if body is not None else None,
104
+ params=cleaned_query,
105
+ headers=req_headers,
106
+ )
107
+ except httpx.HTTPError as exc:
108
+ return OneclawResponse(
109
+ data=None,
110
+ error=ErrorInfo(type="network_error", message=str(exc)),
111
+ meta=ResponseMeta(status=0),
112
+ )
113
+
114
+ if not response.is_success:
115
+ try:
116
+ err_body = response.json()
117
+ except Exception:
118
+ err_body = {}
119
+
120
+ err = error_from_response(
121
+ response.status_code,
122
+ err_body if isinstance(err_body, dict) else {},
123
+ dict(response.headers),
124
+ )
125
+ return OneclawResponse(
126
+ data=None,
127
+ error=ErrorInfo(type=err.error_type, message=str(err), detail=err.detail),
128
+ meta=ResponseMeta(status=response.status_code),
129
+ )
130
+
131
+ if response.status_code == 204:
132
+ return OneclawResponse(data=None, error=None, meta=ResponseMeta(status=204))
133
+
134
+ return OneclawResponse(
135
+ data=response.json(),
136
+ error=None,
137
+ meta=ResponseMeta(status=response.status_code),
138
+ )
139
+
140
+ def request_or_throw(
141
+ self,
142
+ method: str,
143
+ path: str,
144
+ *,
145
+ body: Any | None = None,
146
+ query: dict[str, Any] | None = None,
147
+ headers: dict[str, str] | None = None,
148
+ ) -> Any:
149
+ """Same as :meth:`request` but raises :class:`OneclawError` on failure."""
150
+ self._ensure_token()
151
+
152
+ req_headers: dict[str, str] = {}
153
+ if headers:
154
+ req_headers.update(headers)
155
+ if self._token:
156
+ req_headers["Authorization"] = f"Bearer {self._token}"
157
+
158
+ cleaned_query = None
159
+ if query:
160
+ cleaned_query = {k: str(v) for k, v in query.items() if v is not None}
161
+
162
+ response = self._client.request(
163
+ method,
164
+ path,
165
+ json=body if body is not None else None,
166
+ params=cleaned_query,
167
+ headers=req_headers,
168
+ )
169
+
170
+ if not response.is_success:
171
+ try:
172
+ err_body = response.json()
173
+ except Exception:
174
+ err_body = {}
175
+ raise error_from_response(
176
+ response.status_code,
177
+ err_body if isinstance(err_body, dict) else {},
178
+ dict(response.headers),
179
+ )
180
+
181
+ if response.status_code == 204:
182
+ return None
183
+
184
+ return response.json()
185
+
186
+ def close(self) -> None:
187
+ """Close the underlying HTTP connection pool."""
188
+ self._client.close()
189
+
190
+ # -- private helpers -------------------------------------------------------
191
+
192
+ def _ensure_token(self) -> None:
193
+ if not self._agent_credentials:
194
+ return
195
+ if self._token and time.time() < self._token_expires_at - _REFRESH_BUFFER_SECS:
196
+ return
197
+
198
+ with self._refresh_lock:
199
+ if self._token and time.time() < self._token_expires_at - _REFRESH_BUFFER_SECS:
200
+ return
201
+ self._refresh_agent_token()
202
+
203
+ def _refresh_agent_token(self) -> None:
204
+ assert self._agent_credentials is not None
205
+ payload: dict[str, Any] = {"api_key": self._agent_credentials["api_key"]}
206
+ if self._agent_credentials.get("agent_id"):
207
+ payload["agent_id"] = self._agent_credentials["agent_id"]
208
+
209
+ response = self._client.post(
210
+ "/v1/auth/agent-token",
211
+ json=payload,
212
+ )
213
+
214
+ if not response.is_success:
215
+ raise OneclawError(
216
+ f"Agent token refresh failed: HTTP {response.status_code}",
217
+ response.status_code,
218
+ "auth_error",
219
+ )
220
+
221
+ data = response.json()
222
+ self._token = data["access_token"]
223
+ self._token_expires_at = time.time() + data.get("expires_in", 3600)
224
+
225
+ if data.get("agent_id"):
226
+ self._resolved_agent_id = data["agent_id"]
227
+ if self._agent_credentials and not self._agent_credentials.get("agent_id"):
228
+ self._agent_credentials["agent_id"] = data["agent_id"]
229
+
230
+ @staticmethod
231
+ def _decode_expiry(jwt: str) -> float:
232
+ try:
233
+ parts = jwt.split(".")
234
+ if len(parts) != 3:
235
+ return 0
236
+ padding = 4 - len(parts[1]) % 4
237
+ padded = parts[1] + "=" * padding
238
+ payload = json.loads(base64.urlsafe_b64decode(padded))
239
+ exp = payload.get("exp")
240
+ return float(exp) if isinstance(exp, (int, float)) else 0
241
+ except Exception:
242
+ return 0
oneclaw/py.typed ADDED
File without changes
@@ -0,0 +1,41 @@
1
+ """Resource modules for the 1Claw Python SDK."""
2
+
3
+ from oneclaw.resources.agents import AgentsResource
4
+ from oneclaw.resources.api_keys import ApiKeysResource
5
+ from oneclaw.resources.approvals import ApprovalsResource
6
+ from oneclaw.resources.audit import AuditResource
7
+ from oneclaw.resources.auth import AuthResource
8
+ from oneclaw.resources.billing import BillingResource
9
+ from oneclaw.resources.chains import ChainsResource
10
+ from oneclaw.resources.org import OrgResource
11
+ from oneclaw.resources.platform import PlatformResource
12
+ from oneclaw.resources.policies import PoliciesResource
13
+ from oneclaw.resources.risk import RiskResource
14
+ from oneclaw.resources.secrets import SecretsResource
15
+ from oneclaw.resources.sharing import SharingResource
16
+ from oneclaw.resources.signing_keys import SigningKeysResource
17
+ from oneclaw.resources.treasury import TreasuryResource
18
+ from oneclaw.resources.treasury_wallets import TreasuryWalletsResource
19
+ from oneclaw.resources.vaults import VaultResource
20
+ from oneclaw.resources.webhooks import WebhooksResource
21
+
22
+ __all__ = [
23
+ "AuthResource",
24
+ "VaultResource",
25
+ "SecretsResource",
26
+ "AgentsResource",
27
+ "PoliciesResource",
28
+ "ChainsResource",
29
+ "SharingResource",
30
+ "BillingResource",
31
+ "AuditResource",
32
+ "OrgResource",
33
+ "ApiKeysResource",
34
+ "SigningKeysResource",
35
+ "TreasuryResource",
36
+ "TreasuryWalletsResource",
37
+ "PlatformResource",
38
+ "ApprovalsResource",
39
+ "WebhooksResource",
40
+ "RiskResource",
41
+ ]