spanforge 1.0.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.
Files changed (174) hide show
  1. spanforge/__init__.py +815 -0
  2. spanforge/_ansi.py +93 -0
  3. spanforge/_batch_exporter.py +409 -0
  4. spanforge/_cli.py +2094 -0
  5. spanforge/_cli_audit.py +639 -0
  6. spanforge/_cli_compliance.py +711 -0
  7. spanforge/_cli_cost.py +243 -0
  8. spanforge/_cli_ops.py +791 -0
  9. spanforge/_cli_phase11.py +356 -0
  10. spanforge/_hooks.py +337 -0
  11. spanforge/_server.py +1708 -0
  12. spanforge/_span.py +1036 -0
  13. spanforge/_store.py +288 -0
  14. spanforge/_stream.py +664 -0
  15. spanforge/_trace.py +335 -0
  16. spanforge/_tracer.py +254 -0
  17. spanforge/actor.py +141 -0
  18. spanforge/alerts.py +469 -0
  19. spanforge/auto.py +464 -0
  20. spanforge/baseline.py +335 -0
  21. spanforge/cache.py +635 -0
  22. spanforge/compliance.py +325 -0
  23. spanforge/config.py +532 -0
  24. spanforge/consent.py +228 -0
  25. spanforge/consumer.py +377 -0
  26. spanforge/core/__init__.py +5 -0
  27. spanforge/core/compliance_mapping.py +1254 -0
  28. spanforge/cost.py +600 -0
  29. spanforge/debug.py +548 -0
  30. spanforge/deprecations.py +205 -0
  31. spanforge/drift.py +482 -0
  32. spanforge/egress.py +58 -0
  33. spanforge/eval.py +648 -0
  34. spanforge/event.py +1064 -0
  35. spanforge/exceptions.py +240 -0
  36. spanforge/explain.py +178 -0
  37. spanforge/export/__init__.py +69 -0
  38. spanforge/export/append_only.py +337 -0
  39. spanforge/export/cloud.py +357 -0
  40. spanforge/export/datadog.py +497 -0
  41. spanforge/export/grafana.py +320 -0
  42. spanforge/export/jsonl.py +195 -0
  43. spanforge/export/openinference.py +158 -0
  44. spanforge/export/otel_bridge.py +294 -0
  45. spanforge/export/otlp.py +811 -0
  46. spanforge/export/otlp_bridge.py +233 -0
  47. spanforge/export/redis_backend.py +282 -0
  48. spanforge/export/siem_schema.py +98 -0
  49. spanforge/export/siem_splunk.py +264 -0
  50. spanforge/export/siem_syslog.py +212 -0
  51. spanforge/export/webhook.py +299 -0
  52. spanforge/exporters/__init__.py +30 -0
  53. spanforge/exporters/console.py +271 -0
  54. spanforge/exporters/jsonl.py +144 -0
  55. spanforge/exporters/sqlite.py +142 -0
  56. spanforge/gate.py +1150 -0
  57. spanforge/governance.py +181 -0
  58. spanforge/hitl.py +295 -0
  59. spanforge/http.py +187 -0
  60. spanforge/inspect.py +427 -0
  61. spanforge/integrations/__init__.py +45 -0
  62. spanforge/integrations/_pricing.py +280 -0
  63. spanforge/integrations/anthropic.py +388 -0
  64. spanforge/integrations/azure_openai.py +133 -0
  65. spanforge/integrations/bedrock.py +292 -0
  66. spanforge/integrations/crewai.py +251 -0
  67. spanforge/integrations/gemini.py +351 -0
  68. spanforge/integrations/groq.py +442 -0
  69. spanforge/integrations/langchain.py +349 -0
  70. spanforge/integrations/langgraph.py +306 -0
  71. spanforge/integrations/llamaindex.py +373 -0
  72. spanforge/integrations/ollama.py +287 -0
  73. spanforge/integrations/openai.py +368 -0
  74. spanforge/integrations/together.py +483 -0
  75. spanforge/io.py +214 -0
  76. spanforge/lint.py +322 -0
  77. spanforge/metrics.py +417 -0
  78. spanforge/metrics_export.py +343 -0
  79. spanforge/migrate.py +402 -0
  80. spanforge/model_registry.py +278 -0
  81. spanforge/models.py +389 -0
  82. spanforge/namespaces/__init__.py +254 -0
  83. spanforge/namespaces/audit.py +256 -0
  84. spanforge/namespaces/cache.py +237 -0
  85. spanforge/namespaces/chain.py +77 -0
  86. spanforge/namespaces/confidence.py +72 -0
  87. spanforge/namespaces/consent.py +92 -0
  88. spanforge/namespaces/cost.py +179 -0
  89. spanforge/namespaces/decision.py +143 -0
  90. spanforge/namespaces/diff.py +157 -0
  91. spanforge/namespaces/drift.py +80 -0
  92. spanforge/namespaces/eval_.py +251 -0
  93. spanforge/namespaces/feedback.py +241 -0
  94. spanforge/namespaces/fence.py +193 -0
  95. spanforge/namespaces/guard.py +105 -0
  96. spanforge/namespaces/hitl.py +91 -0
  97. spanforge/namespaces/latency.py +72 -0
  98. spanforge/namespaces/prompt.py +190 -0
  99. spanforge/namespaces/redact.py +173 -0
  100. spanforge/namespaces/retrieval.py +379 -0
  101. spanforge/namespaces/runtime_governance.py +494 -0
  102. spanforge/namespaces/template.py +208 -0
  103. spanforge/namespaces/tool_call.py +77 -0
  104. spanforge/namespaces/trace.py +1029 -0
  105. spanforge/normalizer.py +171 -0
  106. spanforge/plugins.py +82 -0
  107. spanforge/presidio_backend.py +349 -0
  108. spanforge/processor.py +258 -0
  109. spanforge/prompt_registry.py +418 -0
  110. spanforge/py.typed +0 -0
  111. spanforge/redact.py +914 -0
  112. spanforge/regression.py +192 -0
  113. spanforge/runtime_policy.py +159 -0
  114. spanforge/sampling.py +511 -0
  115. spanforge/schema.py +183 -0
  116. spanforge/schemas/v1.0/schema.json +170 -0
  117. spanforge/schemas/v2.0/schema.json +536 -0
  118. spanforge/sdk/__init__.py +625 -0
  119. spanforge/sdk/_base.py +584 -0
  120. spanforge/sdk/_base.pyi +71 -0
  121. spanforge/sdk/_exceptions.py +1096 -0
  122. spanforge/sdk/_types.py +2184 -0
  123. spanforge/sdk/alert.py +1514 -0
  124. spanforge/sdk/alert.pyi +56 -0
  125. spanforge/sdk/audit.py +1196 -0
  126. spanforge/sdk/audit.pyi +67 -0
  127. spanforge/sdk/cec.py +1215 -0
  128. spanforge/sdk/cec.pyi +37 -0
  129. spanforge/sdk/config.py +641 -0
  130. spanforge/sdk/config.pyi +55 -0
  131. spanforge/sdk/enterprise.py +714 -0
  132. spanforge/sdk/enterprise.pyi +79 -0
  133. spanforge/sdk/explain.py +170 -0
  134. spanforge/sdk/fallback.py +432 -0
  135. spanforge/sdk/feedback.py +351 -0
  136. spanforge/sdk/gate.py +874 -0
  137. spanforge/sdk/gate.pyi +51 -0
  138. spanforge/sdk/identity.py +2114 -0
  139. spanforge/sdk/identity.pyi +47 -0
  140. spanforge/sdk/lineage.py +175 -0
  141. spanforge/sdk/observe.py +1065 -0
  142. spanforge/sdk/observe.pyi +50 -0
  143. spanforge/sdk/operator.py +338 -0
  144. spanforge/sdk/pii.py +1473 -0
  145. spanforge/sdk/pii.pyi +119 -0
  146. spanforge/sdk/pipelines.py +458 -0
  147. spanforge/sdk/pipelines.pyi +39 -0
  148. spanforge/sdk/policy.py +930 -0
  149. spanforge/sdk/rag.py +594 -0
  150. spanforge/sdk/rbac.py +280 -0
  151. spanforge/sdk/registry.py +430 -0
  152. spanforge/sdk/registry.pyi +46 -0
  153. spanforge/sdk/scope.py +279 -0
  154. spanforge/sdk/secrets.py +293 -0
  155. spanforge/sdk/secrets.pyi +25 -0
  156. spanforge/sdk/security.py +560 -0
  157. spanforge/sdk/security.pyi +57 -0
  158. spanforge/sdk/trust.py +472 -0
  159. spanforge/sdk/trust.pyi +41 -0
  160. spanforge/secrets.py +799 -0
  161. spanforge/signing.py +1179 -0
  162. spanforge/stats.py +100 -0
  163. spanforge/stream.py +560 -0
  164. spanforge/testing.py +378 -0
  165. spanforge/testing_mocks.py +1052 -0
  166. spanforge/trace.py +199 -0
  167. spanforge/types.py +696 -0
  168. spanforge/ulid.py +300 -0
  169. spanforge/validate.py +379 -0
  170. spanforge-1.0.0.dist-info/METADATA +1509 -0
  171. spanforge-1.0.0.dist-info/RECORD +174 -0
  172. spanforge-1.0.0.dist-info/WHEEL +4 -0
  173. spanforge-1.0.0.dist-info/entry_points.txt +5 -0
  174. spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
@@ -0,0 +1,2184 @@
1
+ """spanforge.sdk._types — Value objects for the SpanForge service SDK.
2
+
3
+ All types here are immutable or clearly documented as mutable where needed.
4
+
5
+ Security requirements
6
+ ---------------------
7
+ * :class:`SecretStr` **never** exposes its value via ``__repr__``,
8
+ ``__str__``, or Python's pickle protocol.
9
+ * :class:`APIKeyBundle` redacts its ``api_key`` field in ``__repr__``.
10
+ * Equality on :class:`SecretStr` uses :func:`hmac.compare_digest` to
11
+ resist timing-based side-channel attacks.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hmac
17
+ import re
18
+ from dataclasses import dataclass, field
19
+ from datetime import datetime, timezone
20
+ from typing import Any, ClassVar
21
+
22
+ __all__ = [
23
+ # Phase 1 — identity
24
+ "APIKeyBundle",
25
+ # Phase 11 — Enterprise Hardening & Supply Chain Security
26
+ "AirGapConfig",
27
+ # Phase 7 — Alert Routing Service
28
+ "AlertRecord",
29
+ "AlertSeverity",
30
+ "AlertStatusInfo",
31
+ # Phase 6 — Observability Named SDK
32
+ "Annotation",
33
+ # Phase 4 — Audit service
34
+ "Article30Record",
35
+ "AuditAppendResult",
36
+ "AuditStatusInfo",
37
+ # Phase 5 — Compliance Evidence Chain
38
+ "BundleResult",
39
+ "BundleVerificationResult",
40
+ "CECStatusInfo",
41
+ "ClauseMapEntry",
42
+ "ClauseSatisfaction",
43
+ # Phase 10 — T.R.U.S.T. Scorecard & HallucCheck Contract
44
+ "CompositeGateInput",
45
+ "CompositeGateResult",
46
+ "DPADocument",
47
+ # Phase 3 — PII hardening
48
+ "DSARExport",
49
+ "DSARResult",
50
+ "DataResidency",
51
+ "DependencyVulnerability",
52
+ "EncryptionConfig",
53
+ "EnterpriseStatusInfo",
54
+ "ErasureReceipt",
55
+ "ExportResult",
56
+ # Phase 8 — CI/CD Gate Pipeline
57
+ "GateArtifact",
58
+ "GateEvaluationResult",
59
+ "GateStatusInfo",
60
+ "GateVerdict",
61
+ "HealthEndpointResult",
62
+ "IsolationScope",
63
+ "JWTClaims",
64
+ "KeyFormat",
65
+ "KeyScope",
66
+ "MagicLinkResult",
67
+ "MaintenanceWindow",
68
+ "ObserveStatusInfo",
69
+ "PIIAnonymisedResult",
70
+ "PIIEntity",
71
+ "PIIHeatMapEntry",
72
+ "PIIPipelineResult",
73
+ "PIIRedactionManifestEntry",
74
+ "PIIStatusInfo",
75
+ "PIITextScanResult",
76
+ "PRRIResult",
77
+ "PRRIVerdict",
78
+ "PipelineResult",
79
+ "PublishResult",
80
+ "QuotaTier",
81
+ "RateLimitInfo",
82
+ "ReceiverConfig",
83
+ # Phase 2 — PII
84
+ "SFPIIAnonymizeResult",
85
+ "SFPIIHit",
86
+ "SFPIIRedactResult",
87
+ "SFPIIScanResult",
88
+ "SafeHarborResult",
89
+ "SamplerStrategy",
90
+ "SecretStr",
91
+ "SecurityAuditResult",
92
+ "SecurityScanResult",
93
+ "SignedRecord",
94
+ "StaticAnalysisFinding",
95
+ "TOTPEnrollResult",
96
+ "TenantConfig",
97
+ "ThreatModelEntry",
98
+ "TokenIntrospectionResult",
99
+ "TopicRegistration",
100
+ "TrainingDataPIIReport",
101
+ "TrustBadgeResult",
102
+ "TrustDimension",
103
+ "TrustDimensionWeights",
104
+ "TrustGateResult",
105
+ "TrustHistoryEntry",
106
+ "TrustScorecard",
107
+ "TrustScorecardResponse",
108
+ "TrustStatusInfo",
109
+ # Phase 1 — SSO (ID-040–ID-043)
110
+ "OIDCAuthRequest",
111
+ "OIDCTokenResult",
112
+ "SCIMGroup",
113
+ "SCIMListResponse",
114
+ "SCIMUser",
115
+ "SSOSession",
116
+ ]
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # API key format constant
120
+ # ---------------------------------------------------------------------------
121
+
122
+ #: Regex for valid SpanForge API keys: ``sf_(live|test)_<48 base62 chars>``
123
+ _KEY_PATTERN: re.Pattern[str] = re.compile(r"^sf_(?:live|test)_[0-9A-Za-z]{48}$")
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # SecretStr — a string that hides its value
127
+ # ---------------------------------------------------------------------------
128
+
129
+
130
+ class SecretStr:
131
+ """A string whose value is never exposed by ``__repr__`` or ``__str__``.
132
+
133
+ Use :meth:`get_secret_value` to retrieve the underlying string for
134
+ cryptographic operations. All other operations (repr, str, pickle)
135
+ deliberately conceal the value to prevent accidental leakage into logs,
136
+ error messages, or serialised state.
137
+
138
+ Equality comparisons use :func:`hmac.compare_digest` to resist
139
+ timing-based side-channel attacks.
140
+
141
+ Example::
142
+
143
+ key = SecretStr("sf_live_abc...")
144
+ print(key) # <SecretStr:***>
145
+ print(repr(key)) # <SecretStr:***>
146
+ print(key.get_secret_value()) # sf_live_abc...
147
+ """
148
+
149
+ __slots__ = ("_value",)
150
+
151
+ def __init__(self, value: str) -> None:
152
+ object.__setattr__(self, "_value", value)
153
+
154
+ # ------------------------------------------------------------------
155
+ # Prevent accidental exposure
156
+ # ------------------------------------------------------------------
157
+
158
+ def __repr__(self) -> str:
159
+ return "<SecretStr:***>"
160
+
161
+ def __str__(self) -> str:
162
+ return "<SecretStr:***>"
163
+
164
+ def __setattr__(self, name: str, value: object) -> None:
165
+ raise AttributeError("SecretStr is immutable")
166
+
167
+ def __reduce__(self) -> None: # type: ignore[override]
168
+ """Prevent pickling to avoid secret leakage via serialised objects."""
169
+ raise TypeError(
170
+ "SecretStr cannot be pickled. "
171
+ "Extract the secret value with get_secret_value() before serialising."
172
+ )
173
+
174
+ # ------------------------------------------------------------------
175
+ # Timing-safe equality
176
+ # ------------------------------------------------------------------
177
+
178
+ def __eq__(self, other: object) -> bool:
179
+ if isinstance(other, SecretStr):
180
+ a = object.__getattribute__(self, "_value")
181
+ b = object.__getattribute__(other, "_value")
182
+ return hmac.compare_digest(a, b)
183
+ return NotImplemented
184
+
185
+ def __hash__(self) -> int:
186
+ return hash(object.__getattribute__(self, "_value"))
187
+
188
+ # ------------------------------------------------------------------
189
+ # Intentional access
190
+ # ------------------------------------------------------------------
191
+
192
+ def get_secret_value(self) -> str:
193
+ """Return the underlying secret string.
194
+
195
+ Call this explicitly and only where the raw value is needed (e.g.
196
+ to set an HTTP header or perform a cryptographic operation). Do not
197
+ pass the result to logging calls.
198
+ """
199
+ return str(object.__getattribute__(self, "_value"))
200
+
201
+ def __len__(self) -> int:
202
+ """Return length without exposing value — safe for format checks."""
203
+ return len(object.__getattribute__(self, "_value"))
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # KeyFormat — API key validation helpers
208
+ # ---------------------------------------------------------------------------
209
+
210
+
211
+ class KeyFormat:
212
+ """Validate and inspect SpanForge API key format.
213
+
214
+ Valid format: ``sf_live_<48 base62 chars>`` or ``sf_test_<48 base62 chars>``.
215
+
216
+ Example::
217
+
218
+ KeyFormat.validate("sf_live_" + "A" * 48) # OK
219
+ KeyFormat.validate("bad-key") # raises SFKeyFormatError
220
+ """
221
+
222
+ PATTERN: re.Pattern[str] = _KEY_PATTERN
223
+
224
+ @classmethod
225
+ def validate(cls, key: str) -> None:
226
+ """Raise :exc:`~spanforge.sdk._exceptions.SFKeyFormatError` if invalid."""
227
+ from spanforge.sdk._exceptions import SFKeyFormatError
228
+
229
+ if not isinstance(key, str) or not cls.PATTERN.match(key):
230
+ raise SFKeyFormatError(
231
+ f"Key must match sf_(live|test)_<48 base62 chars>. "
232
+ f"Received length={len(key) if isinstance(key, str) else 'non-string'}."
233
+ )
234
+
235
+ @classmethod
236
+ def is_test_key(cls, key: str) -> bool:
237
+ """Return ``True`` if *key* is a test-mode key."""
238
+ return isinstance(key, str) and key.startswith("sf_test_")
239
+
240
+ @classmethod
241
+ def is_live_key(cls, key: str) -> bool:
242
+ """Return ``True`` if *key* is a live-mode key."""
243
+ return isinstance(key, str) and key.startswith("sf_live_")
244
+
245
+ @classmethod
246
+ def is_valid(cls, key: str) -> bool:
247
+ """Return ``True`` without raising if *key* matches the format."""
248
+ return isinstance(key, str) and bool(cls.PATTERN.match(key))
249
+
250
+
251
+ # ---------------------------------------------------------------------------
252
+ # Value objects
253
+ # ---------------------------------------------------------------------------
254
+
255
+
256
+ @dataclass
257
+ class KeyScope:
258
+ """Scoping constraints attached to an API key.
259
+
260
+ All fields default to *empty / unrestricted*. Non-empty lists restrict
261
+ access to the listed values only.
262
+
263
+ Attributes:
264
+ pillar_whitelist: SpanForge service names the key may call (e.g.
265
+ ``["sf_pii", "sf_audit"]``). Empty = unrestricted.
266
+ project_scope: Project IDs the key may act on. Empty = unrestricted.
267
+ ip_allowlist: CIDR strings (e.g. ``["192.168.1.0/24", "10.0.0.1/32"]``).
268
+ Empty = unrestricted.
269
+ expires_at: Optional hard expiry. ``None`` = no expiry.
270
+ """
271
+
272
+ pillar_whitelist: list[str] = field(default_factory=list)
273
+ project_scope: list[str] = field(default_factory=list)
274
+ ip_allowlist: list[str] = field(default_factory=list)
275
+ expires_at: datetime | None = None
276
+
277
+ def is_expired(self) -> bool:
278
+ """Return ``True`` if the key has passed its hard expiry."""
279
+ if self.expires_at is None:
280
+ return False
281
+ return datetime.now(timezone.utc) >= self.expires_at
282
+
283
+ def allows_service(self, service_name: str) -> bool:
284
+ """Return ``True`` if this scope permits calls to *service_name*."""
285
+ if not self.pillar_whitelist:
286
+ return True
287
+ return service_name in self.pillar_whitelist
288
+
289
+ def allows_project(self, project_id: str) -> bool:
290
+ """Return ``True`` if this scope permits acting on *project_id*."""
291
+ if not self.project_scope:
292
+ return True
293
+ return project_id in self.project_scope
294
+
295
+
296
+ @dataclass
297
+ class APIKeyBundle:
298
+ """Result of issuing or rotating a SpanForge API key.
299
+
300
+ The ``api_key`` field is a :class:`SecretStr` and must be presented to
301
+ the user **once** at issuance time only. SpanForge never returns it
302
+ again after the initial issuance response.
303
+
304
+ Attributes:
305
+ api_key: The raw key value (write-once; never log).
306
+ key_id: Opaque identifier used for ``rotate_key`` / ``revoke_key``.
307
+ jwt: RS256 (or HS256 in local mode) session JWT.
308
+ expires_at: When the session JWT expires.
309
+ scopes: Permission scopes granted to this key.
310
+ """
311
+
312
+ api_key: SecretStr
313
+ key_id: str
314
+ jwt: str
315
+ expires_at: datetime
316
+ scopes: list[str] = field(default_factory=list)
317
+
318
+ def __repr__(self) -> str:
319
+ return (
320
+ f"APIKeyBundle("
321
+ f"key_id={self.key_id!r}, "
322
+ f"expires_at={self.expires_at.isoformat()!r}, "
323
+ f"scopes={self.scopes!r}, "
324
+ f"api_key=<redacted>)"
325
+ )
326
+
327
+
328
+ @dataclass
329
+ class JWTClaims:
330
+ """Decoded and validated JWT payload.
331
+
332
+ Attributes:
333
+ subject: The ``sub`` claim — typically the ``key_id``.
334
+ scopes: Permission scopes extracted from the JWT.
335
+ project_id: The ``aud`` claim.
336
+ expires_at: Token expiry (UTC).
337
+ issued_at: Token issuance time (UTC).
338
+ jti: Unique JWT ID used for revocation checks.
339
+ issuer: The ``iss`` claim.
340
+ """
341
+
342
+ subject: str
343
+ scopes: list[str]
344
+ project_id: str
345
+ expires_at: datetime
346
+ issued_at: datetime
347
+ jti: str
348
+ issuer: str = "spanforge"
349
+
350
+ def is_expired(self) -> bool:
351
+ """Return ``True`` if the JWT has passed its expiry."""
352
+ return datetime.now(timezone.utc) >= self.expires_at
353
+
354
+
355
+ @dataclass
356
+ class RateLimitInfo:
357
+ """Current rate-limit state for a key.
358
+
359
+ Attributes:
360
+ limit: Total requests allowed per window.
361
+ remaining: Requests remaining in the current window.
362
+ reset_at: When the window resets (UTC).
363
+ """
364
+
365
+ limit: int
366
+ remaining: int
367
+ reset_at: datetime
368
+
369
+
370
+ @dataclass
371
+ class TokenIntrospectionResult:
372
+ """RFC 7662 token introspection response.
373
+
374
+ Attributes:
375
+ active: ``True`` if the token is currently valid.
376
+ scope: Space-separated list of scopes.
377
+ exp: Unix timestamp of expiry, or ``None``.
378
+ sub: Subject claim.
379
+ client_id: Client identifier.
380
+ """
381
+
382
+ active: bool
383
+ scope: str = ""
384
+ exp: int | None = None
385
+ sub: str = ""
386
+ client_id: str = ""
387
+
388
+
389
+ @dataclass
390
+ class MagicLinkResult:
391
+ """Result of :meth:`~spanforge.sdk.identity.SFIdentityClient.issue_magic_link`.
392
+
393
+ Attributes:
394
+ link_id: Opaque ID used to look up the link record.
395
+ expires_at: When the one-time link expires (15 min from issuance, UTC).
396
+ """
397
+
398
+ link_id: str
399
+ expires_at: datetime
400
+
401
+
402
+ @dataclass
403
+ class TOTPEnrollResult:
404
+ """Result of :meth:`~spanforge.sdk.identity.SFIdentityClient.enroll_totp`.
405
+
406
+ Attributes:
407
+ secret_base32: Base32-encoded TOTP secret. **Display once then
408
+ discard** — never store this in logs or database plaintext.
409
+ qr_uri: ``otpauth://`` URI suitable for encoding as a QR code.
410
+ backup_codes: 8 single-use, 8-character alphanumeric codes.
411
+ Store hashed (already done server-side); present plaintext to
412
+ user exactly once.
413
+ """
414
+
415
+ secret_base32: SecretStr
416
+ qr_uri: str
417
+ backup_codes: list[str] # plaintext; user must save these
418
+
419
+ def __repr__(self) -> str:
420
+ return (
421
+ f"TOTPEnrollResult("
422
+ f"qr_uri={self.qr_uri!r}, "
423
+ f"backup_codes=<{len(self.backup_codes)} codes redacted>, "
424
+ f"secret_base32=<redacted>)"
425
+ )
426
+
427
+
428
+ # ---------------------------------------------------------------------------
429
+ # Quota tier constants
430
+ # ---------------------------------------------------------------------------
431
+
432
+
433
+ class QuotaTier:
434
+ """Named quota tier constants.
435
+
436
+ Attributes:
437
+ FREE: Local CLI only (no network calls allowed).
438
+ API: $99 / month — 10 000 scored records / day.
439
+ TEAM: $499 / month — 100 000 scored records / day.
440
+ ENTERPRISE: Unlimited.
441
+ """
442
+
443
+ FREE = "free"
444
+ API = "api"
445
+ TEAM = "team"
446
+ ENTERPRISE = "enterprise"
447
+
448
+ #: Mapping from tier name to daily quota (-1 = unlimited).
449
+ DAILY_LIMITS: ClassVar[dict[str, int]] = {
450
+ FREE: 0,
451
+ API: 10_000,
452
+ TEAM: 100_000,
453
+ ENTERPRISE: -1,
454
+ }
455
+
456
+ @classmethod
457
+ def daily_limit(cls, tier: str) -> int:
458
+ """Return daily record limit for *tier* (``-1`` = unlimited)."""
459
+ return cls.DAILY_LIMITS.get(tier, 0)
460
+
461
+
462
+ # ---------------------------------------------------------------------------
463
+ # Phase 2 — PII redaction service types
464
+ # ---------------------------------------------------------------------------
465
+
466
+
467
+ @dataclass(frozen=True)
468
+ class SFPIIHit:
469
+ """A single PII detection hit returned by :meth:`~spanforge.sdk.pii.SFPIIClient.scan`.
470
+
471
+ Attributes:
472
+ pii_type: PII category label (e.g. ``"email"``, ``"ssn"``,
473
+ ``"credit_card"``, ``"phone"``).
474
+ path: Dot-separated path to the detected field within the
475
+ payload (empty string for top-level string values).
476
+ match_count: Number of regex matches of this type at this path.
477
+ sensitivity: Sensitivity level: ``"high"``, ``"medium"``, or ``"low"``.
478
+ """
479
+
480
+ pii_type: str
481
+ path: str
482
+ match_count: int = 1
483
+ sensitivity: str = "medium"
484
+
485
+
486
+ @dataclass(frozen=True)
487
+ class SFPIIScanResult:
488
+ """Aggregated result of a PII scan operation.
489
+
490
+ Attributes:
491
+ hits: All :class:`SFPIIHit` instances detected. Empty when clean.
492
+ scanned: Total number of string values examined during the scan.
493
+ """
494
+
495
+ hits: list[SFPIIHit]
496
+ scanned: int
497
+
498
+ @property
499
+ def clean(self) -> bool:
500
+ """``True`` when no PII was detected."""
501
+ return len(self.hits) == 0
502
+
503
+
504
+ @dataclass(frozen=True)
505
+ class SFPIIRedactResult:
506
+ """Result of a PII redaction operation.
507
+
508
+ Attributes:
509
+ event: The newly reconstructed event with PII fields replaced
510
+ by safe marker strings (e.g. ``"[REDACTED:pii]"``).
511
+ redaction_count: Number of :class:`~spanforge.redact.Redactable` fields
512
+ that were scrubbed by the policy.
513
+ redacted_at: UTC ISO-8601 timestamp when redaction was applied.
514
+ redacted_by: Policy identifier string embedded in the result.
515
+ """
516
+
517
+ event: Any
518
+ redaction_count: int
519
+ redacted_at: str
520
+ redacted_by: str
521
+
522
+
523
+ @dataclass(frozen=True)
524
+ class SFPIIAnonymizeResult:
525
+ """Result of a text anonymization operation.
526
+
527
+ Attributes:
528
+ text: The anonymized text with PII replaced by type-tagged
529
+ markers (e.g. ``"[REDACTED:email]"``).
530
+ replacements: Total count of PII segments replaced across all
531
+ pattern types.
532
+ pii_types_found: Ordered list of distinct PII type labels detected
533
+ (e.g. ``["email", "phone"]``).
534
+ """
535
+
536
+ text: str
537
+ replacements: int
538
+ pii_types_found: list[str]
539
+
540
+
541
+ # ---------------------------------------------------------------------------
542
+ # Phase 3 — PII Service Hardening types
543
+ # ---------------------------------------------------------------------------
544
+
545
+
546
+ @dataclass(frozen=True)
547
+ class PIIEntity:
548
+ """A single character-level PII entity detected by Presidio.
549
+
550
+ Attributes:
551
+ type: PII entity type label (e.g. ``"EMAIL_ADDRESS"``, ``"US_SSN"``).
552
+ start: Start character offset in the scanned text.
553
+ end: End character offset in the scanned text (exclusive).
554
+ score: Presidio confidence score in ``[0.0, 1.0]``.
555
+ """
556
+
557
+ type: str
558
+ start: int
559
+ end: int
560
+ score: float
561
+
562
+
563
+ @dataclass(frozen=True)
564
+ class PIITextScanResult:
565
+ """Result of a presidio-backed text scan (PII-001).
566
+
567
+ Attributes:
568
+ entities: List of character-level :class:`PIIEntity` instances.
569
+ redacted_text: The input text with each detected entity replaced by
570
+ ``<TYPE>`` (e.g. ``<EMAIL_ADDRESS>``).
571
+ detected: ``True`` if at least one entity was found.
572
+ """
573
+
574
+ entities: list[PIIEntity]
575
+ redacted_text: str
576
+ detected: bool
577
+
578
+
579
+ @dataclass(frozen=True)
580
+ class PIIRedactionManifestEntry:
581
+ """One entry in an anonymise() redaction manifest.
582
+
583
+ Attributes:
584
+ field_path: Dot-separated path to the field within the payload.
585
+ type: PII type label (e.g. ``"email"``, ``"ssn"``).
586
+ original_hash: SHA-256 hex digest of the original value — for audit
587
+ without disclosing the raw PII.
588
+ replacement: The placeholder string that replaced the original value
589
+ (e.g. ``"<EMAIL>"``).
590
+ """
591
+
592
+ field_path: str
593
+ type: str
594
+ original_hash: str
595
+ replacement: str
596
+
597
+
598
+ @dataclass(frozen=True)
599
+ class PIIAnonymisedResult:
600
+ """Result of :meth:`~spanforge.sdk.pii.SFPIIClient.anonymise` (PII-002).
601
+
602
+ Attributes:
603
+ clean_payload: A deep copy of the input payload with all detected
604
+ PII replaced by ``<TYPE>`` placeholders.
605
+ redaction_manifest: Ordered list of :class:`PIIRedactionManifestEntry`
606
+ items — one per replacement, in traversal order.
607
+ """
608
+
609
+ clean_payload: dict[str, Any]
610
+ redaction_manifest: list[PIIRedactionManifestEntry]
611
+
612
+
613
+ @dataclass(frozen=True)
614
+ class PIIPipelineResult:
615
+ """Result of :meth:`~spanforge.sdk.pii.SFPIIClient.apply_pipeline_action` (PII-010/011/012).
616
+
617
+ Attributes:
618
+ text: The effective text after the action was applied.
619
+ For ``"redact"`` this is the redacted text; for
620
+ ``"flag"`` / ``"block"`` it is the original text.
621
+ action: The action that was applied: ``"flag"``,
622
+ ``"redact"``, or ``"block"``.
623
+ detected: ``True`` if any entity was detected above the
624
+ confidence threshold.
625
+ entity_types: List of entity type labels that triggered the
626
+ action (above-threshold hits only).
627
+ low_confidence_hits: List of :class:`PIIEntity` instances that were
628
+ below the threshold — recorded for audit only.
629
+ redacted_text: The redacted form of the input text (always
630
+ populated, even for ``"flag"``).
631
+ blocked: ``True`` when *action* is ``"block"`` and PII
632
+ was detected at or above the threshold.
633
+ """
634
+
635
+ text: str
636
+ action: str
637
+ detected: bool
638
+ entity_types: list[str]
639
+ low_confidence_hits: list[PIIEntity]
640
+ redacted_text: str
641
+ blocked: bool
642
+
643
+
644
+ @dataclass(frozen=True)
645
+ class PIIStatusInfo:
646
+ """sf-pii service status (PII-005).
647
+
648
+ Attributes:
649
+ status: Service status: ``"ok"`` or ``"degraded"``.
650
+ presidio_available: ``True`` if the presidio-analyzer package is
651
+ importable.
652
+ entity_types_loaded: List of entity type labels currently loaded
653
+ (regex + presidio combined).
654
+ last_scan_at: ISO-8601 UTC timestamp of the most recent scan,
655
+ or ``None`` if no scan has run since startup.
656
+ """
657
+
658
+ status: str
659
+ presidio_available: bool
660
+ entity_types_loaded: list[str]
661
+ last_scan_at: str | None
662
+
663
+
664
+ @dataclass(frozen=True)
665
+ class ErasureReceipt:
666
+ """Receipt for a GDPR Article 17 erasure request (PII-021).
667
+
668
+ Attributes:
669
+ subject_id: The data subject whose records were erased.
670
+ project_id: Scoping project for the erasure.
671
+ records_erased: Number of audit records found and marked for
672
+ erasure.
673
+ erasure_id: Opaque UUID for the erasure event itself.
674
+ erased_at: ISO-8601 UTC timestamp of the erasure.
675
+ exceptions: Any Article 17(3) exceptions that prevented
676
+ full erasure (list of reason strings).
677
+ """
678
+
679
+ subject_id: str
680
+ project_id: str
681
+ records_erased: int
682
+ erasure_id: str
683
+ erased_at: str
684
+ exceptions: list[str]
685
+
686
+
687
+ @dataclass(frozen=True)
688
+ class DSARExport:
689
+ """CCPA/DSAR export package (PII-022).
690
+
691
+ Attributes:
692
+ subject_id: The data subject whose records were exported.
693
+ project_id: Scoping project.
694
+ event_count: Number of events included in the export.
695
+ export_id: Opaque UUID for this export package.
696
+ exported_at: ISO-8601 UTC timestamp.
697
+ events: Serialised event records (dicts) — PII-safe subset.
698
+ """
699
+
700
+ subject_id: str
701
+ project_id: str
702
+ event_count: int
703
+ export_id: str
704
+ exported_at: str
705
+ events: list[dict[str, Any]]
706
+
707
+
708
+ @dataclass(frozen=True)
709
+ class SafeHarborResult:
710
+ """Result of HIPAA Safe Harbor de-identification (PII-023).
711
+
712
+ Attributes:
713
+ text: De-identified text with all 18 PHI identifiers
714
+ removed or generalised per 45 CFR §164.514(b)(2).
715
+ replacements: Number of PHI identifiers that were replaced or
716
+ generalised.
717
+ phi_types_found: List of PHI identifier type labels that were
718
+ encountered (e.g. ``["name", "date", "zip"]``).
719
+ """
720
+
721
+ text: str
722
+ replacements: int
723
+ phi_types_found: list[str]
724
+
725
+
726
+ @dataclass(frozen=True)
727
+ class PIIHeatMapEntry:
728
+ """One data point in the PII heat map (PII-032).
729
+
730
+ Attributes:
731
+ project_id: Project the scan belongs to.
732
+ entity_type: PII entity type label (e.g. ``"email"``, ``"ssn"``).
733
+ date: Calendar date in ``YYYY-MM-DD`` format.
734
+ count: Number of detections of this entity type on this date.
735
+ """
736
+
737
+ project_id: str
738
+ entity_type: str
739
+ date: str
740
+ count: int
741
+
742
+
743
+ @dataclass(frozen=True)
744
+ class TrainingDataPIIReport:
745
+ """PII prevalence report for a training dataset (PII-025).
746
+
747
+ Attributes:
748
+ dataset_path: Path to the scanned dataset file.
749
+ total_records: Total number of records scanned.
750
+ pii_records: Number of records that contained at least one PII hit.
751
+ prevalence_pct: ``pii_records / total_records * 100`` (or 0.0).
752
+ entity_counts: Mapping of entity type label → total hit count
753
+ across all records.
754
+ report_id: Opaque UUID for this report.
755
+ generated_at: ISO-8601 UTC timestamp.
756
+ """
757
+
758
+ dataset_path: str
759
+ total_records: int
760
+ pii_records: int
761
+ prevalence_pct: float
762
+ entity_counts: dict[str, int]
763
+ report_id: str
764
+ generated_at: str
765
+
766
+
767
+ # ---------------------------------------------------------------------------
768
+ # Phase 4 — Audit Service High-Level API types
769
+ # ---------------------------------------------------------------------------
770
+
771
+
772
+ @dataclass(frozen=True)
773
+ class AuditAppendResult:
774
+ """Result of :meth:`~spanforge.sdk.audit.SFAuditClient.append` (AUD-001).
775
+
776
+ Attributes:
777
+ record_id: Unique identifier for the audit record (ULID string).
778
+ chain_position: Zero-based position of this record in the HMAC chain.
779
+ timestamp: ISO-8601 UTC timestamp assigned at append time.
780
+ hmac: ``"hmac-sha256:<hex>"`` signature of this record.
781
+ schema_key: The schema key under which this record was stored.
782
+ backend: Storage backend used: ``"local"``, ``"s3"``,
783
+ ``"azure"``, ``"gcs"``, ``"r2"``, or ``"trust_only"``.
784
+ """
785
+
786
+ record_id: str
787
+ chain_position: int
788
+ timestamp: str
789
+ hmac: str
790
+ schema_key: str
791
+ backend: str = "local"
792
+
793
+
794
+ @dataclass(frozen=True)
795
+ class SignedRecord:
796
+ """A raw-dict record signed with an HMAC-SHA256 signature (AUD-003).
797
+
798
+ Attributes:
799
+ record: The original record dict.
800
+ record_id: Unique identifier for this record.
801
+ checksum: ``"sha256:<hex>"`` digest of the canonical JSON payload.
802
+ signature: ``"hmac-sha256:<hex>"`` HMAC signature.
803
+ timestamp: ISO-8601 UTC timestamp when the record was signed.
804
+ """
805
+
806
+ record: dict[str, Any]
807
+ record_id: str
808
+ checksum: str
809
+ signature: str
810
+ timestamp: str
811
+
812
+
813
+ @dataclass(frozen=True)
814
+ class TrustDimension:
815
+ """One dimension of the T.R.U.S.T. scorecard (AUD-031).
816
+
817
+ Attributes:
818
+ score: Normalised score in ``[0, 100]``.
819
+ trend: Direction of recent movement: ``"up"``, ``"flat"``,
820
+ or ``"down"``.
821
+ last_updated: ISO-8601 UTC timestamp of the most recent signal.
822
+ """
823
+
824
+ score: float
825
+ trend: str
826
+ last_updated: str
827
+
828
+
829
+ @dataclass(frozen=True)
830
+ class TrustScorecard:
831
+ """Aggregated T.R.U.S.T. scorecard for a project (AUD-031).
832
+
833
+ Attributes:
834
+ project_id: Scoping project.
835
+ from_dt: ISO-8601 UTC start of the reporting window.
836
+ to_dt: ISO-8601 UTC end of the reporting window.
837
+ hallucination: T.R.U.S.T. hallucination score dimension.
838
+ pii_hygiene: PII detection/redaction hygiene dimension.
839
+ secrets_hygiene: Secrets scanning hygiene dimension.
840
+ gate_pass_rate: CI/CD gate pass-rate dimension.
841
+ compliance_posture: Compliance evidence posture dimension.
842
+ record_count: Total audit records contributing to this scorecard.
843
+ """
844
+
845
+ project_id: str
846
+ from_dt: str
847
+ to_dt: str
848
+ hallucination: TrustDimension
849
+ pii_hygiene: TrustDimension
850
+ secrets_hygiene: TrustDimension
851
+ gate_pass_rate: TrustDimension
852
+ compliance_posture: TrustDimension
853
+ record_count: int
854
+
855
+
856
+ @dataclass(frozen=True)
857
+ class Article30Record:
858
+ """GDPR Article 30 Record of Processing Activities (AUD-042).
859
+
860
+ Attributes:
861
+ project_id: Scoping project.
862
+ controller_name: Name of the data controller.
863
+ processor_name: Name of the data processor (SpanForge).
864
+ processing_purposes: List of processing purposes.
865
+ data_categories: Categories of personal data processed.
866
+ data_subjects: Categories of data subjects.
867
+ recipients: List of recipient categories.
868
+ third_country: Whether data is transferred to a third country.
869
+ retention_period: Retention period description.
870
+ security_measures: List of technical/organisational measures.
871
+ generated_at: ISO-8601 UTC timestamp when the record was generated.
872
+ record_id: Opaque UUID for this Article 30 record.
873
+ """
874
+
875
+ project_id: str
876
+ controller_name: str
877
+ processor_name: str
878
+ processing_purposes: list[str]
879
+ data_categories: list[str]
880
+ data_subjects: list[str]
881
+ recipients: list[str]
882
+ third_country: bool
883
+ retention_period: str
884
+ security_measures: list[str]
885
+ generated_at: str
886
+ record_id: str
887
+
888
+
889
+ @dataclass(frozen=True)
890
+ class AuditStatusInfo:
891
+ """sf-audit service status.
892
+
893
+ Attributes:
894
+ status: Service status: ``"ok"`` or ``"degraded"``.
895
+ backend: Active backend name: ``"local"``, ``"s3"``,
896
+ ``"azure"``, ``"gcs"``, or ``"r2"``.
897
+ byos_enabled: ``True`` if a BYOS provider is configured.
898
+ record_count: Total number of records in the local store.
899
+ last_append_at: ISO-8601 UTC timestamp of the most recent append,
900
+ or ``None`` if no record has been appended.
901
+ schema_count: Number of distinct schema keys in the registry.
902
+ index_healthy: ``True`` if the SQLite query index is healthy.
903
+ retention_years: Configured retention period in years.
904
+ """
905
+
906
+ status: str
907
+ backend: str
908
+ byos_enabled: bool
909
+ record_count: int
910
+ last_append_at: str | None
911
+ schema_count: int
912
+ index_healthy: bool
913
+ retention_years: int
914
+
915
+
916
+ # ---------------------------------------------------------------------------
917
+ # Phase 5 — Compliance Evidence Chain (sf-cec) types
918
+ # ---------------------------------------------------------------------------
919
+
920
+ import enum as _enum
921
+
922
+
923
+ class ClauseSatisfaction(_enum.Enum):
924
+ """Satisfaction status for a single regulatory clause in a CEC bundle.
925
+
926
+ Attributes:
927
+ SATISFIED: Sufficient evidence records exist for this clause.
928
+ PARTIAL: Some evidence exists but below the minimum threshold.
929
+ GAP: No evidence records found for this clause.
930
+ """
931
+
932
+ SATISFIED = "SATISFIED"
933
+ PARTIAL = "PARTIAL"
934
+ GAP = "GAP"
935
+
936
+
937
+ @dataclass(frozen=True)
938
+ class ClauseMapEntry:
939
+ """One clause entry in ``clause_map.json`` (CEC-010 through CEC-014).
940
+
941
+ Attributes:
942
+ framework: Regulatory framework identifier (e.g. ``"eu_ai_act"``).
943
+ clause_id: Clause identifier within the framework (e.g.
944
+ ``"Art.9"``).
945
+ title: Human-readable clause title.
946
+ status: :class:`ClauseSatisfaction` value.
947
+ evidence_count: Number of audit records supporting this clause.
948
+ evidence_ids: Up to 20 record IDs providing evidence.
949
+ description: Short description of what the clause requires.
950
+ """
951
+
952
+ framework: str
953
+ clause_id: str
954
+ title: str
955
+ status: ClauseSatisfaction
956
+ evidence_count: int
957
+ evidence_ids: list[str]
958
+ description: str
959
+
960
+
961
+ @dataclass(frozen=True)
962
+ class BundleResult:
963
+ """Result of :meth:`~spanforge.sdk.cec.SFCECClient.build_bundle` (CEC-001).
964
+
965
+ Attributes:
966
+ bundle_id: Opaque UUID identifying this CEC bundle.
967
+ download_url: Signed URL (local file path in local mode) to the ZIP.
968
+ expires_at: ISO-8601 UTC timestamp when the download URL expires.
969
+ hmac_manifest: ``"hmac-sha256:<hex>"`` signature over ``manifest.json``.
970
+ record_counts: Mapping of schema key → number of records exported.
971
+ zip_path: Absolute path to the assembled ZIP file.
972
+ frameworks: List of regulatory framework identifiers included.
973
+ project_id: Project this bundle covers.
974
+ generated_at: ISO-8601 UTC timestamp of bundle generation.
975
+ """
976
+
977
+ bundle_id: str
978
+ download_url: str
979
+ expires_at: str
980
+ hmac_manifest: str
981
+ record_counts: dict[str, int]
982
+ zip_path: str
983
+ frameworks: list[str]
984
+ project_id: str
985
+ generated_at: str
986
+
987
+
988
+ @dataclass(frozen=True)
989
+ class BundleVerificationResult:
990
+ """Result of :meth:`~spanforge.sdk.cec.SFCECClient.verify_bundle` (CEC-005).
991
+
992
+ Attributes:
993
+ bundle_id: Bundle identifier extracted from the manifest.
994
+ manifest_valid: ``True`` if the manifest HMAC verifies correctly.
995
+ chain_valid: ``True`` if the embedded chain_proof.json is valid.
996
+ timestamp_valid: ``True`` if the RFC 3161 timestamp stub is present.
997
+ overall_valid: ``True`` if all three checks pass.
998
+ errors: List of human-readable validation error strings.
999
+ """
1000
+
1001
+ bundle_id: str
1002
+ manifest_valid: bool
1003
+ chain_valid: bool
1004
+ timestamp_valid: bool
1005
+ overall_valid: bool
1006
+ errors: list[str]
1007
+
1008
+
1009
+ @dataclass(frozen=True)
1010
+ class DPADocument:
1011
+ """GDPR Article 28 Data Processing Agreement (CEC-015).
1012
+
1013
+ Attributes:
1014
+ project_id: Scoping project.
1015
+ controller_name: Legal name of the data controller.
1016
+ controller_address: Registered address of the controller.
1017
+ processor_name: Legal name of the data processor (SpanForge).
1018
+ processor_address: Registered address of the processor.
1019
+ processing_purposes: List of processing purpose descriptions.
1020
+ data_categories: Categories of personal data processed.
1021
+ data_subjects: Categories of data subjects.
1022
+ sub_processors: List of sub-processor names authorised.
1023
+ transfer_mechanism: Cross-border transfer mechanism (e.g. ``"SCCs"``).
1024
+ retention_period: Retention period description.
1025
+ security_measures: List of technical / organisational security measures.
1026
+ scc_clauses: EU Standard Contractual Clauses module applied
1027
+ (e.g. ``"Module 2 (controller-to-processor)"``).
1028
+ document_id: Opaque UUID for this DPA document.
1029
+ generated_at: ISO-8601 UTC timestamp.
1030
+ text: Full plain-text body of the DPA.
1031
+ """
1032
+
1033
+ project_id: str
1034
+ controller_name: str
1035
+ controller_address: str
1036
+ processor_name: str
1037
+ processor_address: str
1038
+ processing_purposes: list[str]
1039
+ data_categories: list[str]
1040
+ data_subjects: list[str]
1041
+ sub_processors: list[str]
1042
+ transfer_mechanism: str
1043
+ retention_period: str
1044
+ security_measures: list[str]
1045
+ scc_clauses: str
1046
+ document_id: str
1047
+ generated_at: str
1048
+ text: str
1049
+
1050
+
1051
+ @dataclass(frozen=True)
1052
+ class CECStatusInfo:
1053
+ """sf-cec service status.
1054
+
1055
+ Attributes:
1056
+ status: Service status: ``"ok"`` or ``"degraded"``.
1057
+ byos_enabled: ``True`` if a BYOS provider is configured.
1058
+ bundle_count: Total number of bundles generated in this session.
1059
+ last_bundle_at: ISO-8601 UTC timestamp of the most recent bundle
1060
+ generation, or ``None`` if none generated yet.
1061
+ frameworks_supported: List of regulatory framework identifiers
1062
+ supported by this installation.
1063
+ """
1064
+
1065
+ status: str
1066
+ byos_enabled: bool
1067
+ bundle_count: int
1068
+ last_bundle_at: str | None
1069
+ frameworks_supported: list[str]
1070
+
1071
+
1072
+ # ---------------------------------------------------------------------------
1073
+ # Phase 6 — Observability Named SDK (sf-observe) types
1074
+ # ---------------------------------------------------------------------------
1075
+
1076
+
1077
+ class SamplerStrategy(_enum.Enum):
1078
+ """Trace sampling strategy for :class:`~spanforge.sdk.observe.SFObserveClient`.
1079
+
1080
+ Attributes:
1081
+ ALWAYS_ON: Every span is exported.
1082
+ ALWAYS_OFF: No spans are exported.
1083
+ PARENT_BASED: Respect parent sampling decision; use
1084
+ :attr:`ALWAYS_ON` when there is no parent.
1085
+ TRACE_ID_RATIO: Export a deterministic fraction of traces based on
1086
+ the trace-ID hash (see ``sample_rate``).
1087
+ """
1088
+
1089
+ ALWAYS_ON = "always_on"
1090
+ ALWAYS_OFF = "always_off"
1091
+ PARENT_BASED = "parent_based"
1092
+ TRACE_ID_RATIO = "trace_id_ratio"
1093
+
1094
+
1095
+ @dataclass(frozen=True)
1096
+ class ReceiverConfig:
1097
+ """Per-call receiver override for :meth:`~spanforge.sdk.observe.SFObserveClient.export_spans`.
1098
+
1099
+ When provided, overrides the global endpoint and headers for a single
1100
+ ``export_spans`` call.
1101
+
1102
+ Attributes:
1103
+ endpoint: Target OTLP/HTTP receiver URL
1104
+ (e.g. ``"https://collector.example.com/v1/traces"``).
1105
+ headers: Extra HTTP headers to include (e.g. authorization).
1106
+ timeout_seconds: Per-request timeout in seconds (default: 30).
1107
+ """
1108
+
1109
+ endpoint: str
1110
+ headers: dict[str, str] = field(default_factory=dict)
1111
+ timeout_seconds: float = 30.0
1112
+
1113
+
1114
+ @dataclass(frozen=True)
1115
+ class ExportResult:
1116
+ """Result of :meth:`~spanforge.sdk.observe.SFObserveClient.export_spans` (OBS-001).
1117
+
1118
+ Attributes:
1119
+ exported_count: Number of spans successfully exported.
1120
+ failed_count: Number of spans that could not be exported.
1121
+ backend: Backend used: ``"local"``, ``"otlp"``, ``"datadog"``,
1122
+ ``"grafana"``, ``"splunk"``, or ``"elastic"``.
1123
+ exported_at: ISO-8601 UTC timestamp of the export.
1124
+ """
1125
+
1126
+ exported_count: int
1127
+ failed_count: int
1128
+ backend: str
1129
+ exported_at: str
1130
+
1131
+
1132
+ @dataclass(frozen=True)
1133
+ class Annotation:
1134
+ """An observability annotation (OBS-002).
1135
+
1136
+ Stored by :meth:`~spanforge.sdk.observe.SFObserveClient.add_annotation`.
1137
+
1138
+ Attributes:
1139
+ annotation_id: Opaque UUID for this annotation.
1140
+ event_type: Category label for the annotation (e.g.
1141
+ ``"model_deployed"``, ``"alert_fired"``).
1142
+ payload: Arbitrary key/value metadata (must be JSON-serialisable).
1143
+ project_id: Project scope for this annotation.
1144
+ created_at: ISO-8601 UTC timestamp when the annotation was stored.
1145
+ """
1146
+
1147
+ annotation_id: str
1148
+ event_type: str
1149
+ payload: dict[str, Any]
1150
+ project_id: str
1151
+ created_at: str
1152
+
1153
+
1154
+ @dataclass(frozen=True)
1155
+ class ObserveStatusInfo:
1156
+ """sf-observe service status.
1157
+
1158
+ Returned by :meth:`~spanforge.sdk.observe.SFObserveClient.get_status`.
1159
+
1160
+ Attributes:
1161
+ status: Service status: ``"ok"`` or ``"degraded"``.
1162
+ backend: Active exporter backend name.
1163
+ sampler_strategy: Active :class:`SamplerStrategy` label.
1164
+ span_count: Total spans emitted in this session.
1165
+ annotation_count: Total annotations stored in this session.
1166
+ export_count: Total export calls completed in this session.
1167
+ last_export_at: ISO-8601 UTC timestamp of the most recent export,
1168
+ or ``None`` if none yet.
1169
+ healthy: ``True`` if the last export succeeded (or no export
1170
+ has been attempted).
1171
+ dropped_count: Total events dropped by the BatchExporter since
1172
+ startup (queue full or circuit open). ``None`` when
1173
+ no BatchExporter is attached.
1174
+ circuit_open: ``True`` when the BatchExporter circuit breaker has
1175
+ tripped open and new events are being dropped.
1176
+ ``None`` when no BatchExporter is attached.
1177
+ """
1178
+
1179
+ status: str
1180
+ backend: str
1181
+ sampler_strategy: str
1182
+ span_count: int
1183
+ annotation_count: int
1184
+ export_count: int
1185
+ last_export_at: str | None
1186
+ healthy: bool
1187
+ dropped_count: int | None = None
1188
+ circuit_open: bool | None = None
1189
+
1190
+
1191
+ # ---------------------------------------------------------------------------
1192
+ # Phase 7 — Alert Routing Service
1193
+ # ---------------------------------------------------------------------------
1194
+
1195
+ import enum as _enum
1196
+
1197
+
1198
+ class AlertSeverity(_enum.Enum):
1199
+ """Severity levels for :class:`PublishResult` and alert history.
1200
+
1201
+ Values are ordered from least to most severe. Use
1202
+ :meth:`AlertSeverity.from_str` to parse a case-insensitive string.
1203
+ """
1204
+
1205
+ INFO = "info"
1206
+ WARNING = "warning"
1207
+ HIGH = "high"
1208
+ CRITICAL = "critical"
1209
+
1210
+ @classmethod
1211
+ def from_str(cls, value: str) -> AlertSeverity:
1212
+ """Parse a severity string, returning WARNING on unknown values."""
1213
+ try:
1214
+ return cls(value.lower())
1215
+ except ValueError:
1216
+ return cls.WARNING
1217
+
1218
+
1219
+ @dataclass(frozen=True)
1220
+ class PublishResult:
1221
+ """Result of :meth:`~spanforge.sdk.alert.SFAlertClient.publish`.
1222
+
1223
+ Attributes:
1224
+ alert_id: UUID4 string uniquely identifying this alert emission.
1225
+ routed_to: List of sink names that were notified (may be empty when
1226
+ suppressed).
1227
+ suppressed: ``True`` when the alert was deduplicated, maintenance-window
1228
+ suppressed, or rate-limited and **not** dispatched.
1229
+ """
1230
+
1231
+ alert_id: str
1232
+ routed_to: list[str]
1233
+ suppressed: bool
1234
+
1235
+
1236
+ @dataclass(frozen=True)
1237
+ class TopicRegistration:
1238
+ """A registered alert topic with metadata.
1239
+
1240
+ Attributes:
1241
+ topic: Canonical topic string (e.g. ``"halluccheck.drift.red"``).
1242
+ description: Human-readable purpose of the topic.
1243
+ default_severity: Default :class:`AlertSeverity` applied when the caller
1244
+ does not specify one.
1245
+ runbook_url: Optional URL to a runbook for this alert topic.
1246
+ dedup_window_seconds: Per-topic deduplication window in seconds
1247
+ (overrides the client-wide default).
1248
+ """
1249
+
1250
+ topic: str
1251
+ description: str
1252
+ default_severity: str
1253
+ runbook_url: str | None = None
1254
+ dedup_window_seconds: float | None = None
1255
+
1256
+
1257
+ @dataclass(frozen=True)
1258
+ class MaintenanceWindow:
1259
+ """A scheduled maintenance window during which all alerts are suppressed.
1260
+
1261
+ Attributes:
1262
+ project_id: Project whose alerts are suppressed.
1263
+ start: Window start (UTC).
1264
+ end: Window end (UTC).
1265
+ """
1266
+
1267
+ project_id: str
1268
+ start: datetime
1269
+ end: datetime
1270
+
1271
+
1272
+ @dataclass(frozen=True)
1273
+ class AlertRecord:
1274
+ """An entry in the in-memory alert history.
1275
+
1276
+ Attributes:
1277
+ alert_id: UUID4 of the alert.
1278
+ topic: Full topic string.
1279
+ severity: Severity string (e.g. ``"critical"``).
1280
+ project_id: Project scope.
1281
+ payload: Caller-supplied payload dict.
1282
+ sinks_notified: Sink names that received this alert.
1283
+ suppressed: Whether the alert was suppressed.
1284
+ status: ``"open"``, ``"acknowledged"``, or ``"resolved"``.
1285
+ timestamp: ISO-8601 UTC emission time.
1286
+ """
1287
+
1288
+ alert_id: str
1289
+ topic: str
1290
+ severity: str
1291
+ project_id: str
1292
+ payload: dict[str, Any]
1293
+ sinks_notified: list[str]
1294
+ suppressed: bool
1295
+ status: str
1296
+ timestamp: str
1297
+
1298
+
1299
+ @dataclass(frozen=True)
1300
+ class AlertStatusInfo:
1301
+ """Health and session statistics for :class:`~spanforge.sdk.alert.SFAlertClient`.
1302
+
1303
+ Attributes:
1304
+ status: ``"ok"`` or ``"degraded"``.
1305
+ publish_count: Total ``publish()`` calls this session.
1306
+ suppress_count: Total suppressed alert count this session.
1307
+ queue_depth: Current number of items waiting in the dispatch queue.
1308
+ registered_topics: Number of topics in the registry.
1309
+ active_maintenance_windows: Number of currently active maintenance windows.
1310
+ healthy: ``True`` when no circuit-breaker is open.
1311
+ """
1312
+
1313
+ status: str
1314
+ publish_count: int
1315
+ suppress_count: int
1316
+ queue_depth: int
1317
+ registered_topics: int
1318
+ active_maintenance_windows: int
1319
+ healthy: bool
1320
+
1321
+
1322
+ # ---------------------------------------------------------------------------
1323
+ # Phase 8 — CI/CD Gate Pipeline (sf-gate) types
1324
+ # ---------------------------------------------------------------------------
1325
+
1326
+
1327
+ class GateVerdict:
1328
+ """Gate execution verdict constants (GAT-001).
1329
+
1330
+ Attributes:
1331
+ PASS: Gate conditions met; no blocking.
1332
+ FAIL: Gate conditions NOT met.
1333
+ WARN: Conditions not met but ``on_fail=warn``; pipeline continues.
1334
+ SKIPPED: Gate skipped due to ``skip_on`` / ``skip_on_draft`` rule.
1335
+ ERROR: Gate executor crashed with an unexpected exception.
1336
+ """
1337
+
1338
+ PASS = "PASS" # nosec B105
1339
+ FAIL = "FAIL"
1340
+ WARN = "WARN"
1341
+ SKIPPED = "SKIPPED"
1342
+ ERROR = "ERROR"
1343
+
1344
+
1345
+ class PRRIVerdict:
1346
+ """PRRI governance gate verdict constants (GAT-010).
1347
+
1348
+ Attributes:
1349
+ GREEN: PRRI score below amber threshold — gate passes.
1350
+ AMBER: PRRI score in amber zone — gate passes with warning.
1351
+ RED: PRRI score at or above red threshold — gate fails / blocks.
1352
+ """
1353
+
1354
+ GREEN = "GREEN"
1355
+ AMBER = "AMBER"
1356
+ RED = "RED"
1357
+
1358
+
1359
+ @dataclass
1360
+ class GateArtifact:
1361
+ """A single gate artifact record (GAT-003).
1362
+
1363
+ Attributes:
1364
+ gate_id: Unique gate identifier.
1365
+ name: Human-readable gate name.
1366
+ verdict: One of :class:`GateVerdict` constants.
1367
+ metrics: Collected metrics dict from the gate run.
1368
+ timestamp: ISO-8601 UTC timestamp when the gate completed.
1369
+ duration_ms: Wall-clock execution time in milliseconds.
1370
+ artifact_path: Absolute path to the written JSON artifact file.
1371
+ """
1372
+
1373
+ gate_id: str
1374
+ name: str
1375
+ verdict: str
1376
+ metrics: dict[str, Any]
1377
+ timestamp: str
1378
+ duration_ms: int
1379
+ artifact_path: str = ""
1380
+
1381
+
1382
+ @dataclass(frozen=True)
1383
+ class GateEvaluationResult:
1384
+ """Result of :meth:`~spanforge.sdk.gate.SFGateClient.evaluate` (GAT-004).
1385
+
1386
+ Attributes:
1387
+ gate_id: Gate identifier.
1388
+ verdict: One of :class:`GateVerdict` constants.
1389
+ metrics: Payload / metrics evaluated.
1390
+ artifact_url: File URI pointing to the written artifact.
1391
+ duration_ms: Wall-clock evaluation time in milliseconds.
1392
+ """
1393
+
1394
+ gate_id: str
1395
+ verdict: str
1396
+ metrics: dict[str, Any]
1397
+ artifact_url: str
1398
+ duration_ms: int
1399
+
1400
+
1401
+ @dataclass(frozen=True)
1402
+ class PRRIResult:
1403
+ """Result of :meth:`~spanforge.sdk.gate.SFGateClient.evaluate_prri` (GAT-010).
1404
+
1405
+ Attributes:
1406
+ gate_id: Always ``"gate5_governance"``.
1407
+ prri_score: Raw PRRI score (0–100).
1408
+ verdict: One of :class:`PRRIVerdict` constants.
1409
+ dimension_breakdown: Per-dimension score breakdown dict.
1410
+ framework: Regulatory framework identifier.
1411
+ policy_file: Path to the policy file used.
1412
+ timestamp: ISO-8601 UTC timestamp of evaluation.
1413
+ allow: ``True`` when the score does not block the pipeline.
1414
+ project_id: Project evaluated.
1415
+ """
1416
+
1417
+ gate_id: str
1418
+ prri_score: int
1419
+ verdict: str
1420
+ dimension_breakdown: dict[str, Any]
1421
+ framework: str
1422
+ policy_file: str
1423
+ timestamp: str
1424
+ allow: bool
1425
+ project_id: str = ""
1426
+
1427
+
1428
+ @dataclass(frozen=True)
1429
+ class TrustGateResult:
1430
+ """Result of :meth:`~spanforge.sdk.gate.SFGateClient.run_trust_gate` (GAT-020).
1431
+
1432
+ Attributes:
1433
+ gate_id: Always ``"gate6_trust"``.
1434
+ verdict: ``GateVerdict.PASS`` or ``GateVerdict.FAIL``.
1435
+ hri_critical_rate: Fraction of critical HRI events in the sample window.
1436
+ hri_critical_threshold: Failure threshold (default: 0.05).
1437
+ pii_detected: ``True`` if PII was detected in the window.
1438
+ pii_detections_24h: Number of PII detections in the last 24 h.
1439
+ secrets_detected: ``True`` if secrets were detected in the window.
1440
+ secrets_detections_24h: Number of secrets detections in the last 24 h.
1441
+ failures: Human-readable failure reasons (empty on PASS).
1442
+ timestamp: ISO-8601 UTC timestamp of evaluation.
1443
+ pipeline_id: CI/CD pipeline identifier.
1444
+ project_id: Project evaluated.
1445
+ pass_: ``True`` when the gate passes (no failures).
1446
+ """
1447
+
1448
+ gate_id: str
1449
+ verdict: str
1450
+ hri_critical_rate: float
1451
+ hri_critical_threshold: float
1452
+ pii_detected: bool
1453
+ pii_detections_24h: int
1454
+ secrets_detected: bool
1455
+ secrets_detections_24h: int
1456
+ failures: list[str]
1457
+ timestamp: str
1458
+ pipeline_id: str
1459
+ project_id: str
1460
+ pass_: bool = True
1461
+
1462
+
1463
+ # ---------------------------------------------------------------------------
1464
+ # Phase 10 — T.R.U.S.T. Scorecard & HallucCheck Contract
1465
+ # ---------------------------------------------------------------------------
1466
+
1467
+
1468
+ @dataclass(frozen=True)
1469
+ class TrustDimensionWeights:
1470
+ """Configurable weights for the five T.R.U.S.T. dimensions (TRS-001).
1471
+
1472
+ Each weight is a float ≥ 0. The overall T.R.U.S.T. score is a weighted
1473
+ average of the five dimension scores using these weights. Weights do not
1474
+ need to sum to 1.0 — they are normalised internally.
1475
+
1476
+ Attributes:
1477
+ transparency: Weight for Transparency (explainability events).
1478
+ reliability: Weight for Reliability (HRI + drift).
1479
+ user_trust: Weight for UserTrust (bias parity).
1480
+ security: Weight for Security (PII + secrets hygiene).
1481
+ traceability: Weight for Traceability (audit chain completeness).
1482
+ """
1483
+
1484
+ transparency: float = 1.0
1485
+ reliability: float = 1.0
1486
+ user_trust: float = 1.0
1487
+ security: float = 1.0
1488
+ traceability: float = 1.0
1489
+
1490
+
1491
+ @dataclass(frozen=True)
1492
+ class TrustHistoryEntry:
1493
+ """A single time-point entry in T.R.U.S.T. scorecard history (TRS-005).
1494
+
1495
+ Attributes:
1496
+ timestamp: ISO-8601 UTC timestamp for this snapshot.
1497
+ overall: Weighted overall T.R.U.S.T. score (0–100).
1498
+ transparency: Transparency dimension score (0–100).
1499
+ reliability: Reliability dimension score (0–100).
1500
+ user_trust: UserTrust dimension score (0–100).
1501
+ security: Security dimension score (0–100).
1502
+ traceability: Traceability dimension score (0–100).
1503
+ """
1504
+
1505
+ timestamp: str
1506
+ overall: float
1507
+ transparency: float
1508
+ reliability: float
1509
+ user_trust: float
1510
+ security: float
1511
+ traceability: float
1512
+
1513
+
1514
+ @dataclass(frozen=True)
1515
+ class TrustScorecardResponse:
1516
+ """Full T.R.U.S.T. scorecard API response (TRS-005).
1517
+
1518
+ Extends the existing :class:`TrustScorecard` with the five renamed
1519
+ T.R.U.S.T. dimensions and a weighted overall score.
1520
+
1521
+ Attributes:
1522
+ project_id: Scoping project.
1523
+ overall_score: Weighted average across all five dimensions (0–100).
1524
+ colour_band: ``"green"`` (≥ 80), ``"amber"`` (≥ 60), or ``"red"`` (< 60).
1525
+ transparency: Transparency dimension.
1526
+ reliability: Reliability dimension.
1527
+ user_trust: UserTrust dimension.
1528
+ security: Security dimension.
1529
+ traceability: Traceability dimension.
1530
+ from_dt: Reporting window start.
1531
+ to_dt: Reporting window end.
1532
+ record_count: Total contributing records.
1533
+ weights: The weights used for this computation.
1534
+ """
1535
+
1536
+ project_id: str
1537
+ overall_score: float
1538
+ colour_band: str
1539
+ transparency: TrustDimension
1540
+ reliability: TrustDimension
1541
+ user_trust: TrustDimension
1542
+ security: TrustDimension
1543
+ traceability: TrustDimension
1544
+ from_dt: str
1545
+ to_dt: str
1546
+ record_count: int
1547
+ weights: TrustDimensionWeights
1548
+
1549
+
1550
+ @dataclass(frozen=True)
1551
+ class TrustBadgeResult:
1552
+ """Result of the T.R.U.S.T. badge endpoint (TRS-006).
1553
+
1554
+ Attributes:
1555
+ svg: The SVG badge markup.
1556
+ overall: Weighted overall score (0–100).
1557
+ colour_band: ``"green"``, ``"amber"``, or ``"red"``.
1558
+ etag: ETag for cache-busting.
1559
+ """
1560
+
1561
+ svg: str
1562
+ overall: float
1563
+ colour_band: str
1564
+ etag: str
1565
+
1566
+
1567
+ @dataclass(frozen=True)
1568
+ class CompositeGateInput:
1569
+ """Input for the composite trust gate ``POST /v1/trust-gate`` (TRS-020).
1570
+
1571
+ Attributes:
1572
+ project_id: Project to evaluate.
1573
+ pipeline_id: CI/CD pipeline identifier.
1574
+ min_score: Minimum required overall T.R.U.S.T. score (0–100).
1575
+ run_pii_scan: Whether to run a PII scan check.
1576
+ run_secrets_scan: Whether to run a secrets scan check.
1577
+ run_hri_check: Whether to run an HRI critical-rate check.
1578
+ payload: Optional extra payload dict for gate evaluation.
1579
+ """
1580
+
1581
+ project_id: str
1582
+ pipeline_id: str = ""
1583
+ min_score: float = 60.0
1584
+ run_pii_scan: bool = True
1585
+ run_secrets_scan: bool = True
1586
+ run_hri_check: bool = True
1587
+ payload: dict[str, Any] = field(default_factory=dict)
1588
+
1589
+
1590
+ @dataclass(frozen=True)
1591
+ class CompositeGateResult:
1592
+ """Result of the composite trust gate (TRS-020).
1593
+
1594
+ Attributes:
1595
+ pass_: ``True`` when all checks pass.
1596
+ verdict: ``"PASS"`` or ``"FAIL"``.
1597
+ overall_score: Current T.R.U.S.T. overall score.
1598
+ colour_band: ``"green"``, ``"amber"``, or ``"red"``.
1599
+ trust_gate: The underlying :class:`TrustGateResult` if run.
1600
+ failures: List of human-readable failure reasons.
1601
+ timestamp: ISO-8601 UTC timestamp.
1602
+ """
1603
+
1604
+ pass_: bool
1605
+ verdict: str
1606
+ overall_score: float
1607
+ colour_band: str
1608
+ trust_gate: TrustGateResult | None
1609
+ failures: list[str]
1610
+ timestamp: str
1611
+
1612
+
1613
+ @dataclass(frozen=True)
1614
+ class PipelineResult:
1615
+ """Generic result for pipeline integration calls (TRS-010 through TRS-014).
1616
+
1617
+ Attributes:
1618
+ pipeline: Pipeline name (e.g. ``"score"``, ``"bias"``, ``"monitor"``).
1619
+ success: ``True`` when the pipeline completed without error.
1620
+ audit_id: Record ID from the sf-audit append call (if any).
1621
+ alerts_sent: Number of alerts published by this pipeline run.
1622
+ span_id: Span ID from sf-observe (if any).
1623
+ details: Extra key/value metadata.
1624
+ """
1625
+
1626
+ pipeline: str
1627
+ success: bool
1628
+ audit_id: str = ""
1629
+ alerts_sent: int = 0
1630
+ span_id: str = ""
1631
+ details: dict[str, Any] = field(default_factory=dict)
1632
+
1633
+
1634
+ @dataclass(frozen=True)
1635
+ class DSARResult:
1636
+ """Result of the DSAR export endpoint (TRS-025).
1637
+
1638
+ Attributes:
1639
+ subject_id: The data subject identifier.
1640
+ records: Matching audit records for this subject.
1641
+ record_count: Number of records returned.
1642
+ exported_at: ISO-8601 UTC timestamp.
1643
+ """
1644
+
1645
+ subject_id: str
1646
+ records: list[dict[str, Any]]
1647
+ record_count: int
1648
+ exported_at: str
1649
+
1650
+
1651
+ @dataclass(frozen=True)
1652
+ class TrustStatusInfo:
1653
+ """Health and statistics for the T.R.U.S.T. scorecard service (Phase 10).
1654
+
1655
+ Attributes:
1656
+ status: ``"ok"`` or ``"degraded"``.
1657
+ dimension_count: Number of dimensions (always 5).
1658
+ total_trust_records: Total T.R.U.S.T. records in the store.
1659
+ pipelines_registered: Number of registered pipeline integration points.
1660
+ last_scorecard_computed: ISO-8601 UTC timestamp of last computation.
1661
+ """
1662
+
1663
+ status: str
1664
+ dimension_count: int = 5
1665
+ total_trust_records: int = 0
1666
+ pipelines_registered: int = 5
1667
+ last_scorecard_computed: str | None = None
1668
+
1669
+
1670
+ @dataclass(frozen=True)
1671
+ class GateStatusInfo:
1672
+ """Health and session statistics for :class:`~spanforge.sdk.gate.SFGateClient`.
1673
+
1674
+ Attributes:
1675
+ status: ``"ok"`` or ``"degraded"``.
1676
+ evaluate_count: Total ``evaluate()`` calls this session.
1677
+ trust_gate_count: Total ``run_trust_gate()`` calls this session.
1678
+ last_evaluate_at: ISO-8601 UTC timestamp of the most recent
1679
+ ``evaluate()`` call, or ``None``.
1680
+ artifact_count: Number of artifact files in the store.
1681
+ artifact_dir: Absolute path to the artifact directory.
1682
+ open_circuit_breakers: List of gate-sink IDs with open circuit breakers.
1683
+ """
1684
+
1685
+ status: str
1686
+ evaluate_count: int
1687
+ trust_gate_count: int
1688
+ last_evaluate_at: str | None
1689
+ artifact_count: int
1690
+ artifact_dir: str
1691
+ open_circuit_breakers: list[str]
1692
+
1693
+
1694
+ # ---------------------------------------------------------------------------
1695
+ # Phase 11 — Enterprise Hardening & Supply Chain Security
1696
+ # ---------------------------------------------------------------------------
1697
+
1698
+
1699
+ class DataResidency:
1700
+ """Data residency region constants (ENT-004 / ENT-005).
1701
+
1702
+ Attributes:
1703
+ EU: European Union.
1704
+ US: United States.
1705
+ AP: Asia-Pacific.
1706
+ IN: India (DPDP).
1707
+ GLOBAL: No residency restriction.
1708
+ """
1709
+
1710
+ EU = "eu"
1711
+ US = "us"
1712
+ AP = "ap"
1713
+ IN = "in"
1714
+ GLOBAL = "global"
1715
+
1716
+ _ALL = frozenset({"eu", "us", "ap", "in", "global"})
1717
+
1718
+ @classmethod
1719
+ def is_valid(cls, value: str) -> bool:
1720
+ """Return ``True`` if *value* is a recognised residency region."""
1721
+ return value.lower() in cls._ALL
1722
+
1723
+
1724
+ @dataclass(frozen=True)
1725
+ class IsolationScope:
1726
+ """Composite key for namespace isolation (ENT-002).
1727
+
1728
+ Attributes:
1729
+ org_id: Organisation identifier.
1730
+ project_id: Project identifier.
1731
+ """
1732
+
1733
+ org_id: str
1734
+ project_id: str
1735
+
1736
+
1737
+ @dataclass(frozen=True)
1738
+ class TenantConfig:
1739
+ """Per-project multi-tenancy configuration (ENT-001 through ENT-005).
1740
+
1741
+ Attributes:
1742
+ project_id: Project identifier.
1743
+ org_id: Organisation owning this project.
1744
+ data_residency: One of :class:`DataResidency` constants.
1745
+ org_secret: Per-project HMAC secret for audit chain isolation (ENT-003).
1746
+ cross_project_read: ``True`` to allow cross-project queries.
1747
+ allowed_project_ids: Explicit list of project IDs allowed in cross-project queries.
1748
+ """
1749
+
1750
+ project_id: str
1751
+ org_id: str
1752
+ data_residency: str = "global"
1753
+ org_secret: str = ""
1754
+ cross_project_read: bool = False
1755
+ allowed_project_ids: list[str] = field(default_factory=list)
1756
+
1757
+
1758
+ @dataclass(frozen=True)
1759
+ class EncryptionConfig:
1760
+ """Encryption-at-rest and KMS configuration (ENT-010 through ENT-013).
1761
+
1762
+ Attributes:
1763
+ encrypt_at_rest: ``True`` to enable AES-256-GCM encryption of audit files.
1764
+ kms_provider: Cloud KMS provider: ``"aws"``, ``"azure"``, ``"gcp"``, or ``None``.
1765
+ mtls_enabled: ``True`` to enable mutual TLS.
1766
+ tls_cert_path: Path to TLS client certificate.
1767
+ tls_key_path: Path to TLS client private key.
1768
+ tls_ca_path: Path to TLS CA certificate bundle.
1769
+ fips_mode: ``True`` to restrict to FIPS 140-2 approved algorithms only.
1770
+ """
1771
+
1772
+ encrypt_at_rest: bool = False
1773
+ kms_provider: str | None = None
1774
+ mtls_enabled: bool = False
1775
+ tls_cert_path: str = ""
1776
+ tls_key_path: str = ""
1777
+ tls_ca_path: str = ""
1778
+ fips_mode: bool = False
1779
+
1780
+
1781
+ @dataclass(frozen=True)
1782
+ class AirGapConfig:
1783
+ """Air-gap and self-hosted configuration (ENT-020 through ENT-023).
1784
+
1785
+ Attributes:
1786
+ offline: ``True`` to run in fully offline mode (no network).
1787
+ self_hosted: ``True`` when running the self-hosted Docker stack.
1788
+ compose_file: Path to the Docker Compose file for the self-hosted stack.
1789
+ helm_release_name: Helm release name for Kubernetes deployment.
1790
+ health_check_interval_s: Interval between health endpoint polls (seconds).
1791
+ """
1792
+
1793
+ offline: bool = False
1794
+ self_hosted: bool = False
1795
+ compose_file: str = "docker-compose.yml"
1796
+ helm_release_name: str = "spanforge"
1797
+ health_check_interval_s: int = 30
1798
+
1799
+
1800
+ @dataclass(frozen=True)
1801
+ class RetentionExportPolicy:
1802
+ """Retention and export control policy for enterprise evidence flows.
1803
+
1804
+ Attributes:
1805
+ retention_days: Retention window for exported evidence.
1806
+ export_formats: Allowed export formats.
1807
+ require_encryption_for_export: ``True`` when package export requires encryption at rest.
1808
+ allow_external_sharing: ``True`` when evidence packages may leave the tenant boundary.
1809
+ classification: Data classification label for generated packages.
1810
+ """
1811
+
1812
+ retention_days: int = 2555
1813
+ export_formats: list[str] = field(default_factory=lambda: ["json"])
1814
+ require_encryption_for_export: bool = False
1815
+ allow_external_sharing: bool = False
1816
+ classification: str = "confidential"
1817
+
1818
+
1819
+ @dataclass(frozen=True)
1820
+ class DeploymentProfile:
1821
+ """Enterprise deployment profile for one project environment.
1822
+
1823
+ Attributes:
1824
+ project_id: Project identifier.
1825
+ org_id: Organisation / tenant identifier.
1826
+ environment: Deployment environment label.
1827
+ mode: Deployment mode such as ``"self_hosted"`` or ``"air_gapped"``.
1828
+ isolation_scope: Composite tenant isolation scope string.
1829
+ data_residency: Active residency region.
1830
+ offline_mode: ``True`` when network egress is disabled.
1831
+ self_hosted: ``True`` when the deployment is self-hosted.
1832
+ compose_file: Compose stack path when applicable.
1833
+ helm_release_name: Helm release name when applicable.
1834
+ key_management: Short description of the active key-management posture.
1835
+ """
1836
+
1837
+ project_id: str
1838
+ org_id: str
1839
+ environment: str
1840
+ mode: str
1841
+ isolation_scope: str
1842
+ data_residency: str
1843
+ offline_mode: bool = False
1844
+ self_hosted: bool = False
1845
+ compose_file: str = ""
1846
+ helm_release_name: str = ""
1847
+ key_management: str = "application_managed"
1848
+
1849
+
1850
+ @dataclass(frozen=True)
1851
+ class DeploymentArchitectureReference:
1852
+ """Reference architecture artifact for enterprise deployment.
1853
+
1854
+ Attributes:
1855
+ architecture_id: Stable identifier.
1856
+ title: Human-readable name.
1857
+ mode: Deployment mode this reference applies to.
1858
+ artifact_path: Repo-local path to the reference artifact.
1859
+ description: Short explanation of the artifact's purpose.
1860
+ """
1861
+
1862
+ architecture_id: str
1863
+ title: str
1864
+ mode: str
1865
+ artifact_path: str
1866
+ description: str
1867
+
1868
+
1869
+ @dataclass(frozen=True)
1870
+ class EnterpriseEvidencePackage:
1871
+ """Enterprise deployment and evidence package for audits.
1872
+
1873
+ Attributes:
1874
+ package_id: Opaque package identifier.
1875
+ trace_id: Runtime trace covered by the package.
1876
+ project_id: Project scope.
1877
+ org_id: Tenant scope.
1878
+ generated_at: ISO-8601 UTC generation timestamp.
1879
+ deployment_profile: Deployment profile at package generation time.
1880
+ retention_policy: Retention and export controls applied to the package.
1881
+ enterprise_status: Enterprise hardening status summary.
1882
+ operator_package: Embedded operator workflow package.
1883
+ architectures: Reference architecture artifacts.
1884
+ checksum: Package checksum.
1885
+ signature: Package signature.
1886
+ output_path: Written file path when exported.
1887
+ """
1888
+
1889
+ package_id: str
1890
+ trace_id: str
1891
+ project_id: str
1892
+ org_id: str
1893
+ generated_at: str
1894
+ deployment_profile: DeploymentProfile
1895
+ retention_policy: RetentionExportPolicy
1896
+ enterprise_status: EnterpriseStatusInfo
1897
+ operator_package: dict[str, Any]
1898
+ architectures: list[DeploymentArchitectureReference]
1899
+ checksum: str
1900
+ signature: str
1901
+ output_path: str | None = None
1902
+
1903
+
1904
+ @dataclass(frozen=True)
1905
+ class HealthEndpointResult:
1906
+ """Result from a container health probe (ENT-023).
1907
+
1908
+ Attributes:
1909
+ service: Service name (e.g. ``"sf-pii"``).
1910
+ endpoint: ``"/healthz"`` or ``"/readyz"``.
1911
+ status: HTTP status code.
1912
+ ok: ``True`` when status is 200.
1913
+ latency_ms: Round-trip time in milliseconds.
1914
+ checked_at: ISO-8601 UTC timestamp of the check.
1915
+ """
1916
+
1917
+ service: str
1918
+ endpoint: str
1919
+ status: int
1920
+ ok: bool
1921
+ latency_ms: float
1922
+ checked_at: str
1923
+
1924
+
1925
+ @dataclass(frozen=True)
1926
+ class DependencyVulnerability:
1927
+ """A single dependency vulnerability (ENT-033).
1928
+
1929
+ Attributes:
1930
+ package: Package name (e.g. ``"requests"``).
1931
+ version: Installed version.
1932
+ advisory_id: Advisory identifier (e.g. ``"CVE-2023-12345"``).
1933
+ severity: ``"critical"``, ``"high"``, ``"medium"``, or ``"low"``.
1934
+ description: Human-readable description.
1935
+ fix_version: Version that fixes the vulnerability (empty if unknown).
1936
+ """
1937
+
1938
+ package: str
1939
+ version: str
1940
+ advisory_id: str
1941
+ severity: str
1942
+ description: str
1943
+ fix_version: str = ""
1944
+
1945
+
1946
+ @dataclass(frozen=True)
1947
+ class StaticAnalysisFinding:
1948
+ """A single static analysis finding (ENT-034).
1949
+
1950
+ Attributes:
1951
+ tool: Tool name (``"bandit"`` or ``"semgrep"``).
1952
+ rule_id: Rule identifier.
1953
+ severity: ``"high"``, ``"medium"``, or ``"low"``.
1954
+ file_path: File where the finding was detected.
1955
+ line: Line number.
1956
+ message: Human-readable description.
1957
+ """
1958
+
1959
+ tool: str
1960
+ rule_id: str
1961
+ severity: str
1962
+ file_path: str
1963
+ line: int
1964
+ message: str
1965
+
1966
+
1967
+ @dataclass(frozen=True)
1968
+ class ThreatModelEntry:
1969
+ """A STRIDE threat model entry (ENT-031).
1970
+
1971
+ Attributes:
1972
+ service: Service boundary (e.g. ``"sf-identity"``).
1973
+ category: STRIDE category: ``"spoofing"``, ``"tampering"``,
1974
+ ``"repudiation"``, ``"information_disclosure"``,
1975
+ ``"denial_of_service"``, or ``"elevation_of_privilege"``.
1976
+ threat: Description of the threat.
1977
+ mitigation: Description of the mitigation.
1978
+ risk_level: ``"high"``, ``"medium"``, or ``"low"``.
1979
+ reviewed_at: ISO-8601 UTC timestamp of the last review.
1980
+ """
1981
+
1982
+ service: str
1983
+ category: str
1984
+ threat: str
1985
+ mitigation: str
1986
+ risk_level: str
1987
+ reviewed_at: str
1988
+
1989
+
1990
+ @dataclass(frozen=True)
1991
+ class SecurityScanResult:
1992
+ """Result of a dependency + static analysis scan (ENT-033 / ENT-034).
1993
+
1994
+ Attributes:
1995
+ vulnerabilities: List of :class:`DependencyVulnerability` findings.
1996
+ static_findings: List of :class:`StaticAnalysisFinding` results.
1997
+ secrets_in_logs: Number of secrets found in log lines (ENT-035).
1998
+ pass_: ``True`` when no critical/high findings block merge.
1999
+ scanned_at: ISO-8601 UTC timestamp.
2000
+ """
2001
+
2002
+ vulnerabilities: list[DependencyVulnerability]
2003
+ static_findings: list[StaticAnalysisFinding]
2004
+ secrets_in_logs: int
2005
+ pass_: bool
2006
+ scanned_at: str
2007
+
2008
+
2009
+ @dataclass(frozen=True)
2010
+ class SecurityAuditResult:
2011
+ """Result of the OWASP API Security Top 10 audit (ENT-030).
2012
+
2013
+ Attributes:
2014
+ categories: Dict mapping OWASP category IDs to status (``"pass"``
2015
+ or ``"fail"`` with detail).
2016
+ pass_: ``True`` when all 10 categories pass.
2017
+ audited_at: ISO-8601 UTC timestamp.
2018
+ threat_model: List of :class:`ThreatModelEntry` items (ENT-031).
2019
+ """
2020
+
2021
+ categories: dict[str, dict[str, str]]
2022
+ pass_: bool
2023
+ audited_at: str
2024
+ threat_model: list[ThreatModelEntry] = field(default_factory=list)
2025
+
2026
+
2027
+ @dataclass(frozen=True)
2028
+ class EnterpriseStatusInfo:
2029
+ """Health and configuration summary for enterprise hardening (Phase 11).
2030
+
2031
+ Attributes:
2032
+ status: ``"ok"`` or ``"degraded"``.
2033
+ multi_tenancy_enabled: ``True`` when project-level isolation is active.
2034
+ encryption_at_rest: ``True`` when AES-256-GCM is configured.
2035
+ fips_mode: ``True`` when FIPS 140-2 mode is active.
2036
+ offline_mode: ``True`` when air-gap offline mode is active.
2037
+ data_residency: Active data residency region.
2038
+ tenant_count: Number of registered tenant projects.
2039
+ last_security_scan: ISO-8601 UTC timestamp of the last scan.
2040
+ """
2041
+
2042
+ status: str
2043
+ multi_tenancy_enabled: bool = False
2044
+ encryption_at_rest: bool = False
2045
+ fips_mode: bool = False
2046
+ offline_mode: bool = False
2047
+ data_residency: str = "global"
2048
+ tenant_count: int = 0
2049
+ last_security_scan: str | None = None
2050
+
2051
+
2052
+ # ---------------------------------------------------------------------------
2053
+ # Phase 1 — SSO: SCIM 2.0, OIDC, SAML (ID-040 – ID-043)
2054
+ # ---------------------------------------------------------------------------
2055
+
2056
+
2057
+ @dataclass(frozen=True)
2058
+ class SCIMUser:
2059
+ """RFC 7644 SCIM 2.0 User resource.
2060
+
2061
+ Attributes:
2062
+ id: SpanForge-assigned unique user id.
2063
+ user_name: Primary identifier (usually email).
2064
+ display_name: Human-readable full name.
2065
+ active: Whether the account is active.
2066
+ email: Primary e-mail address.
2067
+ groups: List of group ids the user belongs to.
2068
+ external_id: Optional id from the provisioning IdP.
2069
+ meta: Resource metadata dict (``resourceType``, ``created``,
2070
+ ``lastModified``, ``location``).
2071
+ """
2072
+
2073
+ id: str
2074
+ user_name: str
2075
+ display_name: str
2076
+ active: bool
2077
+ email: str
2078
+ groups: list[str] = field(default_factory=list)
2079
+ external_id: str | None = None
2080
+ meta: dict[str, str] = field(default_factory=dict)
2081
+
2082
+
2083
+ @dataclass(frozen=True)
2084
+ class SCIMGroup:
2085
+ """RFC 7644 SCIM 2.0 Group resource.
2086
+
2087
+ Attributes:
2088
+ id: SpanForge-assigned unique group id.
2089
+ display_name: Human-readable group name.
2090
+ members: List of SCIMUser ids in the group.
2091
+ external_id: Optional id from the provisioning IdP.
2092
+ meta: Resource metadata dict.
2093
+ """
2094
+
2095
+ id: str
2096
+ display_name: str
2097
+ members: list[str] = field(default_factory=list)
2098
+ external_id: str | None = None
2099
+ meta: dict[str, str] = field(default_factory=dict)
2100
+
2101
+
2102
+ @dataclass(frozen=True)
2103
+ class SCIMListResponse:
2104
+ """RFC 7644 SCIM 2.0 ListResponse envelope.
2105
+
2106
+ Attributes:
2107
+ total_results: Total matching resources (before pagination).
2108
+ start_index: 1-based index of the first result in this page.
2109
+ items_per_page: Number of resources returned.
2110
+ resources: List of :class:`SCIMUser` or :class:`SCIMGroup` objects.
2111
+ """
2112
+
2113
+ total_results: int
2114
+ start_index: int
2115
+ items_per_page: int
2116
+ resources: list[Any]
2117
+
2118
+
2119
+ @dataclass(frozen=True)
2120
+ class OIDCAuthRequest:
2121
+ """Parameters returned by :meth:`SFIdentityClient.oidc_authorize`.
2122
+
2123
+ Attributes:
2124
+ authorization_url: Full URL to redirect the user to at the IdP.
2125
+ state: Opaque CSRF-protection state token.
2126
+ code_verifier: PKCE plain-text verifier (client must store; never
2127
+ send to IdP).
2128
+ code_challenge: SHA-256 code challenge sent in the auth request.
2129
+ nonce: Replay-prevention nonce embedded in the id_token.
2130
+ """
2131
+
2132
+ authorization_url: str
2133
+ state: str
2134
+ code_verifier: str
2135
+ code_challenge: str
2136
+ nonce: str
2137
+
2138
+
2139
+ @dataclass(frozen=True)
2140
+ class OIDCTokenResult:
2141
+ """Token response returned by :meth:`SFIdentityClient.oidc_callback`.
2142
+
2143
+ Attributes:
2144
+ session_jwt: SpanForge-native session JWT (RS256/HS256).
2145
+ id_token: Raw id_token from the IdP (may be empty in local mode).
2146
+ access_token: OAuth 2.0 access token (may be empty in local mode).
2147
+ expires_in: Session TTL in seconds.
2148
+ subject: ``sub`` claim from the id_token.
2149
+ email: ``email`` claim (may be empty).
2150
+ """
2151
+
2152
+ session_jwt: str
2153
+ id_token: str
2154
+ access_token: str
2155
+ expires_in: int
2156
+ subject: str
2157
+ email: str
2158
+
2159
+
2160
+ @dataclass(frozen=True)
2161
+ class SSOSession:
2162
+ """A SpanForge-native session delegated from an IdP session.
2163
+
2164
+ Attributes:
2165
+ session_id: SpanForge-internal session identifier.
2166
+ idp_session_id: Opaque session id supplied by the IdP.
2167
+ subject: ``sub`` claim from the IdP.
2168
+ email: Email address (may be empty).
2169
+ jwt: SpanForge session JWT for this delegated session.
2170
+ project_id: Project the session is scoped to.
2171
+ created_at: ISO-8601 UTC creation timestamp.
2172
+ expires_at: ISO-8601 UTC expiry timestamp.
2173
+ active: Whether the session is still valid.
2174
+ """
2175
+
2176
+ session_id: str
2177
+ idp_session_id: str
2178
+ subject: str
2179
+ email: str
2180
+ jwt: str
2181
+ project_id: str
2182
+ created_at: str
2183
+ expires_at: str
2184
+ active: bool = True