swiftapi-python 1.0.3__tar.gz → 1.1.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swiftapi-python
3
- Version: 1.0.3
3
+ Version: 1.1.1
4
4
  Summary: SwiftAPI Python SDK - AI Action Verification Gateway
5
5
  Author-email: Rayan Pal <rayan@swiftapi.ai>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swiftapi-python"
7
- version = "1.0.3"
7
+ version = "1.1.1"
8
8
  description = "SwiftAPI Python SDK - AI Action Verification Gateway"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -29,13 +29,14 @@ Usage:
29
29
  db.update(user_id, data)
30
30
  """
31
31
 
32
- __version__ = "1.0.0"
32
+ __version__ = "1.1.1"
33
33
  __author__ = "Rayan Pal"
34
34
 
35
35
  # Core exports
36
36
  from .client import SwiftAPI
37
37
  from .enforcement import Enforcement, enforce
38
38
  from .verifier import verify_signature, is_valid, get_public_key
39
+ from .openai import OpenAI
39
40
 
40
41
  # Exceptions
41
42
  from .exceptions import (
@@ -67,6 +68,8 @@ __all__ = [
67
68
  "SwiftAPI",
68
69
  "Enforcement",
69
70
  "enforce",
71
+ # Drop-in OpenAI replacement
72
+ "OpenAI",
70
73
  # Verification
71
74
  "verify_signature",
72
75
  "is_valid",
@@ -213,6 +213,30 @@ class SwiftAPI:
213
213
  # If endpoint fails, assume not revoked (fail-open for availability)
214
214
  return False
215
215
 
216
+ def attest(
217
+ self,
218
+ action_type: str,
219
+ action_data: Dict[str, Any],
220
+ ) -> Dict[str, Any]:
221
+ """
222
+ Request an execution attestation from the authority.
223
+
224
+ Calls /attest to get a signed attestation for the given action.
225
+ The attestation can then be passed to gated endpoints like /chat/vibe.
226
+
227
+ Args:
228
+ action_type: Type of action (e.g., "chat_completion")
229
+ action_data: Action payload to be attested
230
+
231
+ Returns:
232
+ Signed attestation dict with jti, signature, action_fingerprint, etc.
233
+ """
234
+ payload = {
235
+ "action_type": action_type,
236
+ "action_data": action_data,
237
+ }
238
+ return self._request("POST", "/attest", json=payload)
239
+
216
240
  # =========================================================================
217
241
  # Info Endpoints
218
242
  # =========================================================================
@@ -0,0 +1,200 @@
1
+ """
2
+ SwiftAPI Attested OpenAI - Drop-in replacement.
3
+
4
+ # Before (vibe coding):
5
+ from openai import OpenAI
6
+ client = OpenAI()
7
+
8
+ # After (attested):
9
+ from swiftapi import OpenAI
10
+ client = OpenAI(swiftapi_key="swiftapi_live_...")
11
+
12
+ Same interface. Same return type. Every completion is cryptographically attested.
13
+ No attestation, no output.
14
+ """
15
+
16
+ import json
17
+ import requests
18
+ from typing import List, Dict, Union, Optional, Any
19
+
20
+ from .exceptions import SwiftAPIError, AuthenticationError, NetworkError
21
+
22
+
23
+ class Message:
24
+ """OpenAI-compatible message object."""
25
+
26
+ def __init__(self, role: str, content: str):
27
+ self.role = role
28
+ self.content = content
29
+
30
+ def __repr__(self):
31
+ truncated = self.content[:80] if self.content else ""
32
+ return f"Message(role='{self.role}', content='{truncated}')"
33
+
34
+
35
+ class Choice:
36
+ """OpenAI-compatible choice object."""
37
+
38
+ def __init__(self, index: int, message: Message, finish_reason: str):
39
+ self.index = index
40
+ self.message = message
41
+ self.finish_reason = finish_reason
42
+
43
+
44
+ class ChatCompletion:
45
+ """OpenAI-compatible chat completion response."""
46
+
47
+ def __init__(self, content: str, model: str, attested: bool = False, jti: str = None):
48
+ self.choices = [
49
+ Choice(
50
+ index=0,
51
+ message=Message(role="assistant", content=content),
52
+ finish_reason="stop" if content else "void",
53
+ )
54
+ ]
55
+ self.model = model
56
+ self.attested = attested
57
+ self.jti = jti
58
+
59
+ @property
60
+ def content(self):
61
+ """Convenience: response.content instead of response.choices[0].message.content"""
62
+ return self.choices[0].message.content
63
+
64
+
65
+ class Completions:
66
+ """Handles chat.completions.create() calls."""
67
+
68
+ def __init__(self, key: str, base_url: str, timeout: int):
69
+ self._key = key
70
+ self._base_url = base_url.rstrip("/")
71
+ self._timeout = timeout
72
+ self._session = requests.Session()
73
+ self._session.headers.update({
74
+ "X-SwiftAPI-Authority": key,
75
+ "Content-Type": "application/json",
76
+ "User-Agent": "swiftapi-python/1.1.0",
77
+ })
78
+
79
+ def create(
80
+ self,
81
+ model: str,
82
+ messages: List[Dict[str, Any]],
83
+ temperature: float = 0.7,
84
+ max_completion_tokens: int = 300,
85
+ max_tokens: Optional[int] = None,
86
+ **kwargs,
87
+ ) -> ChatCompletion:
88
+ """
89
+ Create an attested chat completion.
90
+
91
+ Same interface as openai.chat.completions.create().
92
+ Attestation is handled transparently. If any gate fails, content is empty string.
93
+
94
+ Args:
95
+ model: Model name (e.g. "gpt-4o-mini")
96
+ messages: List of message dicts with role and content
97
+ temperature: Sampling temperature
98
+ max_completion_tokens: Max tokens in response
99
+ max_tokens: Alias for max_completion_tokens (OpenAI compat)
100
+
101
+ Returns:
102
+ ChatCompletion with .choices[0].message.content
103
+ """
104
+ payload = {
105
+ "model": model,
106
+ "messages": messages,
107
+ "temperature": temperature,
108
+ "max_completion_tokens": max_tokens or max_completion_tokens,
109
+ }
110
+
111
+ # Step 1: Attest
112
+ try:
113
+ attest_resp = self._session.post(
114
+ f"{self._base_url}/attest",
115
+ json={"action_type": "chat_completion", "action_data": payload},
116
+ timeout=self._timeout,
117
+ )
118
+ except requests.exceptions.RequestException as e:
119
+ raise NetworkError(f"Attestation request failed: {e}")
120
+
121
+ if attest_resp.status_code == 403:
122
+ raise AuthenticationError("Invalid SwiftAPI authority key")
123
+ if attest_resp.status_code == 429:
124
+ raise SwiftAPIError("Rate limit exceeded")
125
+ if attest_resp.status_code != 200:
126
+ raise SwiftAPIError(f"Attestation failed: {attest_resp.text[:200]}")
127
+
128
+ attestation = attest_resp.json()
129
+
130
+ if not attestation.get("approved"):
131
+ return ChatCompletion(content="", model=model)
132
+
133
+ # Step 2: Forward to /chat/vibe with attestation
134
+ try:
135
+ vibe_resp = self._session.post(
136
+ f"{self._base_url}/chat/vibe",
137
+ json=payload,
138
+ headers={"X-SwiftAPI-Attestation": json.dumps(attestation)},
139
+ timeout=self._timeout,
140
+ )
141
+ except requests.exceptions.RequestException as e:
142
+ raise NetworkError(f"Chat request failed: {e}")
143
+
144
+ if vibe_resp.status_code == 403:
145
+ raise AuthenticationError("Invalid SwiftAPI authority key")
146
+ if vibe_resp.status_code != 200:
147
+ raise SwiftAPIError(f"Chat failed: {vibe_resp.text[:200]}")
148
+
149
+ content = vibe_resp.json()
150
+ if content is None:
151
+ content = ""
152
+
153
+ return ChatCompletion(
154
+ content=content,
155
+ model=model,
156
+ attested=True,
157
+ jti=attestation.get("jti"),
158
+ )
159
+
160
+
161
+ class _ChatNamespace:
162
+ """Namespace for client.chat.completions"""
163
+
164
+ def __init__(self, key: str, base_url: str, timeout: int):
165
+ self.completions = Completions(key, base_url, timeout)
166
+
167
+
168
+ class OpenAI:
169
+ """
170
+ Drop-in replacement for openai.OpenAI.
171
+
172
+ Every completion is cryptographically attested via SwiftAPI.
173
+ No attestation, no output.
174
+
175
+ Usage:
176
+ from swiftapi import OpenAI
177
+
178
+ client = OpenAI(swiftapi_key="swiftapi_live_...")
179
+ r = client.chat.completions.create(
180
+ model="gpt-4o-mini",
181
+ messages=[{"role": "user", "content": "Hello"}]
182
+ )
183
+ print(r.choices[0].message.content)
184
+ """
185
+
186
+ def __init__(
187
+ self,
188
+ swiftapi_key: str = None,
189
+ api_key: str = None,
190
+ base_url: str = "https://swiftapi.ai",
191
+ timeout: int = 60,
192
+ **kwargs,
193
+ ):
194
+ key = swiftapi_key or api_key
195
+ if not key:
196
+ raise AuthenticationError("swiftapi_key is required")
197
+ if not key.startswith("swiftapi_live_"):
198
+ raise AuthenticationError("Invalid key format: must start with 'swiftapi_live_'")
199
+
200
+ self.chat = _ChatNamespace(key, base_url, timeout)
@@ -27,22 +27,27 @@ SWIFTAPI_PUBLIC_KEY = base64.b64decode(SWIFTAPI_PUBLIC_KEY_B64)
27
27
  VERIFY_KEY = VerifyKey(SWIFTAPI_PUBLIC_KEY)
28
28
 
29
29
 
30
- def verify_signature(attestation: Dict[str, Any]) -> bool:
30
+ def verify_signature(attestation: Dict[str, Any], ignore_expiry: bool = False) -> bool:
31
31
  """
32
32
  Verify the Ed25519 signature of an execution attestation.
33
33
 
34
34
  This is OFFLINE verification - no network required.
35
35
  The signature proves the attestation was issued by SwiftAPI.
36
36
 
37
+ Signature authenticity is permanent. Expiry is about the execution window,
38
+ not whether the attestation is genuine. Use ignore_expiry=True for audit
39
+ and post-hoc verification.
40
+
37
41
  Args:
38
42
  attestation: The execution_attestation dict from /verify response
43
+ ignore_expiry: If True, verify signature without checking expiry
39
44
 
40
45
  Returns:
41
46
  True if signature is valid
42
47
 
43
48
  Raises:
44
49
  SignatureVerificationError: If signature is invalid or missing
45
- AttestationExpiredError: If attestation has expired
50
+ AttestationExpiredError: If attestation has expired (and ignore_expiry is False)
46
51
  """
47
52
  # Extract required fields
48
53
  signature_b64 = attestation.get("signature")
@@ -56,35 +61,34 @@ def verify_signature(attestation: Dict[str, Any]) -> bool:
56
61
  if not all([jti, action_fingerprint, expires_at]):
57
62
  raise SignatureVerificationError("Incomplete attestation: missing required fields")
58
63
 
59
- # Check expiration FIRST
60
- try:
61
- expiry = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
62
- now = datetime.now(timezone.utc)
63
- if now > expiry:
64
- raise AttestationExpiredError(f"Attestation expired at {expires_at}")
65
- except ValueError as e:
66
- raise SignatureVerificationError(f"Invalid expiration format: {e}")
67
-
68
- # Reconstruct the signed payload (deterministic serialization)
69
- # This MUST match the server's signing logic exactly
64
+ # Verify signature FIRST — authenticity is permanent
70
65
  signed_payload = _reconstruct_signed_payload(attestation)
71
66
 
72
- # Decode signature
73
67
  try:
74
68
  signature = base64.b64decode(signature_b64)
75
69
  except Exception as e:
76
70
  raise SignatureVerificationError(f"Invalid signature encoding: {e}")
77
71
 
78
- # Verify signature
79
72
  try:
80
73
  VERIFY_KEY.verify(signed_payload, signature)
81
- return True
82
74
  except BadSignatureError:
83
75
  raise SignatureVerificationError(
84
76
  "INVALID SIGNATURE: Attestation was not signed by SwiftAPI. "
85
77
  "This could indicate a forged or tampered attestation."
86
78
  )
87
79
 
80
+ # Check expiration AFTER signature (execution window, not authenticity)
81
+ if not ignore_expiry:
82
+ try:
83
+ expiry = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
84
+ now = datetime.now(timezone.utc)
85
+ if now > expiry:
86
+ raise AttestationExpiredError(f"Attestation expired at {expires_at}")
87
+ except ValueError as e:
88
+ raise SignatureVerificationError(f"Invalid expiration format: {e}")
89
+
90
+ return True
91
+
88
92
 
89
93
  def _reconstruct_signed_payload(attestation: Dict[str, Any]) -> bytes:
90
94
  """
@@ -93,8 +97,11 @@ def _reconstruct_signed_payload(attestation: Dict[str, Any]) -> bytes:
93
97
  The server signs the attestation BEFORE adding signature/signing_mode fields.
94
98
  We must reconstruct that exact payload.
95
99
  """
96
- # Copy attestation and remove signature fields (they weren't in the signed payload)
97
- signed_fields = {k: v for k, v in attestation.items() if k not in ("signature", "signing_mode")}
100
+ # Copy attestation and remove fields that weren't in the signed payload.
101
+ # signature/signing_mode are added after signing.
102
+ # action_data/denial_reason are added by /attest after signing.
103
+ _unsigned = ("signature", "signing_mode", "action_data", "denial_reason")
104
+ signed_fields = {k: v for k, v in attestation.items() if k not in _unsigned}
98
105
 
99
106
  # Deterministic JSON serialization (must match server exactly)
100
107
  payload_str = json.dumps(signed_fields, sort_keys=True, separators=(",", ":"))
@@ -106,17 +113,18 @@ def get_public_key() -> str:
106
113
  return SWIFTAPI_PUBLIC_KEY_B64
107
114
 
108
115
 
109
- def is_valid(attestation: Dict[str, Any]) -> bool:
116
+ def is_valid(attestation: Dict[str, Any], ignore_expiry: bool = False) -> bool:
110
117
  """
111
118
  Check if attestation is valid without raising exceptions.
112
119
 
113
120
  Args:
114
121
  attestation: The execution_attestation dict
122
+ ignore_expiry: If True, only check signature authenticity
115
123
 
116
124
  Returns:
117
125
  True if valid, False otherwise
118
126
  """
119
127
  try:
120
- return verify_signature(attestation)
128
+ return verify_signature(attestation, ignore_expiry=ignore_expiry)
121
129
  except Exception:
122
130
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swiftapi-python
3
- Version: 1.0.3
3
+ Version: 1.1.1
4
4
  Summary: SwiftAPI Python SDK - AI Action Verification Gateway
5
5
  Author-email: Rayan Pal <rayan@swiftapi.ai>
6
6
  License-Expression: MIT
@@ -4,6 +4,7 @@ swiftapi/__init__.py
4
4
  swiftapi/client.py
5
5
  swiftapi/enforcement.py
6
6
  swiftapi/exceptions.py
7
+ swiftapi/openai.py
7
8
  swiftapi/utils.py
8
9
  swiftapi/verifier.py
9
10
  swiftapi_python.egg-info/PKG-INFO