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.
@@ -0,0 +1,476 @@
1
+ """
2
+ GLACIS integration base module.
3
+
4
+ Provides shared functionality for all provider integrations:
5
+ - GlacisBlockedError exception
6
+ - Thread-local receipt storage
7
+ - Evidence retrieval
8
+ - Logger suppression
9
+ - Config and client initialization helpers
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import threading
16
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Union
17
+
18
+ if TYPE_CHECKING:
19
+ from glacis import Glacis
20
+ from glacis.config import GlacisConfig
21
+ from glacis.controls import ControlsRunner
22
+ from glacis.models import (
23
+ AttestReceipt,
24
+ ControlExecution,
25
+ ControlPlaneAttestation,
26
+ JailbreakSummary,
27
+ OfflineAttestReceipt,
28
+ PiiPhiSummary,
29
+ )
30
+
31
+
32
+ # Thread-local storage for the last receipt
33
+ _thread_local = threading.local()
34
+
35
+
36
+ class GlacisBlockedError(Exception):
37
+ """Raised when a control blocks the request."""
38
+
39
+ def __init__(self, message: str, control_type: str, score: Optional[float] = None):
40
+ super().__init__(message)
41
+ self.control_type = control_type
42
+ self.score = score
43
+
44
+
45
+ def get_last_receipt() -> Optional[Union["AttestReceipt", "OfflineAttestReceipt"]]:
46
+ """
47
+ Get the last attestation receipt from the current thread.
48
+
49
+ Returns:
50
+ The last AttestReceipt or OfflineAttestReceipt, or None if no attestation
51
+ has been made in this thread.
52
+ """
53
+ return getattr(_thread_local, "last_receipt", None)
54
+
55
+
56
+ def set_last_receipt(receipt: Union["AttestReceipt", "OfflineAttestReceipt"]) -> None:
57
+ """Store the last receipt in thread-local storage."""
58
+ _thread_local.last_receipt = receipt
59
+
60
+
61
+ def get_evidence(attestation_id: str) -> Optional[dict[str, Any]]:
62
+ """
63
+ Get the full evidence for an attestation by ID.
64
+
65
+ Evidence includes the full input, output, and control_plane_results that
66
+ were attested. This data is stored locally and never sent to GLACIS servers.
67
+
68
+ Args:
69
+ attestation_id: The attestation ID (att_xxx or oatt_xxx)
70
+
71
+ Returns:
72
+ Dict with input, output, control_plane_results, and metadata,
73
+ or None if not found
74
+
75
+ Example:
76
+ >>> receipt = get_last_receipt()
77
+ >>> evidence = get_evidence(receipt.attestation_id)
78
+ >>> print(evidence["input"]["messages"])
79
+ >>> print(evidence["control_plane_results"]["pii_phi"])
80
+ """
81
+ from glacis.storage import ReceiptStorage
82
+
83
+ storage = ReceiptStorage()
84
+ return storage.get_evidence(attestation_id)
85
+
86
+
87
+ # Loggers to suppress for clean customer experience
88
+ NOISY_LOGGERS = [
89
+ "glacis",
90
+ "presidio-analyzer",
91
+ "presidio-anonymizer",
92
+ "presidio_analyzer",
93
+ "presidio_anonymizer",
94
+ "spacy",
95
+ "httpx",
96
+ "httpcore",
97
+ "httpcore.http11",
98
+ "httpcore.connection",
99
+ "transformers",
100
+ "urllib3",
101
+ "urllib3.connectionpool",
102
+ "huggingface_hub",
103
+ "filelock",
104
+ ]
105
+
106
+
107
+ def suppress_noisy_loggers(provider_loggers: list[str] | None = None) -> None:
108
+ """
109
+ Suppress noisy third-party loggers for clean customer experience.
110
+
111
+ Args:
112
+ provider_loggers: Additional provider-specific loggers to suppress
113
+ """
114
+ loggers = NOISY_LOGGERS.copy()
115
+ if provider_loggers:
116
+ loggers.extend(provider_loggers)
117
+
118
+ for logger_name in loggers:
119
+ logging.getLogger(logger_name).setLevel(logging.WARNING)
120
+
121
+
122
+ def initialize_config(
123
+ config_path: Optional[str],
124
+ redaction: Union[bool, Literal["fast", "full"], None],
125
+ offline: Optional[bool],
126
+ glacis_api_key: Optional[str],
127
+ default_service_id: str,
128
+ service_id: str,
129
+ ) -> tuple["GlacisConfig", bool, str]:
130
+ """
131
+ Initialize and configure Glacis settings.
132
+
133
+ Args:
134
+ config_path: Path to glacis.yaml config file
135
+ redaction: PII redaction mode override
136
+ offline: Offline mode override
137
+ glacis_api_key: Glacis API key (implies online mode if provided)
138
+ default_service_id: Default service ID for this provider
139
+ service_id: User-provided service ID
140
+
141
+ Returns:
142
+ Tuple of (config, effective_offline, effective_service_id)
143
+ """
144
+ from glacis.config import load_config
145
+
146
+ cfg: GlacisConfig = load_config(config_path)
147
+
148
+ # Handle backward-compatible redaction parameter
149
+ if redaction is not None:
150
+ if redaction is True:
151
+ cfg.controls.pii_phi.enabled = True
152
+ cfg.controls.pii_phi.mode = "fast"
153
+ elif redaction is False:
154
+ cfg.controls.pii_phi.enabled = False
155
+ else:
156
+ cfg.controls.pii_phi.enabled = True
157
+ cfg.controls.pii_phi.mode = redaction
158
+
159
+ # Determine offline mode
160
+ if offline is not None:
161
+ effective_offline = offline
162
+ elif glacis_api_key:
163
+ effective_offline = False
164
+ else:
165
+ effective_offline = cfg.attestation.offline
166
+
167
+ # Determine service ID
168
+ effective_service_id = (
169
+ service_id if service_id != default_service_id else cfg.attestation.service_id
170
+ )
171
+
172
+ return cfg, effective_offline, effective_service_id
173
+
174
+
175
+ def create_glacis_client(
176
+ offline: bool,
177
+ signing_seed: Optional[bytes],
178
+ glacis_api_key: Optional[str],
179
+ glacis_base_url: str,
180
+ debug: bool,
181
+ ) -> "Glacis":
182
+ """
183
+ Create a Glacis client (online or offline).
184
+
185
+ Args:
186
+ offline: Whether to use offline mode
187
+ signing_seed: 32-byte signing seed (required for offline)
188
+ glacis_api_key: API key (required for online)
189
+ glacis_base_url: Base URL for Glacis API
190
+ debug: Enable debug logging
191
+
192
+ Returns:
193
+ Configured Glacis client
194
+
195
+ Raises:
196
+ ValueError: If required parameters are missing
197
+ """
198
+ from glacis import Glacis
199
+
200
+ if offline:
201
+ if not signing_seed:
202
+ raise ValueError("signing_seed is required for offline mode")
203
+ return Glacis(
204
+ mode="offline",
205
+ signing_seed=signing_seed,
206
+ debug=debug,
207
+ )
208
+ else:
209
+ if not glacis_api_key:
210
+ raise ValueError("glacis_api_key is required for online mode")
211
+ return Glacis(
212
+ api_key=glacis_api_key,
213
+ base_url=glacis_base_url,
214
+ debug=debug,
215
+ )
216
+
217
+
218
+ def create_controls_runner(
219
+ cfg: "GlacisConfig",
220
+ debug: bool,
221
+ ) -> Optional["ControlsRunner"]:
222
+ """
223
+ Create controls runner if any control is enabled.
224
+
225
+ Args:
226
+ cfg: Glacis configuration
227
+ debug: Enable debug logging
228
+
229
+ Returns:
230
+ ControlsRunner if any control is enabled, None otherwise
231
+ """
232
+ if cfg.controls.pii_phi.enabled or cfg.controls.jailbreak.enabled:
233
+ from glacis.controls import ControlsRunner
234
+ return ControlsRunner(cfg.controls, debug=debug)
235
+ return None
236
+
237
+
238
+ def store_evidence(
239
+ receipt: Union["AttestReceipt", "OfflineAttestReceipt"],
240
+ service_id: str,
241
+ operation_type: str,
242
+ input_data: dict[str, Any],
243
+ output_data: dict[str, Any],
244
+ control_plane_results: Optional["ControlPlaneAttestation"],
245
+ metadata: dict[str, Any],
246
+ debug: bool,
247
+ ) -> None:
248
+ """
249
+ Store attestation evidence locally for audit trail.
250
+
251
+ Args:
252
+ receipt: Attestation receipt
253
+ service_id: Service identifier
254
+ operation_type: Type of operation
255
+ input_data: Input payload
256
+ output_data: Output payload
257
+ control_plane_results: Control plane attestation
258
+ metadata: Additional metadata
259
+ debug: Enable debug logging
260
+ """
261
+ from glacis.models import OfflineAttestReceipt
262
+ from glacis.storage import ReceiptStorage
263
+
264
+ storage = ReceiptStorage()
265
+ attestation_hash = (
266
+ receipt.payload_hash
267
+ if isinstance(receipt, OfflineAttestReceipt)
268
+ else receipt.attestation_hash
269
+ )
270
+ storage.store_evidence(
271
+ attestation_id=receipt.attestation_id,
272
+ attestation_hash=attestation_hash,
273
+ mode="offline" if isinstance(receipt, OfflineAttestReceipt) else "online",
274
+ service_id=service_id,
275
+ operation_type=operation_type,
276
+ timestamp=receipt.timestamp,
277
+ input_data=input_data,
278
+ output_data=output_data,
279
+ control_plane_results=control_plane_results,
280
+ metadata=metadata,
281
+ )
282
+ if debug:
283
+ print(f"[glacis] Attestation created: {receipt.attestation_id}")
284
+
285
+
286
+ __all__ = [
287
+ "GlacisBlockedError",
288
+ "get_last_receipt",
289
+ "set_last_receipt",
290
+ "get_evidence",
291
+ "suppress_noisy_loggers",
292
+ "initialize_config",
293
+ "create_glacis_client",
294
+ "create_controls_runner",
295
+ "store_evidence",
296
+ "NOISY_LOGGERS",
297
+ "ControlResultsAccumulator",
298
+ "process_text_for_controls",
299
+ "create_control_plane_attestation_from_accumulator",
300
+ "handle_blocked_request",
301
+ ]
302
+
303
+
304
+ # --- Shared Control Execution Logic ---
305
+
306
+ class ControlResultsAccumulator:
307
+ """Accumulates results from multiple control execution runs."""
308
+
309
+ def __init__(self) -> None:
310
+ self.pii_summary: Optional["PiiPhiSummary"] = None
311
+ self.jailbreak_summary: Optional["JailbreakSummary"] = None
312
+ self.control_executions: list["ControlExecution"] = []
313
+ self.should_block: bool = False
314
+
315
+ def update(self, results: list[Any]) -> None:
316
+ """Update accumulator with check results."""
317
+ from glacis.models import ControlExecution, JailbreakSummary, PiiPhiSummary
318
+
319
+ for result in results:
320
+ if result.control_type == "pii" and result.detected:
321
+ if self.pii_summary:
322
+ self.pii_summary.categories = sorted(
323
+ set(self.pii_summary.categories) | set(result.categories)
324
+ )
325
+ self.pii_summary.count += len(result.categories)
326
+ else:
327
+ self.pii_summary = PiiPhiSummary(
328
+ detected=True,
329
+ action="redacted",
330
+ categories=result.categories,
331
+ count=len(result.categories),
332
+ )
333
+ self.control_executions.append(
334
+ ControlExecution(
335
+ id="glacis-pii-redactor",
336
+ type="pii",
337
+ version="0.3.0",
338
+ provider="glacis",
339
+ latency_ms=result.latency_ms,
340
+ status="flag",
341
+ )
342
+ )
343
+
344
+ elif result.control_type == "jailbreak":
345
+ if not self.jailbreak_summary or (result.score or 0) > self.jailbreak_summary.score:
346
+ self.jailbreak_summary = JailbreakSummary(
347
+ detected=result.detected,
348
+ score=result.score or 0.0,
349
+ action=result.action,
350
+ categories=result.categories,
351
+ backend=result.metadata.get("backend", ""),
352
+ )
353
+ self.control_executions.append(
354
+ ControlExecution(
355
+ id="glacis-jailbreak-detector",
356
+ type="jailbreak",
357
+ version="0.3.0",
358
+ provider="glacis",
359
+ latency_ms=result.latency_ms,
360
+ status=result.action if result.detected else "pass",
361
+ )
362
+ )
363
+ if result.action == "block":
364
+ self.should_block = True
365
+
366
+
367
+ def process_text_for_controls(
368
+ runner: "ControlsRunner",
369
+ text: str,
370
+ accumulator: ControlResultsAccumulator
371
+ ) -> str:
372
+ """Run controls on text, update accumulator, and return (potentially redacted) text."""
373
+ results = runner.run(text)
374
+ accumulator.update(results)
375
+ final_text = runner.get_final_text(results) or text
376
+ return final_text
377
+
378
+
379
+ def create_control_plane_attestation_from_accumulator(
380
+ accumulator: ControlResultsAccumulator,
381
+ cfg: "GlacisConfig",
382
+ model: str,
383
+ provider: str,
384
+ endpoint: str,
385
+ ) -> Any:
386
+ """Create ControlPlaneAttestation from accumulated results."""
387
+ from glacis.models import (
388
+ ControlPlaneAttestation,
389
+ Determination,
390
+ ModelInfo,
391
+ PolicyContext,
392
+ PolicyScope,
393
+ SafetyScores,
394
+ SamplingDecision,
395
+ SamplingMetadata,
396
+ )
397
+
398
+ action: Literal["forwarded", "redacted", "blocked"]
399
+ trigger: Optional[str]
400
+ if accumulator.pii_summary:
401
+ action, trigger = "redacted", "pii"
402
+ elif accumulator.jailbreak_summary and accumulator.jailbreak_summary.detected:
403
+ action = "blocked" if accumulator.jailbreak_summary.action == "block" else "forwarded"
404
+ trigger = "jailbreak"
405
+ else:
406
+ action, trigger = "forwarded", None
407
+
408
+ return ControlPlaneAttestation(
409
+ policy=PolicyContext(
410
+ id=cfg.policy.id,
411
+ version=cfg.policy.version,
412
+ model=ModelInfo(model_id=model, provider=provider),
413
+ scope=PolicyScope(
414
+ tenant_id=cfg.policy.tenant_id,
415
+ endpoint=endpoint,
416
+ ),
417
+ ),
418
+ determination=Determination(action=action, trigger=trigger, confidence=1.0),
419
+ controls=accumulator.control_executions,
420
+ safety=SafetyScores(
421
+ overall_risk=accumulator.jailbreak_summary.score
422
+ if accumulator.jailbreak_summary
423
+ else 0.0
424
+ ),
425
+ pii_phi=accumulator.pii_summary,
426
+ jailbreak=accumulator.jailbreak_summary,
427
+ sampling=SamplingMetadata(
428
+ level="L0",
429
+ decision=SamplingDecision(sampled=True, reason="forced", rate=1.0),
430
+ ),
431
+ )
432
+
433
+
434
+ def handle_blocked_request(
435
+ glacis_client: "Glacis",
436
+ service_id: str,
437
+ input_data: dict[str, Any],
438
+ control_plane_results: Any,
439
+ provider: str,
440
+ model: str,
441
+ jailbreak_score: float,
442
+ debug: bool,
443
+ ) -> None:
444
+ """Attest a blocked request and raise GlacisBlockedError."""
445
+ output_data = {"blocked": True, "reason": "jailbreak_detected"}
446
+ metadata = {"provider": provider, "model": model, "blocked": str(True)}
447
+
448
+ try:
449
+ receipt = glacis_client.attest(
450
+ service_id=service_id,
451
+ operation_type="completion",
452
+ input=input_data,
453
+ output=output_data,
454
+ metadata=metadata,
455
+ control_plane_results=control_plane_results,
456
+ )
457
+ set_last_receipt(receipt)
458
+ store_evidence(
459
+ receipt=receipt,
460
+ service_id=service_id,
461
+ operation_type="completion",
462
+ input_data=input_data,
463
+ output_data=output_data,
464
+ control_plane_results=control_plane_results,
465
+ metadata=metadata,
466
+ debug=debug,
467
+ )
468
+ except Exception as e:
469
+ if debug:
470
+ print(f"[glacis] Attestation failed: {e}")
471
+
472
+ raise GlacisBlockedError(
473
+ f"Jailbreak detected (score={jailbreak_score:.2f})",
474
+ control_type="jailbreak",
475
+ score=jailbreak_score,
476
+ )