glacis 0.1.3__py3-none-any.whl → 0.2.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.
- glacis/__init__.py +62 -1
- glacis/__main__.py +1 -80
- glacis/client.py +60 -31
- glacis/config.py +141 -0
- glacis/controls/__init__.py +232 -0
- glacis/controls/base.py +104 -0
- glacis/controls/jailbreak.py +224 -0
- glacis/controls/pii.py +855 -0
- glacis/crypto.py +70 -1
- glacis/integrations/__init__.py +53 -3
- glacis/integrations/anthropic.py +207 -142
- glacis/integrations/base.py +476 -0
- glacis/integrations/openai.py +156 -121
- glacis/models.py +277 -24
- glacis/storage.py +324 -8
- glacis/verify.py +154 -0
- glacis-0.2.0.dist-info/METADATA +275 -0
- glacis-0.2.0.dist-info/RECORD +21 -0
- glacis/wasm/s3p_core_wasi.wasm +0 -0
- glacis/wasm_runtime.py +0 -533
- glacis-0.1.3.dist-info/METADATA +0 -324
- glacis-0.1.3.dist-info/RECORD +0 -16
- {glacis-0.1.3.dist-info → glacis-0.2.0.dist-info}/WHEEL +0 -0
- {glacis-0.1.3.dist-info → glacis-0.2.0.dist-info}/licenses/LICENSE +0 -0
glacis/crypto.py
CHANGED
|
@@ -19,7 +19,8 @@ Example:
|
|
|
19
19
|
|
|
20
20
|
import hashlib
|
|
21
21
|
import json
|
|
22
|
-
from
|
|
22
|
+
from base64 import b64encode
|
|
23
|
+
from typing import Any, Optional
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def canonical_json(data: Any) -> str:
|
|
@@ -119,3 +120,71 @@ def hash_bytes(data: bytes) -> str:
|
|
|
119
120
|
Hex-encoded SHA-256 hash (64 characters)
|
|
120
121
|
"""
|
|
121
122
|
return hashlib.sha256(data).hexdigest()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# =============================================================================
|
|
126
|
+
# Ed25519 Signing (for offline attestations)
|
|
127
|
+
# =============================================================================
|
|
128
|
+
|
|
129
|
+
class CryptoError(Exception):
|
|
130
|
+
"""Error from cryptographic operations."""
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Ed25519Runtime:
|
|
135
|
+
"""
|
|
136
|
+
Ed25519 cryptographic runtime using PyNaCl (libsodium bindings).
|
|
137
|
+
|
|
138
|
+
Provides signing and verification for offline attestations.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def __init__(self) -> None:
|
|
142
|
+
try:
|
|
143
|
+
import nacl.signing
|
|
144
|
+
self._nacl_signing = nacl.signing
|
|
145
|
+
except ImportError:
|
|
146
|
+
raise CryptoError(
|
|
147
|
+
"PyNaCl not installed. Install with: pip install pynacl"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def get_public_key_hex(self, seed: bytes) -> str:
|
|
151
|
+
"""Get hex-encoded public key from a 32-byte seed."""
|
|
152
|
+
if len(seed) != 32:
|
|
153
|
+
raise ValueError("Seed must be exactly 32 bytes")
|
|
154
|
+
signing_key = self._nacl_signing.SigningKey(seed)
|
|
155
|
+
return bytes(signing_key.verify_key).hex()
|
|
156
|
+
|
|
157
|
+
def sign(self, seed: bytes, message: bytes) -> bytes:
|
|
158
|
+
"""Sign a message with Ed25519, returning 64-byte signature."""
|
|
159
|
+
if len(seed) != 32:
|
|
160
|
+
raise ValueError("Seed must be exactly 32 bytes")
|
|
161
|
+
signing_key = self._nacl_signing.SigningKey(seed)
|
|
162
|
+
signed = signing_key.sign(message)
|
|
163
|
+
return signed.signature
|
|
164
|
+
|
|
165
|
+
def sign_attestation_json(self, seed: bytes, attestation_json: str) -> str:
|
|
166
|
+
"""Sign an attestation JSON and return SignedAttestation JSON."""
|
|
167
|
+
if len(seed) != 32:
|
|
168
|
+
raise ValueError("Seed must be exactly 32 bytes")
|
|
169
|
+
|
|
170
|
+
json_bytes = attestation_json.encode("utf-8")
|
|
171
|
+
signature = self.sign(seed, json_bytes)
|
|
172
|
+
signature_b64 = b64encode(signature).decode("ascii")
|
|
173
|
+
|
|
174
|
+
payload = json.loads(attestation_json)
|
|
175
|
+
return json.dumps({
|
|
176
|
+
"payload": payload,
|
|
177
|
+
"signature": signature_b64,
|
|
178
|
+
}, separators=(",", ":"))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Singleton instance
|
|
182
|
+
_ed25519_runtime: Optional[Ed25519Runtime] = None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_ed25519_runtime() -> Ed25519Runtime:
|
|
186
|
+
"""Get the singleton Ed25519 runtime instance."""
|
|
187
|
+
global _ed25519_runtime
|
|
188
|
+
if _ed25519_runtime is None:
|
|
189
|
+
_ed25519_runtime = Ed25519Runtime()
|
|
190
|
+
return _ed25519_runtime
|
glacis/integrations/__init__.py
CHANGED
|
@@ -1,12 +1,62 @@
|
|
|
1
1
|
"""
|
|
2
|
-
GLACIS integrations for
|
|
2
|
+
GLACIS integrations for AI providers.
|
|
3
3
|
|
|
4
4
|
These integrations provide drop-in wrappers that automatically attest
|
|
5
|
-
all API calls to the GLACIS transparency log.
|
|
5
|
+
all API calls to the GLACIS transparency log with optional PII/PHI redaction.
|
|
6
6
|
|
|
7
7
|
Available integrations:
|
|
8
8
|
- OpenAI: `from glacis.integrations.openai import attested_openai`
|
|
9
9
|
- Anthropic: `from glacis.integrations.anthropic import attested_anthropic`
|
|
10
|
+
|
|
11
|
+
Example (OpenAI):
|
|
12
|
+
>>> from glacis.integrations.openai import attested_openai, get_last_receipt
|
|
13
|
+
>>> client = attested_openai(
|
|
14
|
+
... openai_api_key="sk-xxx",
|
|
15
|
+
... offline=True,
|
|
16
|
+
... signing_seed=os.urandom(32),
|
|
17
|
+
... redaction="fast", # Enable PII redaction
|
|
18
|
+
... )
|
|
19
|
+
>>> response = client.chat.completions.create(
|
|
20
|
+
... model="gpt-4o",
|
|
21
|
+
... messages=[{"role": "user", "content": "Hello!"}]
|
|
22
|
+
... )
|
|
23
|
+
>>> receipt = get_last_receipt()
|
|
24
|
+
|
|
25
|
+
Example (Anthropic):
|
|
26
|
+
>>> from glacis.integrations.anthropic import attested_anthropic, get_last_receipt
|
|
27
|
+
>>> client = attested_anthropic(
|
|
28
|
+
... anthropic_api_key="sk-ant-xxx",
|
|
29
|
+
... offline=True,
|
|
30
|
+
... signing_seed=os.urandom(32),
|
|
31
|
+
... redaction="fast",
|
|
32
|
+
... )
|
|
33
|
+
>>> response = client.messages.create(
|
|
34
|
+
... model="claude-3-5-sonnet-20241022",
|
|
35
|
+
... max_tokens=1024,
|
|
36
|
+
... messages=[{"role": "user", "content": "Hello!"}]
|
|
37
|
+
... )
|
|
38
|
+
|
|
39
|
+
Note: For Azure OpenAI, use the openai package with azure endpoint configuration.
|
|
10
40
|
"""
|
|
11
41
|
|
|
12
|
-
|
|
42
|
+
from glacis.integrations.anthropic import attested_anthropic
|
|
43
|
+
from glacis.integrations.base import (
|
|
44
|
+
GlacisBlockedError,
|
|
45
|
+
get_evidence,
|
|
46
|
+
get_last_receipt,
|
|
47
|
+
)
|
|
48
|
+
from glacis.integrations.openai import attested_openai
|
|
49
|
+
|
|
50
|
+
# Backwards compatible aliases
|
|
51
|
+
get_last_openai_receipt = get_last_receipt
|
|
52
|
+
get_last_anthropic_receipt = get_last_receipt
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"attested_openai",
|
|
56
|
+
"attested_anthropic",
|
|
57
|
+
"get_last_receipt",
|
|
58
|
+
"get_last_openai_receipt",
|
|
59
|
+
"get_last_anthropic_receipt",
|
|
60
|
+
"get_evidence",
|
|
61
|
+
"GlacisBlockedError",
|
|
62
|
+
]
|
glacis/integrations/anthropic.py
CHANGED
|
@@ -1,62 +1,82 @@
|
|
|
1
1
|
"""
|
|
2
2
|
GLACIS integration for Anthropic.
|
|
3
3
|
|
|
4
|
-
Provides an attested Anthropic client wrapper that automatically
|
|
5
|
-
|
|
4
|
+
Provides an attested Anthropic client wrapper that automatically:
|
|
5
|
+
1. Runs enabled controls (PII/PHI redaction, jailbreak detection, etc.)
|
|
6
|
+
2. Logs all completions to the GLACIS transparency log
|
|
7
|
+
3. Creates control plane attestations
|
|
6
8
|
|
|
7
9
|
Example:
|
|
8
|
-
>>> from glacis.integrations.anthropic import attested_anthropic
|
|
9
|
-
>>> client = attested_anthropic(
|
|
10
|
+
>>> from glacis.integrations.anthropic import attested_anthropic, get_last_receipt
|
|
11
|
+
>>> client = attested_anthropic(
|
|
12
|
+
... anthropic_api_key="sk-ant-xxx",
|
|
13
|
+
... offline=True,
|
|
14
|
+
... signing_seed=os.urandom(32),
|
|
15
|
+
... )
|
|
10
16
|
>>> response = client.messages.create(
|
|
11
|
-
... model="claude-3-
|
|
17
|
+
... model="claude-3-5-sonnet-20241022",
|
|
18
|
+
... max_tokens=1024,
|
|
12
19
|
... messages=[{"role": "user", "content": "Hello!"}]
|
|
13
20
|
... )
|
|
14
|
-
|
|
21
|
+
>>> receipt = get_last_receipt()
|
|
15
22
|
"""
|
|
16
23
|
|
|
17
|
-
from
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
|
27
|
+
|
|
28
|
+
from glacis.integrations.base import (
|
|
29
|
+
GlacisBlockedError,
|
|
30
|
+
create_controls_runner,
|
|
31
|
+
create_glacis_client,
|
|
32
|
+
get_evidence,
|
|
33
|
+
get_last_receipt,
|
|
34
|
+
initialize_config,
|
|
35
|
+
set_last_receipt,
|
|
36
|
+
store_evidence,
|
|
37
|
+
suppress_noisy_loggers,
|
|
38
|
+
)
|
|
18
39
|
|
|
19
40
|
if TYPE_CHECKING:
|
|
20
41
|
from anthropic import Anthropic
|
|
21
42
|
|
|
22
43
|
|
|
23
44
|
def attested_anthropic(
|
|
24
|
-
glacis_api_key: str,
|
|
45
|
+
glacis_api_key: Optional[str] = None,
|
|
25
46
|
anthropic_api_key: Optional[str] = None,
|
|
26
47
|
glacis_base_url: str = "https://api.glacis.io",
|
|
27
48
|
service_id: str = "anthropic",
|
|
28
49
|
debug: bool = False,
|
|
50
|
+
offline: Optional[bool] = None,
|
|
51
|
+
signing_seed: Optional[bytes] = None,
|
|
52
|
+
redaction: Union[bool, Literal["fast", "full"], None] = None,
|
|
53
|
+
config: Optional[str] = None,
|
|
29
54
|
**anthropic_kwargs: Any,
|
|
30
55
|
) -> "Anthropic":
|
|
31
56
|
"""
|
|
32
|
-
Create an attested Anthropic client.
|
|
33
|
-
|
|
34
|
-
All messages are automatically attested to the GLACIS transparency log.
|
|
35
|
-
The input (messages) and output (response) are hashed locally - the actual
|
|
36
|
-
content never leaves your infrastructure.
|
|
57
|
+
Create an attested Anthropic client with controls (PII redaction, jailbreak detection).
|
|
37
58
|
|
|
38
59
|
Args:
|
|
39
|
-
glacis_api_key: GLACIS API key
|
|
60
|
+
glacis_api_key: GLACIS API key (required for online mode)
|
|
40
61
|
anthropic_api_key: Anthropic API key (default: from ANTHROPIC_API_KEY env var)
|
|
41
62
|
glacis_base_url: GLACIS API base URL
|
|
42
63
|
service_id: Service identifier for attestations
|
|
43
64
|
debug: Enable debug logging
|
|
65
|
+
offline: Enable offline mode (local signing, no server)
|
|
66
|
+
signing_seed: 32-byte Ed25519 signing seed (required for offline mode)
|
|
67
|
+
redaction: PII/PHI redaction mode - "fast", "full", True, False, or None
|
|
68
|
+
config: Path to glacis.yaml config file
|
|
44
69
|
**anthropic_kwargs: Additional arguments passed to Anthropic client
|
|
45
70
|
|
|
46
71
|
Returns:
|
|
47
72
|
Wrapped Anthropic client
|
|
48
73
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
... glacis_api_key="glsk_live_xxx",
|
|
52
|
-
... anthropic_api_key="sk-xxx"
|
|
53
|
-
... )
|
|
54
|
-
>>> response = client.messages.create(
|
|
55
|
-
... model="claude-3-opus-20240229",
|
|
56
|
-
... max_tokens=1024,
|
|
57
|
-
... messages=[{"role": "user", "content": "Hello!"}]
|
|
58
|
-
... )
|
|
74
|
+
Raises:
|
|
75
|
+
GlacisBlockedError: If a control blocks the request
|
|
59
76
|
"""
|
|
77
|
+
# Suppress noisy loggers
|
|
78
|
+
suppress_noisy_loggers(["anthropic", "anthropic._base_client"])
|
|
79
|
+
|
|
60
80
|
try:
|
|
61
81
|
from anthropic import Anthropic
|
|
62
82
|
except ImportError:
|
|
@@ -65,11 +85,24 @@ def attested_anthropic(
|
|
|
65
85
|
"Install it with: pip install glacis[anthropic]"
|
|
66
86
|
)
|
|
67
87
|
|
|
68
|
-
from glacis import Glacis
|
|
69
88
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
# Initialize config and determine modes
|
|
90
|
+
cfg, effective_offline, effective_service_id = initialize_config(
|
|
91
|
+
config_path=config,
|
|
92
|
+
redaction=redaction,
|
|
93
|
+
offline=offline,
|
|
94
|
+
glacis_api_key=glacis_api_key,
|
|
95
|
+
default_service_id="anthropic",
|
|
96
|
+
service_id=service_id,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Create controls runner and Glacis client
|
|
100
|
+
controls_runner = create_controls_runner(cfg, debug)
|
|
101
|
+
glacis = create_glacis_client(
|
|
102
|
+
offline=effective_offline,
|
|
103
|
+
signing_seed=signing_seed,
|
|
104
|
+
glacis_api_key=glacis_api_key,
|
|
105
|
+
glacis_base_url=glacis_base_url,
|
|
73
106
|
debug=debug,
|
|
74
107
|
)
|
|
75
108
|
|
|
@@ -84,132 +117,157 @@ def attested_anthropic(
|
|
|
84
117
|
original_create = client.messages.create
|
|
85
118
|
|
|
86
119
|
def attested_create(*args: Any, **kwargs: Any) -> Any:
|
|
87
|
-
|
|
120
|
+
if kwargs.get("stream", False):
|
|
121
|
+
raise NotImplementedError(
|
|
122
|
+
"Streaming is not currently supported with attested_anthropic. "
|
|
123
|
+
"Use stream=False for now."
|
|
124
|
+
)
|
|
125
|
+
|
|
88
126
|
messages = kwargs.get("messages", [])
|
|
89
127
|
model = kwargs.get("model", "unknown")
|
|
90
128
|
system = kwargs.get("system")
|
|
91
129
|
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"messages": messages,
|
|
100
|
-
}
|
|
101
|
-
if system:
|
|
102
|
-
input_data["system"] = system
|
|
103
|
-
|
|
104
|
-
glacis.attest(
|
|
105
|
-
service_id=service_id,
|
|
106
|
-
operation_type="completion",
|
|
107
|
-
input=input_data,
|
|
108
|
-
output={
|
|
109
|
-
"model": response.model,
|
|
110
|
-
"content": [
|
|
111
|
-
{
|
|
112
|
-
"type": block.type,
|
|
113
|
-
"text": getattr(block, "text", None),
|
|
114
|
-
}
|
|
115
|
-
for block in response.content
|
|
116
|
-
],
|
|
117
|
-
"stop_reason": response.stop_reason,
|
|
118
|
-
"usage": {
|
|
119
|
-
"input_tokens": response.usage.input_tokens,
|
|
120
|
-
"output_tokens": response.usage.output_tokens,
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
metadata={"provider": "anthropic", "model": model},
|
|
130
|
+
# Run controls if enabled
|
|
131
|
+
if controls_runner:
|
|
132
|
+
from glacis.integrations.base import (
|
|
133
|
+
ControlResultsAccumulator,
|
|
134
|
+
create_control_plane_attestation_from_accumulator,
|
|
135
|
+
handle_blocked_request,
|
|
136
|
+
process_text_for_controls,
|
|
124
137
|
)
|
|
125
|
-
except Exception as e:
|
|
126
|
-
if debug:
|
|
127
|
-
print(f"[glacis] Attestation failed: {e}")
|
|
128
|
-
|
|
129
|
-
return response
|
|
130
|
-
|
|
131
|
-
# Replace the create method
|
|
132
|
-
client.messages.create = attested_create # type: ignore
|
|
133
|
-
|
|
134
|
-
return client
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def attested_async_anthropic(
|
|
138
|
-
glacis_api_key: str,
|
|
139
|
-
anthropic_api_key: Optional[str] = None,
|
|
140
|
-
glacis_base_url: str = "https://api.glacis.io",
|
|
141
|
-
service_id: str = "anthropic",
|
|
142
|
-
debug: bool = False,
|
|
143
|
-
**anthropic_kwargs: Any,
|
|
144
|
-
) -> Any:
|
|
145
|
-
"""
|
|
146
|
-
Create an attested async Anthropic client.
|
|
147
138
|
|
|
148
|
-
|
|
139
|
+
accumulator = ControlResultsAccumulator()
|
|
140
|
+
|
|
141
|
+
# Process system prompt through controls
|
|
142
|
+
# (system prompt is always "new" in current context)
|
|
143
|
+
if system and isinstance(system, str):
|
|
144
|
+
final_system = process_text_for_controls(
|
|
145
|
+
controls_runner, system, accumulator
|
|
146
|
+
)
|
|
147
|
+
kwargs["system"] = final_system
|
|
148
|
+
|
|
149
|
+
# Process messages with delta scanning (only scan last user message)
|
|
150
|
+
processed_messages = []
|
|
151
|
+
|
|
152
|
+
# Find the last user message index
|
|
153
|
+
last_user_idx = -1
|
|
154
|
+
for i, msg in enumerate(messages):
|
|
155
|
+
if isinstance(msg, dict) and msg.get("role") == "user":
|
|
156
|
+
last_user_idx = i
|
|
157
|
+
|
|
158
|
+
for i, msg in enumerate(messages):
|
|
159
|
+
role = msg.get("role", "") if isinstance(msg, dict) else ""
|
|
160
|
+
|
|
161
|
+
# Only run controls on the LAST user message (the new one)
|
|
162
|
+
if role == "user" and i == last_user_idx:
|
|
163
|
+
if isinstance(msg, dict) and isinstance(msg.get("content"), str):
|
|
164
|
+
content = msg["content"]
|
|
165
|
+
final_text = process_text_for_controls(
|
|
166
|
+
controls_runner, content, accumulator
|
|
167
|
+
)
|
|
168
|
+
processed_messages.append({**msg, "content": final_text})
|
|
169
|
+
|
|
170
|
+
elif isinstance(msg, dict) and isinstance(msg.get("content"), list):
|
|
171
|
+
# Handle content blocks (text, image, etc.)
|
|
172
|
+
redacted_content = []
|
|
173
|
+
for block in msg["content"]:
|
|
174
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
175
|
+
text = block.get("text", "")
|
|
176
|
+
final_text = process_text_for_controls(
|
|
177
|
+
controls_runner, text, accumulator
|
|
178
|
+
)
|
|
179
|
+
redacted_content.append({**block, "text": final_text})
|
|
180
|
+
else:
|
|
181
|
+
redacted_content.append(block)
|
|
182
|
+
processed_messages.append({**msg, "content": redacted_content})
|
|
183
|
+
else:
|
|
184
|
+
processed_messages.append(msg)
|
|
185
|
+
else:
|
|
186
|
+
processed_messages.append(msg)
|
|
187
|
+
|
|
188
|
+
kwargs["messages"] = processed_messages
|
|
189
|
+
messages = processed_messages
|
|
149
190
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
debug=debug,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
client_kwargs: dict[str, Any] = {**anthropic_kwargs}
|
|
171
|
-
if anthropic_api_key:
|
|
172
|
-
client_kwargs["api_key"] = anthropic_api_key
|
|
173
|
-
|
|
174
|
-
client = AsyncAnthropic(**client_kwargs)
|
|
175
|
-
|
|
176
|
-
original_create = client.messages.create
|
|
177
|
-
|
|
178
|
-
async def attested_create(*args: Any, **kwargs: Any) -> Any:
|
|
179
|
-
messages = kwargs.get("messages", [])
|
|
180
|
-
model = kwargs.get("model", "unknown")
|
|
181
|
-
system = kwargs.get("system")
|
|
191
|
+
if debug:
|
|
192
|
+
if accumulator.pii_summary:
|
|
193
|
+
print(
|
|
194
|
+
f"[glacis] PII redacted: {accumulator.pii_summary.categories} "
|
|
195
|
+
f"({accumulator.pii_summary.count} items)"
|
|
196
|
+
)
|
|
197
|
+
if accumulator.jailbreak_summary and accumulator.jailbreak_summary.detected:
|
|
198
|
+
print(
|
|
199
|
+
f"[glacis] Jailbreak detected: "
|
|
200
|
+
f"score={accumulator.jailbreak_summary.score:.2f}, "
|
|
201
|
+
f"action={accumulator.jailbreak_summary.action}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Build control plane attestation
|
|
205
|
+
control_plane_results = create_control_plane_attestation_from_accumulator(
|
|
206
|
+
accumulator, cfg, model, "anthropic", "messages.create"
|
|
207
|
+
)
|
|
182
208
|
|
|
183
|
-
|
|
209
|
+
# Check if we need to block BEFORE making the API call
|
|
210
|
+
if accumulator.should_block:
|
|
211
|
+
handle_blocked_request(
|
|
212
|
+
glacis_client=glacis,
|
|
213
|
+
service_id=effective_service_id,
|
|
214
|
+
input_data={
|
|
215
|
+
"model": model,
|
|
216
|
+
"messages": messages,
|
|
217
|
+
"system": kwargs.get("system")
|
|
218
|
+
},
|
|
219
|
+
control_plane_results=control_plane_results,
|
|
220
|
+
provider="anthropic",
|
|
221
|
+
model=model,
|
|
222
|
+
jailbreak_score=accumulator.jailbreak_summary.score
|
|
223
|
+
if accumulator.jailbreak_summary
|
|
224
|
+
else 0.0,
|
|
225
|
+
debug=debug,
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
control_plane_results = None
|
|
229
|
+
|
|
230
|
+
# Make the API call (only if not blocked)
|
|
231
|
+
response = original_create(*args, **kwargs)
|
|
184
232
|
|
|
233
|
+
# Build input/output data
|
|
234
|
+
input_data: dict[str, Any] = {"model": model, "messages": messages}
|
|
235
|
+
if system:
|
|
236
|
+
input_data["system"] = kwargs.get("system", system)
|
|
237
|
+
|
|
238
|
+
output_data = {
|
|
239
|
+
"model": response.model,
|
|
240
|
+
"content": [
|
|
241
|
+
{"type": block.type, "text": getattr(block, "text", None)}
|
|
242
|
+
for block in response.content
|
|
243
|
+
],
|
|
244
|
+
"stop_reason": response.stop_reason,
|
|
245
|
+
"usage": {
|
|
246
|
+
"input_tokens": response.usage.input_tokens,
|
|
247
|
+
"output_tokens": response.usage.output_tokens,
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Attest and store
|
|
185
252
|
try:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"messages": messages,
|
|
189
|
-
}
|
|
190
|
-
if system:
|
|
191
|
-
input_data["system"] = system
|
|
192
|
-
|
|
193
|
-
await glacis.attest(
|
|
194
|
-
service_id=service_id,
|
|
253
|
+
receipt = glacis.attest(
|
|
254
|
+
service_id=effective_service_id,
|
|
195
255
|
operation_type="completion",
|
|
196
256
|
input=input_data,
|
|
197
|
-
output=
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
"output_tokens": response.usage.output_tokens,
|
|
210
|
-
},
|
|
211
|
-
},
|
|
257
|
+
output=output_data,
|
|
258
|
+
metadata={"provider": "anthropic", "model": model},
|
|
259
|
+
control_plane_results=control_plane_results,
|
|
260
|
+
)
|
|
261
|
+
set_last_receipt(receipt)
|
|
262
|
+
store_evidence(
|
|
263
|
+
receipt=receipt,
|
|
264
|
+
service_id=effective_service_id,
|
|
265
|
+
operation_type="completion",
|
|
266
|
+
input_data=input_data,
|
|
267
|
+
output_data=output_data,
|
|
268
|
+
control_plane_results=control_plane_results,
|
|
212
269
|
metadata={"provider": "anthropic", "model": model},
|
|
270
|
+
debug=debug,
|
|
213
271
|
)
|
|
214
272
|
except Exception as e:
|
|
215
273
|
if debug:
|
|
@@ -218,5 +276,12 @@ def attested_async_anthropic(
|
|
|
218
276
|
return response
|
|
219
277
|
|
|
220
278
|
client.messages.create = attested_create # type: ignore
|
|
221
|
-
|
|
222
279
|
return client
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
__all__ = [
|
|
283
|
+
"attested_anthropic",
|
|
284
|
+
"get_last_receipt",
|
|
285
|
+
"get_evidence",
|
|
286
|
+
"GlacisBlockedError",
|
|
287
|
+
]
|