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.
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/PKG-INFO +1 -1
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/pyproject.toml +1 -1
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi/__init__.py +4 -1
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi/client.py +24 -0
- swiftapi_python-1.1.1/swiftapi/openai.py +200 -0
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi/verifier.py +28 -20
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi_python.egg-info/PKG-INFO +1 -1
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi_python.egg-info/SOURCES.txt +1 -0
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/README.md +0 -0
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/setup.cfg +0 -0
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi/enforcement.py +0 -0
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi/exceptions.py +0 -0
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi/utils.py +0 -0
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi_python.egg-info/dependency_links.txt +0 -0
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi_python.egg-info/requires.txt +0 -0
- {swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi_python.egg-info/top_level.txt +0 -0
|
@@ -29,13 +29,14 @@ Usage:
|
|
|
29
29
|
db.update(user_id, data)
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
__version__ = "1.
|
|
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
|
-
#
|
|
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
|
|
97
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{swiftapi_python-1.0.3 → swiftapi_python-1.1.1}/swiftapi_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|