glacis 0.1.4__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 +209 -16
- 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.4.dist-info/METADATA +0 -324
- glacis-0.1.4.dist-info/RECORD +0 -16
- {glacis-0.1.4.dist-info → glacis-0.2.0.dist-info}/WHEEL +0 -0
- {glacis-0.1.4.dist-info → glacis-0.2.0.dist-info}/licenses/LICENSE +0 -0
glacis/__init__.py
CHANGED
|
@@ -37,6 +37,13 @@ Streaming Example:
|
|
|
37
37
|
... })
|
|
38
38
|
>>> await session.attest_chunk(input=audio_chunk, output=transcript)
|
|
39
39
|
>>> receipt = await session.end(metadata={"duration": "00:05:23"})
|
|
40
|
+
|
|
41
|
+
Controls Example:
|
|
42
|
+
>>> from glacis.controls import ControlsRunner, PIIControl, JailbreakControl
|
|
43
|
+
>>> from glacis.config import load_config
|
|
44
|
+
>>> cfg = load_config() # Loads glacis.yaml
|
|
45
|
+
>>> runner = ControlsRunner(cfg.controls)
|
|
46
|
+
>>> results = runner.run("Patient SSN: 123-45-6789")
|
|
40
47
|
"""
|
|
41
48
|
|
|
42
49
|
from glacis.client import AsyncGlacis, Glacis, GlacisMode
|
|
@@ -44,26 +51,56 @@ from glacis.crypto import canonical_json, hash_payload
|
|
|
44
51
|
from glacis.models import (
|
|
45
52
|
AttestInput,
|
|
46
53
|
AttestReceipt,
|
|
54
|
+
ControlExecution,
|
|
55
|
+
ControlPlaneAttestation,
|
|
56
|
+
ControlStatus,
|
|
57
|
+
ControlType,
|
|
58
|
+
Determination,
|
|
59
|
+
GlacisApiError,
|
|
47
60
|
GlacisConfig,
|
|
61
|
+
JailbreakSummary,
|
|
48
62
|
LogEntry,
|
|
49
63
|
LogQueryParams,
|
|
50
64
|
LogQueryResult,
|
|
51
65
|
MerkleInclusionProof,
|
|
66
|
+
ModelInfo,
|
|
52
67
|
OfflineAttestReceipt,
|
|
53
68
|
OfflineVerifyResult,
|
|
69
|
+
PiiPhiSummary,
|
|
70
|
+
PolicyContext,
|
|
71
|
+
PolicyScope,
|
|
72
|
+
SafetyScores,
|
|
73
|
+
SamplingDecision,
|
|
74
|
+
SamplingMetadata,
|
|
54
75
|
SignedTreeHead,
|
|
55
76
|
VerifyResult,
|
|
56
77
|
)
|
|
57
78
|
from glacis.storage import ReceiptStorage
|
|
58
79
|
from glacis.streaming import SessionContext, SessionReceipt, StreamingSession
|
|
59
80
|
|
|
60
|
-
|
|
81
|
+
# Controls module (optional dependencies for individual controls)
|
|
82
|
+
try:
|
|
83
|
+
from glacis.controls import ( # noqa: F401
|
|
84
|
+
BaseControl,
|
|
85
|
+
ControlResult,
|
|
86
|
+
ControlsRunner,
|
|
87
|
+
JailbreakControl,
|
|
88
|
+
PIIControl,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
_CONTROLS_AVAILABLE = True
|
|
92
|
+
except ImportError:
|
|
93
|
+
_CONTROLS_AVAILABLE = False
|
|
94
|
+
|
|
95
|
+
__version__ = "0.3.0"
|
|
61
96
|
|
|
62
97
|
__all__ = [
|
|
63
98
|
# Main classes
|
|
64
99
|
"Glacis",
|
|
65
100
|
"AsyncGlacis",
|
|
66
101
|
"GlacisMode",
|
|
102
|
+
# Exceptions
|
|
103
|
+
"GlacisApiError",
|
|
67
104
|
# Streaming
|
|
68
105
|
"StreamingSession",
|
|
69
106
|
"SessionContext",
|
|
@@ -82,7 +119,31 @@ __all__ = [
|
|
|
82
119
|
"LogEntry",
|
|
83
120
|
"MerkleInclusionProof",
|
|
84
121
|
"SignedTreeHead",
|
|
122
|
+
# Control Plane
|
|
123
|
+
"ControlPlaneAttestation",
|
|
124
|
+
"PolicyContext",
|
|
125
|
+
"PolicyScope",
|
|
126
|
+
"ModelInfo",
|
|
127
|
+
"Determination",
|
|
128
|
+
"ControlExecution",
|
|
129
|
+
"ControlType",
|
|
130
|
+
"ControlStatus",
|
|
131
|
+
"SafetyScores",
|
|
132
|
+
"PiiPhiSummary",
|
|
133
|
+
"JailbreakSummary",
|
|
134
|
+
"SamplingMetadata",
|
|
135
|
+
"SamplingDecision",
|
|
85
136
|
# Crypto utilities
|
|
86
137
|
"canonical_json",
|
|
87
138
|
"hash_payload",
|
|
88
139
|
]
|
|
140
|
+
|
|
141
|
+
# Add controls exports if available
|
|
142
|
+
if _CONTROLS_AVAILABLE:
|
|
143
|
+
__all__.extend([
|
|
144
|
+
"BaseControl",
|
|
145
|
+
"ControlResult",
|
|
146
|
+
"ControlsRunner",
|
|
147
|
+
"PIIControl",
|
|
148
|
+
"JailbreakControl",
|
|
149
|
+
])
|
glacis/__main__.py
CHANGED
|
@@ -5,86 +5,7 @@ Usage:
|
|
|
5
5
|
python -m glacis verify <receipt.json>
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
import json
|
|
10
|
-
import sys
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Any, Union
|
|
13
|
-
|
|
14
|
-
from glacis import Glacis
|
|
15
|
-
from glacis.models import AttestReceipt, OfflineAttestReceipt, OfflineVerifyResult, VerifyResult
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def verify_command(args: argparse.Namespace) -> None:
|
|
19
|
-
"""Verify a receipt file."""
|
|
20
|
-
receipt_path = Path(args.receipt)
|
|
21
|
-
|
|
22
|
-
if not receipt_path.exists():
|
|
23
|
-
print(f"Error: File not found: {receipt_path}", file=sys.stderr)
|
|
24
|
-
sys.exit(1)
|
|
25
|
-
|
|
26
|
-
try:
|
|
27
|
-
with open(receipt_path) as f:
|
|
28
|
-
data: dict[str, Any] = json.load(f)
|
|
29
|
-
except json.JSONDecodeError as e:
|
|
30
|
-
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
|
|
31
|
-
sys.exit(1)
|
|
32
|
-
|
|
33
|
-
# Determine receipt type and verify
|
|
34
|
-
receipt: Union[AttestReceipt, OfflineAttestReceipt]
|
|
35
|
-
result: Union[VerifyResult, OfflineVerifyResult]
|
|
36
|
-
|
|
37
|
-
if data.get("attestation_id", "").startswith("oatt_"):
|
|
38
|
-
offline_receipt = OfflineAttestReceipt(**data)
|
|
39
|
-
# For verification, we need a signing_seed but it's not used
|
|
40
|
-
# since we verify using the public key from the receipt
|
|
41
|
-
dummy_seed = bytes(32) # All zeros - only used to satisfy constructor
|
|
42
|
-
glacis = Glacis(mode="offline", signing_seed=dummy_seed)
|
|
43
|
-
result = glacis.verify(offline_receipt)
|
|
44
|
-
receipt = offline_receipt
|
|
45
|
-
else:
|
|
46
|
-
online_receipt = AttestReceipt(**data)
|
|
47
|
-
# Online verification doesn't require API key for public receipts
|
|
48
|
-
glacis = Glacis(api_key="verify_only")
|
|
49
|
-
result = glacis.verify(online_receipt)
|
|
50
|
-
receipt = online_receipt
|
|
51
|
-
|
|
52
|
-
# Output
|
|
53
|
-
print(f"Receipt: {receipt.attestation_id}")
|
|
54
|
-
print(f"Type: {'Offline' if isinstance(receipt, OfflineAttestReceipt) else 'Online'}")
|
|
55
|
-
print()
|
|
56
|
-
|
|
57
|
-
if result.valid:
|
|
58
|
-
print("Status: VALID")
|
|
59
|
-
# Get signature validity based on result type
|
|
60
|
-
if isinstance(result, OfflineVerifyResult):
|
|
61
|
-
sig_valid = result.signature_valid
|
|
62
|
-
else:
|
|
63
|
-
sig_valid = result.verification.signature_valid
|
|
64
|
-
print(f" Signature: {'PASS' if sig_valid else 'FAIL'}")
|
|
65
|
-
if isinstance(result, VerifyResult):
|
|
66
|
-
print(f" Merkle proof: {'PASS' if result.verification.proof_valid else 'FAIL'}")
|
|
67
|
-
else:
|
|
68
|
-
print("Status: INVALID")
|
|
69
|
-
if result.error:
|
|
70
|
-
print(f" Error: {result.error}")
|
|
71
|
-
sys.exit(1)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def main() -> None:
|
|
75
|
-
parser = argparse.ArgumentParser(
|
|
76
|
-
prog="glacis", description="Glacis CLI - Cryptographic attestation for AI systems"
|
|
77
|
-
)
|
|
78
|
-
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
79
|
-
|
|
80
|
-
# verify command
|
|
81
|
-
verify_parser = subparsers.add_parser("verify", help="Verify a receipt")
|
|
82
|
-
verify_parser.add_argument("receipt", help="Path to receipt JSON file")
|
|
83
|
-
verify_parser.set_defaults(func=verify_command)
|
|
84
|
-
|
|
85
|
-
args = parser.parse_args()
|
|
86
|
-
args.func(args)
|
|
87
|
-
|
|
8
|
+
from glacis.verify import main
|
|
88
9
|
|
|
89
10
|
if __name__ == "__main__":
|
|
90
11
|
main()
|
glacis/client.py
CHANGED
|
@@ -8,7 +8,7 @@ your infrastructure.
|
|
|
8
8
|
|
|
9
9
|
Supports two modes:
|
|
10
10
|
- Online (default): Sends attestations to api.glacis.io for witnessing
|
|
11
|
-
- Offline: Signs attestations locally using Ed25519
|
|
11
|
+
- Offline: Signs attestations locally using Ed25519
|
|
12
12
|
|
|
13
13
|
Example (online):
|
|
14
14
|
>>> from glacis import Glacis
|
|
@@ -43,6 +43,7 @@ import httpx
|
|
|
43
43
|
from glacis.crypto import hash_payload
|
|
44
44
|
from glacis.models import (
|
|
45
45
|
AttestReceipt,
|
|
46
|
+
ControlPlaneAttestation,
|
|
46
47
|
GlacisApiError,
|
|
47
48
|
GlacisRateLimitError,
|
|
48
49
|
LogQueryResult,
|
|
@@ -53,8 +54,8 @@ from glacis.models import (
|
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
if TYPE_CHECKING:
|
|
57
|
+
from glacis.crypto import Ed25519Runtime
|
|
56
58
|
from glacis.storage import ReceiptStorage
|
|
57
|
-
from glacis.wasm_runtime import WasmRuntime
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
class GlacisMode(str, Enum):
|
|
@@ -136,7 +137,7 @@ class Glacis:
|
|
|
136
137
|
self._storage: Optional["ReceiptStorage"] = None
|
|
137
138
|
self._signing_seed: Optional[bytes] = None
|
|
138
139
|
self._public_key: Optional[str] = None
|
|
139
|
-
self.
|
|
140
|
+
self._ed25519: Optional["Ed25519Runtime"] = None
|
|
140
141
|
else:
|
|
141
142
|
# Offline mode
|
|
142
143
|
if not signing_seed:
|
|
@@ -148,11 +149,11 @@ class Glacis:
|
|
|
148
149
|
self._signing_seed = signing_seed
|
|
149
150
|
self._client = None # No HTTP client needed
|
|
150
151
|
|
|
151
|
-
# Initialize
|
|
152
|
-
from glacis.
|
|
152
|
+
# Initialize Ed25519 runtime and derive public key
|
|
153
|
+
from glacis.crypto import get_ed25519_runtime
|
|
153
154
|
|
|
154
|
-
self.
|
|
155
|
-
self._public_key = self.
|
|
155
|
+
self._ed25519 = get_ed25519_runtime()
|
|
156
|
+
self._public_key = self._ed25519.get_public_key_hex(signing_seed)
|
|
156
157
|
|
|
157
158
|
# Initialize storage
|
|
158
159
|
from glacis.storage import ReceiptStorage
|
|
@@ -183,20 +184,23 @@ class Glacis:
|
|
|
183
184
|
input: Any,
|
|
184
185
|
output: Any,
|
|
185
186
|
metadata: Optional[dict[str, str]] = None,
|
|
187
|
+
control_plane_results: Optional[ControlPlaneAttestation] = None,
|
|
186
188
|
) -> Union[AttestReceipt, OfflineAttestReceipt]:
|
|
187
189
|
"""
|
|
188
190
|
Attest an AI operation.
|
|
189
191
|
|
|
190
|
-
The input and
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
The input, output, and control_plane_results are hashed locally using RFC 8785
|
|
193
|
+
canonical JSON + SHA-256. Only the hash is sent to the server - the actual
|
|
194
|
+
data never leaves your infrastructure (zero egress).
|
|
193
195
|
|
|
194
196
|
Args:
|
|
195
197
|
service_id: Service identifier (e.g., "my-ai-service")
|
|
196
198
|
operation_type: Type of operation (inference, embedding, completion, classification)
|
|
197
199
|
input: Input data (hashed locally, never sent)
|
|
198
200
|
output: Output data (hashed locally, never sent)
|
|
199
|
-
metadata: Optional metadata (
|
|
201
|
+
metadata: Optional metadata (stored locally for evidence)
|
|
202
|
+
control_plane_results: Optional control plane attestation
|
|
203
|
+
(hashed locally, cryptographically bound)
|
|
200
204
|
|
|
201
205
|
Returns:
|
|
202
206
|
AttestReceipt (online) or OfflineAttestReceipt (offline)
|
|
@@ -205,32 +209,47 @@ class Glacis:
|
|
|
205
209
|
GlacisApiError: On API errors (online mode)
|
|
206
210
|
GlacisRateLimitError: When rate limited (online mode)
|
|
207
211
|
"""
|
|
208
|
-
|
|
212
|
+
# Build payload to hash - includes control_plane_results for cryptographic binding
|
|
213
|
+
payload_to_hash: dict[str, Any] = {"input": input, "output": output}
|
|
214
|
+
if control_plane_results:
|
|
215
|
+
payload_to_hash["control_plane_results"] = (
|
|
216
|
+
control_plane_results.model_dump(by_alias=True)
|
|
217
|
+
)
|
|
218
|
+
payload_hash = self.hash(payload_to_hash)
|
|
209
219
|
|
|
210
220
|
if self.mode == GlacisMode.OFFLINE:
|
|
211
221
|
return self._attest_offline(
|
|
212
|
-
service_id, operation_type, payload_hash,
|
|
222
|
+
service_id, operation_type, payload_hash,
|
|
223
|
+
input, output, metadata, control_plane_results,
|
|
213
224
|
)
|
|
214
225
|
|
|
215
|
-
return self._attest_online(service_id, operation_type, payload_hash,
|
|
226
|
+
return self._attest_online(service_id, operation_type, payload_hash, control_plane_results)
|
|
216
227
|
|
|
217
228
|
def _attest_online(
|
|
218
229
|
self,
|
|
219
230
|
service_id: str,
|
|
220
231
|
operation_type: str,
|
|
221
232
|
payload_hash: str,
|
|
222
|
-
|
|
233
|
+
control_plane_results: Optional[ControlPlaneAttestation] = None,
|
|
223
234
|
) -> AttestReceipt:
|
|
224
|
-
"""Create a server-witnessed attestation.
|
|
235
|
+
"""Create a server-witnessed attestation.
|
|
236
|
+
|
|
237
|
+
The server is a pure witness service - it only receives the hash and
|
|
238
|
+
adds it to the Merkle tree. All rich metadata (input, output, control_plane_results)
|
|
239
|
+
stays in the customer's infrastructure for zero-egress compliance.
|
|
240
|
+
|
|
241
|
+
The control_plane_results is cryptographically bound via the payload_hash (which
|
|
242
|
+
includes control_plane_results in its computation).
|
|
243
|
+
"""
|
|
225
244
|
self._debug(f"Attesting (online): service_id={service_id}, hash={payload_hash[:16]}...")
|
|
226
245
|
|
|
246
|
+
# Only send what the server needs - hash goes into Merkle tree
|
|
247
|
+
# metadata and control_plane_results are NOT sent (zero egress - stored locally)
|
|
227
248
|
body: dict[str, Any] = {
|
|
228
249
|
"serviceId": service_id,
|
|
229
250
|
"operationType": operation_type,
|
|
230
251
|
"payloadHash": payload_hash,
|
|
231
252
|
}
|
|
232
|
-
if metadata:
|
|
233
|
-
body["metadata"] = metadata
|
|
234
253
|
|
|
235
254
|
response = self._request_with_retry(
|
|
236
255
|
"POST",
|
|
@@ -240,6 +259,8 @@ class Glacis:
|
|
|
240
259
|
)
|
|
241
260
|
|
|
242
261
|
receipt = AttestReceipt.model_validate(response)
|
|
262
|
+
# Attach control_plane_results locally for evidence (not sent to server)
|
|
263
|
+
receipt.control_plane_results = control_plane_results
|
|
243
264
|
self._debug(f"Attestation successful: {receipt.attestation_id}")
|
|
244
265
|
return receipt
|
|
245
266
|
|
|
@@ -251,6 +272,7 @@ class Glacis:
|
|
|
251
272
|
input: Any,
|
|
252
273
|
output: Any,
|
|
253
274
|
metadata: Optional[dict[str, str]],
|
|
275
|
+
control_plane_results: Optional[ControlPlaneAttestation] = None,
|
|
254
276
|
) -> OfflineAttestReceipt:
|
|
255
277
|
"""Create a locally-signed attestation."""
|
|
256
278
|
self._debug(f"Attesting (offline): service_id={service_id}, hash={payload_hash[:16]}...")
|
|
@@ -259,8 +281,8 @@ class Glacis:
|
|
|
259
281
|
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
260
282
|
timestamp_ms = int(datetime.utcnow().timestamp() * 1000)
|
|
261
283
|
|
|
262
|
-
# Build attestation payload
|
|
263
|
-
attestation_payload = {
|
|
284
|
+
# Build attestation payload (this is what gets signed)
|
|
285
|
+
attestation_payload: dict[str, Any] = {
|
|
264
286
|
"version": 1,
|
|
265
287
|
"serviceId": service_id,
|
|
266
288
|
"operationType": operation_type,
|
|
@@ -269,15 +291,21 @@ class Glacis:
|
|
|
269
291
|
"mode": "offline",
|
|
270
292
|
}
|
|
271
293
|
|
|
294
|
+
# Include control plane results in signed payload if provided
|
|
295
|
+
if control_plane_results:
|
|
296
|
+
attestation_payload["controlPlaneResults"] = (
|
|
297
|
+
control_plane_results.model_dump(by_alias=True)
|
|
298
|
+
)
|
|
299
|
+
|
|
272
300
|
# Sign using WASM
|
|
273
301
|
attestation_json = json.dumps(
|
|
274
302
|
attestation_payload, separators=(",", ":"), sort_keys=True
|
|
275
303
|
)
|
|
276
|
-
assert self.
|
|
304
|
+
assert self._ed25519 is not None
|
|
277
305
|
assert self._signing_seed is not None
|
|
278
306
|
assert self._public_key is not None
|
|
279
307
|
|
|
280
|
-
signed_json = self.
|
|
308
|
+
signed_json = self._ed25519.sign_attestation_json(
|
|
281
309
|
self._signing_seed, attestation_json
|
|
282
310
|
)
|
|
283
311
|
signed = json.loads(signed_json)
|
|
@@ -290,6 +318,7 @@ class Glacis:
|
|
|
290
318
|
payload_hash=payload_hash,
|
|
291
319
|
signature=signed["signature"],
|
|
292
320
|
public_key=self._public_key,
|
|
321
|
+
control_plane_results=control_plane_results,
|
|
293
322
|
)
|
|
294
323
|
|
|
295
324
|
# Store in SQLite
|
|
@@ -334,7 +363,7 @@ class Glacis:
|
|
|
334
363
|
# Online attestation ID
|
|
335
364
|
return self._verify_online(receipt)
|
|
336
365
|
elif isinstance(receipt, AttestReceipt):
|
|
337
|
-
return self._verify_online(receipt.
|
|
366
|
+
return self._verify_online(receipt.attestation_hash)
|
|
338
367
|
else:
|
|
339
368
|
raise TypeError(f"Invalid receipt type: {type(receipt)}")
|
|
340
369
|
|
|
@@ -359,13 +388,13 @@ class Glacis:
|
|
|
359
388
|
# which we don't currently do. A more robust solution would store the
|
|
360
389
|
# signed payload or timestampMs for later verification.
|
|
361
390
|
|
|
362
|
-
# Use
|
|
363
|
-
if self.
|
|
391
|
+
# Use Ed25519 to verify if we have the runtime
|
|
392
|
+
if self._ed25519 and self._signing_seed:
|
|
364
393
|
# We can at least verify the public key matches our seed
|
|
365
|
-
derived_pubkey = self.
|
|
394
|
+
derived_pubkey = self._ed25519.get_public_key_hex(self._signing_seed)
|
|
366
395
|
signature_valid = derived_pubkey == receipt.public_key
|
|
367
396
|
else:
|
|
368
|
-
# Without the
|
|
397
|
+
# Without the Ed25519 runtime, we can't fully verify
|
|
369
398
|
# But we can check if the receipt is in our storage
|
|
370
399
|
signature_valid = True # Trusted from local storage
|
|
371
400
|
|
|
@@ -430,9 +459,9 @@ class Glacis:
|
|
|
430
459
|
"""
|
|
431
460
|
params: dict[str, Any] = {}
|
|
432
461
|
if org_id:
|
|
433
|
-
params["
|
|
462
|
+
params["orgId"] = org_id
|
|
434
463
|
if service_id:
|
|
435
|
-
params["
|
|
464
|
+
params["serviceId"] = service_id
|
|
436
465
|
if start:
|
|
437
466
|
params["start"] = start
|
|
438
467
|
if end:
|
|
@@ -702,9 +731,9 @@ class AsyncGlacis:
|
|
|
702
731
|
"""
|
|
703
732
|
params: dict[str, Any] = {}
|
|
704
733
|
if org_id:
|
|
705
|
-
params["
|
|
734
|
+
params["orgId"] = org_id
|
|
706
735
|
if service_id:
|
|
707
|
-
params["
|
|
736
|
+
params["serviceId"] = service_id
|
|
708
737
|
if start:
|
|
709
738
|
params["start"] = start
|
|
710
739
|
if end:
|
glacis/config.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GLACIS Configuration Module
|
|
3
|
+
|
|
4
|
+
Provides configuration models and loading for glacis.yaml config files.
|
|
5
|
+
The config file allows toggling controls (PII/PHI redaction, etc.) on/off.
|
|
6
|
+
|
|
7
|
+
Example glacis.yaml:
|
|
8
|
+
version: "1.0"
|
|
9
|
+
policy:
|
|
10
|
+
id: "hipaa-safe-harbor"
|
|
11
|
+
tenant_id: "my-org"
|
|
12
|
+
controls:
|
|
13
|
+
pii_phi:
|
|
14
|
+
enabled: true
|
|
15
|
+
backend: "presidio"
|
|
16
|
+
mode: "fast"
|
|
17
|
+
jailbreak:
|
|
18
|
+
enabled: true
|
|
19
|
+
backend: "prompt_guard_22m"
|
|
20
|
+
threshold: 0.5
|
|
21
|
+
action: "flag"
|
|
22
|
+
attestation:
|
|
23
|
+
offline: true
|
|
24
|
+
service_id: "my-service"
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
from glacis.config import load_config
|
|
28
|
+
|
|
29
|
+
# Auto-load from ./glacis.yaml
|
|
30
|
+
config = load_config()
|
|
31
|
+
|
|
32
|
+
# Or explicit path
|
|
33
|
+
config = load_config("path/to/glacis.yaml")
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Literal, Optional
|
|
38
|
+
|
|
39
|
+
from pydantic import BaseModel, Field
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PiiPhiConfig(BaseModel):
|
|
43
|
+
"""PII/PHI redaction control configuration."""
|
|
44
|
+
|
|
45
|
+
enabled: bool = Field(default=False, description="Enable PII/PHI redaction")
|
|
46
|
+
backend: str = Field(
|
|
47
|
+
default="presidio",
|
|
48
|
+
description="Backend model identifier (e.g., 'presidio')",
|
|
49
|
+
)
|
|
50
|
+
mode: Literal["fast", "full"] = Field(
|
|
51
|
+
default="fast",
|
|
52
|
+
description="Redaction mode: 'fast' (regex-only) or 'full' (regex + NER)",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class JailbreakConfig(BaseModel):
|
|
57
|
+
"""Jailbreak/prompt injection detection configuration."""
|
|
58
|
+
|
|
59
|
+
enabled: bool = Field(default=False, description="Enable jailbreak detection")
|
|
60
|
+
backend: str = Field(
|
|
61
|
+
default="prompt_guard_22m",
|
|
62
|
+
description="Backend model: 'prompt_guard_22m' or 'prompt_guard_86m'",
|
|
63
|
+
)
|
|
64
|
+
threshold: float = Field(
|
|
65
|
+
default=0.5,
|
|
66
|
+
ge=0.0,
|
|
67
|
+
le=1.0,
|
|
68
|
+
description="Classification threshold (0-1)",
|
|
69
|
+
)
|
|
70
|
+
action: Literal["block", "flag", "log"] = Field(
|
|
71
|
+
default="flag",
|
|
72
|
+
description="Action when jailbreak detected: 'block', 'flag', or 'log'",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ControlsConfig(BaseModel):
|
|
77
|
+
"""Configuration for all controls."""
|
|
78
|
+
|
|
79
|
+
pii_phi: PiiPhiConfig = Field(default_factory=PiiPhiConfig)
|
|
80
|
+
jailbreak: JailbreakConfig = Field(default_factory=JailbreakConfig)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class PolicyConfig(BaseModel):
|
|
84
|
+
"""Policy metadata included in attestations."""
|
|
85
|
+
|
|
86
|
+
id: str = Field(default="default", description="Policy identifier")
|
|
87
|
+
version: str = Field(default="1.0", description="Policy version")
|
|
88
|
+
tenant_id: str = Field(default="default", description="Tenant identifier")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AttestationConfig(BaseModel):
|
|
92
|
+
"""Attestation settings."""
|
|
93
|
+
|
|
94
|
+
offline: bool = Field(default=True, description="Use offline mode (local signing)")
|
|
95
|
+
service_id: str = Field(default="openai", description="Service identifier")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class GlacisConfig(BaseModel):
|
|
99
|
+
"""Root configuration model for GLACIS."""
|
|
100
|
+
|
|
101
|
+
version: str = Field(default="1.0", description="Config schema version")
|
|
102
|
+
policy: PolicyConfig = Field(default_factory=PolicyConfig)
|
|
103
|
+
controls: ControlsConfig = Field(default_factory=ControlsConfig)
|
|
104
|
+
attestation: AttestationConfig = Field(default_factory=AttestationConfig)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def load_config(path: Optional[str] = None) -> GlacisConfig:
|
|
108
|
+
"""
|
|
109
|
+
Load configuration from a YAML file.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
path: Explicit path to config file. If None, looks for ./glacis.yaml
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
GlacisConfig instance (defaults if no config file found)
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> config = load_config() # Auto-load ./glacis.yaml
|
|
119
|
+
>>> config = load_config("configs/production.yaml") # Explicit path
|
|
120
|
+
"""
|
|
121
|
+
if path:
|
|
122
|
+
config_path = Path(path)
|
|
123
|
+
else:
|
|
124
|
+
config_path = Path("glacis.yaml")
|
|
125
|
+
|
|
126
|
+
if config_path.exists():
|
|
127
|
+
try:
|
|
128
|
+
import yaml
|
|
129
|
+
except ImportError:
|
|
130
|
+
raise ImportError(
|
|
131
|
+
"pyyaml is required for config file support. "
|
|
132
|
+
"Install with: pip install pyyaml"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
with open(config_path) as f:
|
|
136
|
+
data = yaml.safe_load(f)
|
|
137
|
+
|
|
138
|
+
if data:
|
|
139
|
+
return GlacisConfig(**data)
|
|
140
|
+
|
|
141
|
+
return GlacisConfig()
|