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,1096 @@
1
+ """spanforge.sdk._exceptions — Error hierarchy for the SpanForge service SDK.
2
+
3
+ All SDK errors inherit from :class:`SFError`. Callers can catch the whole
4
+ family with ``except SFError`` or target specific subtypes for fine-grained
5
+ handling.
6
+
7
+ Security requirements
8
+ ---------------------
9
+ * Error messages **never** include API key values, HMAC secrets, JWT private
10
+ keys, TOTP secrets, or raw PII.
11
+ * IP addresses in :class:`SFIPDeniedError` are reported as-is (they are not
12
+ secret) to aid diagnosability without leaking private material.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+
19
+ __all__ = [
20
+ # Phase 11 — Enterprise Hardening & Supply Chain Security
21
+ "SFAirGapError",
22
+ # Phase 7 — Alert Routing Service
23
+ "SFAlertError",
24
+ "SFAlertPublishError",
25
+ "SFAlertQueueFullError",
26
+ "SFAlertRateLimitedError",
27
+ # Phase 4 — Audit service
28
+ "SFAuditAppendError",
29
+ "SFAuditError",
30
+ "SFAuditQueryError",
31
+ "SFAuditSchemaError",
32
+ # Base
33
+ "SFAuthError",
34
+ "SFBruteForceLockedError",
35
+ # Phase 5 — Compliance Evidence Chain
36
+ "SFCECBuildError",
37
+ "SFCECError",
38
+ "SFCECExportError",
39
+ "SFCECVerifyError",
40
+ # Phase 9 — Integration Config & Local Fallback
41
+ "SFConfigError",
42
+ "SFConfigValidationError",
43
+ "SFDataResidencyError",
44
+ "SFEncryptionError",
45
+ "SFEnterpriseError",
46
+ "SFError",
47
+ "SFFIPSError",
48
+ "SFIPDeniedError",
49
+ "SFIsolationError",
50
+ "SFKeyFormatError",
51
+ "SFMFARequiredError",
52
+ # Phase 6 — Observability Named SDK
53
+ "SFObserveAnnotationError",
54
+ "SFObserveEmitError",
55
+ "SFObserveError",
56
+ "SFObserveExportError",
57
+ # Phase 3 — PII hardening
58
+ "SFPIIBlockedError",
59
+ "SFPIIDPDPConsentMissingError",
60
+ # Phase 2 — PII
61
+ "SFPIIError",
62
+ "SFPIINotRedactedError",
63
+ "SFPIIPolicyError",
64
+ "SFPIIScanError",
65
+ "SFQuotaExceededError",
66
+ "SFRateLimitError",
67
+ "SFScopeError",
68
+ "SFSecretsBlockedError",
69
+ "SFSecretsError",
70
+ "SFSecretsInLogsError",
71
+ "SFSecretsScanError",
72
+ "SFSecurityScanError",
73
+ "SFServiceUnavailableError",
74
+ "SFStartupError",
75
+ "SFTokenInvalidError",
76
+ ]
77
+
78
+
79
+ class SFError(Exception):
80
+ """Base class for all SpanForge SDK errors.
81
+
82
+ All public-facing SDK exceptions derive from this class, enabling callers
83
+ to write a single broad ``except SFError`` guard as a safety net while
84
+ still being able to catch specific sub-types for targeted handling.
85
+ """
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Authentication errors
90
+ # ---------------------------------------------------------------------------
91
+
92
+
93
+ class SFAuthError(SFError):
94
+ """Authentication failed.
95
+
96
+ Raised when credentials are missing, malformed, or rejected by the
97
+ sf-identity service.
98
+ """
99
+
100
+
101
+ class SFKeyFormatError(SFAuthError):
102
+ """API key does not match the ``sf_(live|test)_<48-base62>`` format.
103
+
104
+ Args:
105
+ detail: Human-readable description of the format violation.
106
+
107
+ Example::
108
+
109
+ try:
110
+ KeyFormat.validate("not-a-key")
111
+ except SFKeyFormatError as exc:
112
+ print(exc.detail) # "Key must match sf_(live|test)_<48 base62 chars>; ..."
113
+ """
114
+
115
+ def __init__(self, detail: str) -> None:
116
+ self.detail = detail
117
+ super().__init__(f"API key format error: {detail}")
118
+
119
+
120
+ class SFTokenInvalidError(SFAuthError):
121
+ """JWT validation failed (expired, bad signature, or revoked).
122
+
123
+ Args:
124
+ reason: Short description of why validation failed. Must not contain
125
+ secret material.
126
+
127
+ Example::
128
+
129
+ try:
130
+ claims = identity.verify_token(jwt)
131
+ except SFTokenInvalidError as exc:
132
+ print(exc.reason) # "JWT has expired"
133
+ """
134
+
135
+ def __init__(self, reason: str) -> None:
136
+ self.reason = reason
137
+ super().__init__(f"Token invalid: {reason}")
138
+
139
+
140
+ class SFIPDeniedError(SFAuthError):
141
+ """Request IP address is not in the key's ``ip_allowlist``.
142
+
143
+ Args:
144
+ ip: The IP address that was denied.
145
+
146
+ Example::
147
+
148
+ try:
149
+ identity.check_ip_allowlist("key_abc123", "10.0.0.5")
150
+ except SFIPDeniedError as exc:
151
+ print(exc.ip) # "10.0.0.5"
152
+ """
153
+
154
+ def __init__(self, ip: str) -> None:
155
+ self.ip = ip
156
+ super().__init__(f"IP address {ip!r} is not in the key's allowlist")
157
+
158
+
159
+ class SFMFARequiredError(SFAuthError):
160
+ """MFA factor must be provided before a session token can be issued.
161
+
162
+ Args:
163
+ challenge_id: Opaque identifier the caller must return when
164
+ submitting the OTP.
165
+
166
+ Example::
167
+
168
+ try:
169
+ bundle = identity.exchange_magic_link(token)
170
+ except SFMFARequiredError as exc:
171
+ otp = input("Enter your TOTP code: ")
172
+ bundle = identity.exchange_magic_link(token, mfa_challenge=exc.challenge_id, otp=otp)
173
+ """
174
+
175
+ def __init__(self, challenge_id: str) -> None:
176
+ self.challenge_id = challenge_id
177
+ super().__init__(
178
+ f"MFA is required; challenge_id={challenge_id!r}. "
179
+ "Submit TOTP code via exchange_magic_link(mfa_challenge=..., otp=...)."
180
+ )
181
+
182
+
183
+ class SFBruteForceLockedError(SFAuthError):
184
+ """Account is temporarily locked due to repeated authentication failures.
185
+
186
+ Args:
187
+ unlock_at: ISO-8601 timestamp when the lockout expires.
188
+ resource: What was locked — e.g. ``"magic_link:user@example.com"``
189
+ or ``"totp:key_abc"``.
190
+ """
191
+
192
+ def __init__(self, unlock_at: str, resource: str = "") -> None:
193
+ self.unlock_at = unlock_at
194
+ self.resource = resource
195
+ super().__init__(
196
+ f"Locked until {unlock_at}" + (f" (resource={resource!r})" if resource else "")
197
+ )
198
+
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # Service availability errors
202
+ # ---------------------------------------------------------------------------
203
+
204
+
205
+ class SFServiceUnavailableError(SFError):
206
+ """Service is unreachable and ``local_fallback`` is disabled.
207
+
208
+ Args:
209
+ service: Short name of the unavailable service (e.g. ``"identity"``).
210
+ """
211
+
212
+ def __init__(self, service: str) -> None:
213
+ self.service = service
214
+ super().__init__(
215
+ f"sf-{service} is unavailable and local_fallback is disabled. "
216
+ "Set local_fallback_enabled=True or restore service connectivity."
217
+ )
218
+
219
+
220
+ class SFStartupError(SFError):
221
+ """A required service was unreachable at startup and fallback is disabled.
222
+
223
+ Args:
224
+ services: List of service names that failed their startup health check.
225
+ """
226
+
227
+ def __init__(self, services: list[str]) -> None:
228
+ self.services = services
229
+ super().__init__(
230
+ f"Required services unreachable at startup: {services}. "
231
+ "Set local_fallback_enabled=True or restore connectivity before starting."
232
+ )
233
+
234
+
235
+ # ---------------------------------------------------------------------------
236
+ # Quota and scope errors
237
+ # ---------------------------------------------------------------------------
238
+
239
+
240
+ class SFRateLimitError(SFError):
241
+ """Rate limit or daily quota exceeded.
242
+
243
+ Args:
244
+ retry_after: Seconds to wait before retrying (from ``Retry-After``
245
+ response header or estimated reset window).
246
+ """
247
+
248
+ def __init__(self, retry_after: int = 60) -> None:
249
+ self.retry_after = retry_after
250
+ super().__init__(
251
+ f"Rate limit exceeded. Retry after {retry_after} second(s). "
252
+ "See X-SF-RateLimit-Reset header for precise reset time."
253
+ )
254
+
255
+
256
+ class SFQuotaExceededError(SFRateLimitError):
257
+ """Daily scored-record quota for the current tier has been exhausted.
258
+
259
+ Args:
260
+ tier: Pricing tier name (e.g. ``"api"``).
261
+ daily_limit: Maximum records allowed per day on this tier.
262
+ retry_after: Seconds until quota resets (midnight UTC).
263
+ """
264
+
265
+ def __init__(self, tier: str, daily_limit: int, retry_after: int) -> None:
266
+ self.tier = tier
267
+ self.daily_limit = daily_limit
268
+ super().__init__(retry_after=retry_after)
269
+ self.args = (
270
+ f"Daily quota of {daily_limit} records exceeded for tier '{tier}'. "
271
+ f"Quota resets in {retry_after}s (midnight UTC). "
272
+ "Upgrade to a higher tier for more capacity.",
273
+ )
274
+
275
+
276
+ class SFScopeError(SFAuthError):
277
+ """The API key does not have the required scope for this operation.
278
+
279
+ Args:
280
+ required_scope: The scope that was needed.
281
+ key_scopes: The scopes the key actually has.
282
+ """
283
+
284
+ def __init__(self, required_scope: str, key_scopes: list[str]) -> None:
285
+ self.required_scope = required_scope
286
+ self.key_scopes = key_scopes
287
+ super().__init__(
288
+ f"Key lacks required scope {required_scope!r}. Key has scopes: {key_scopes}."
289
+ )
290
+
291
+
292
+ # ---------------------------------------------------------------------------
293
+ # Phase 2 — PII redaction service errors
294
+ # ---------------------------------------------------------------------------
295
+
296
+
297
+ class SFPIIError(SFError):
298
+ """Base class for all PII redaction service errors.
299
+
300
+ Callers can write ``except SFPIIError`` to handle any PII-related failure.
301
+ """
302
+
303
+
304
+ class SFPIINotRedactedError(SFPIIError):
305
+ """Unredacted PII detected in an event payload.
306
+
307
+ Raised by :meth:`~spanforge.sdk.pii.SFPIIClient.assert_redacted` when
308
+ :class:`~spanforge.redact.Redactable` instances or raw-string PII remain
309
+ in an event after a :class:`~spanforge.redact.RedactionPolicy` should
310
+ have been applied.
311
+
312
+ Security: the error message never contains PII values. The optional
313
+ *context* string is SHA-256-hashed before inclusion so identifiers are
314
+ preserved for correlation without disclosing content.
315
+
316
+ Args:
317
+ count: Number of unredacted PII fields detected.
318
+ context: Optional call-site label (hashed before inclusion).
319
+
320
+ Attributes:
321
+ count: Number of outstanding unredacted fields.
322
+ """
323
+
324
+ count: int
325
+
326
+ def __init__(self, count: int, context: str = "") -> None:
327
+ self.count = count
328
+ ctx = ""
329
+ if context:
330
+ ctx_hash = hashlib.sha256(context.encode()).hexdigest()[:8]
331
+ ctx = f" [context-hash:{ctx_hash}]"
332
+ super().__init__(
333
+ f"Found {count} unredacted PII field(s){ctx}. "
334
+ "Apply a RedactionPolicy before serialising or exporting this event."
335
+ )
336
+
337
+
338
+ class SFPIIScanError(SFPIIError):
339
+ """Scan or anonymize operation failed.
340
+
341
+ Raised when :meth:`~spanforge.sdk.pii.SFPIIClient.scan` or
342
+ :meth:`~spanforge.sdk.pii.SFPIIClient.anonymize` encounters a structural
343
+ error (e.g. non-dict payload, maximum nesting depth exceeded).
344
+ """
345
+
346
+
347
+ class SFPIIPolicyError(SFPIIError):
348
+ """Invalid PII policy configuration.
349
+
350
+ Raised when :meth:`~spanforge.sdk.pii.SFPIIClient.make_policy` or
351
+ :meth:`~spanforge.sdk.pii.SFPIIClient.wrap` is called with an invalid
352
+ ``min_sensitivity`` level or a malformed replacement template.
353
+
354
+ Args:
355
+ detail: Human-readable description of the configuration problem.
356
+
357
+ Attributes:
358
+ detail: The detail message passed at construction time.
359
+ """
360
+
361
+ def __init__(self, detail: str) -> None:
362
+ self.detail = detail
363
+ super().__init__(f"PII policy configuration error: {detail}")
364
+
365
+
366
+ # ---------------------------------------------------------------------------
367
+ # Phase 2 — Secrets scanning service errors
368
+ # ---------------------------------------------------------------------------
369
+
370
+
371
+ class SFSecretsError(SFError):
372
+ """Base class for all secrets scanning service errors.
373
+
374
+ Callers can write ``except SFSecretsError`` to handle any secrets-related
375
+ failure.
376
+ """
377
+
378
+
379
+ class SFSecretsBlockedError(SFSecretsError):
380
+ """One or more secrets were detected and the auto-block policy fired.
381
+
382
+ Raised when the caller's policy requires that processing be halted after
383
+ a high-confidence or zero-tolerance secret is detected.
384
+
385
+ Args:
386
+ secret_types: List of detected secret type labels (e.g.
387
+ ``["aws_access_key", "stripe_live_key"]``).
388
+ count: Number of blocking hits.
389
+
390
+ Attributes:
391
+ secret_types: Labels of the detected secret types.
392
+ count: Number of hits that triggered the block.
393
+
394
+ Example::
395
+
396
+ result = sf_secrets.scan(text)
397
+ if result.auto_blocked:
398
+ raise SFSecretsBlockedError(
399
+ secret_types=result.secret_types,
400
+ count=len(result.hits),
401
+ )
402
+ """
403
+
404
+ def __init__(self, secret_types: list[str], count: int = 1) -> None:
405
+ self.secret_types = secret_types
406
+ self.count = count
407
+ types_str = ", ".join(repr(t) for t in secret_types) if secret_types else "(unknown)"
408
+ super().__init__(
409
+ f"Secrets scan blocked: {count} secret(s) detected of type(s) {types_str}. "
410
+ "Remove the secret and rotate credentials before continuing."
411
+ )
412
+
413
+
414
+ class SFSecretsScanError(SFSecretsError):
415
+ """Secrets scan operation failed.
416
+
417
+ Raised when :meth:`~spanforge.sdk.secrets.SFSecretsClient.scan` or
418
+ :meth:`~spanforge.sdk.secrets.SFSecretsClient.scan_batch` encounters a
419
+ structural error (e.g. non-str input, invalid configuration).
420
+ """
421
+
422
+
423
+ # ---------------------------------------------------------------------------
424
+ # Phase 3 — PII hardening errors
425
+ # ---------------------------------------------------------------------------
426
+
427
+
428
+ class SFPIIBlockedError(SFPIIError):
429
+ """PII was detected and the pipeline action is ``"block"``.
430
+
431
+ Raised by
432
+ :meth:`~spanforge.sdk.pii.SFPIIClient.apply_pipeline_action` when PII is
433
+ detected above the confidence threshold and *action* is ``"block"``.
434
+ Callers should return HTTP 422 with error code ``PII_DETECTED``.
435
+
436
+ Args:
437
+ entity_types: List of entity type labels that triggered the block.
438
+ count: Number of above-threshold entities detected.
439
+
440
+ Attributes:
441
+ entity_types: Labels of the entity types that triggered the block.
442
+ count: Number of blocking hits.
443
+ """
444
+
445
+ def __init__(self, entity_types: list[str], count: int = 1) -> None:
446
+ self.entity_types = entity_types
447
+ self.count = count
448
+ types_str = ", ".join(repr(t) for t in entity_types) if entity_types else "(unknown)"
449
+ super().__init__(
450
+ f"PII detected ({count} entity/entities of type(s) {types_str}) — "
451
+ "pipeline action 'block' prevents scoring. "
452
+ "Remove PII from the input or change the pipeline pii_action to 'flag' or 'redact'."
453
+ )
454
+
455
+
456
+ class SFPIIDPDPConsentMissingError(SFPIIError):
457
+ """DPDP scope enforcement: consent record absent for the current purpose.
458
+
459
+ Raised by :meth:`~spanforge.sdk.pii.SFPIIClient.scan_text` when the
460
+ scanned text contains a DPDP-regulated entity type AND no valid consent
461
+ record exists for the current processing purpose in sf-audit schema
462
+ ``spanforge.consent.v1``.
463
+
464
+ Args:
465
+ subject_id: Opaque subject identifier (hashed before inclusion).
466
+ purpose: The processing purpose that lacks consent.
467
+ entity_type: The DPDP entity type that triggered the check.
468
+
469
+ Attributes:
470
+ purpose: Processing purpose string.
471
+ entity_type: The entity type that triggered the error.
472
+ """
473
+
474
+ def __init__(self, subject_id: str, purpose: str, entity_type: str) -> None:
475
+ self.purpose = purpose
476
+ self.entity_type = entity_type
477
+ # Hash subject_id to avoid leaking PII in the exception message.
478
+ sid_hash = hashlib.sha256(subject_id.encode()).hexdigest()[:12]
479
+ super().__init__(
480
+ f"DPDP_CONSENT_MISSING: No valid consent for purpose={purpose!r} "
481
+ f"covering entity_type={entity_type!r} "
482
+ f"(subject-hash:{sid_hash}). "
483
+ "Obtain explicit consent before processing this data."
484
+ )
485
+
486
+
487
+ # ---------------------------------------------------------------------------
488
+ # Phase 4 — Audit service errors
489
+ # ---------------------------------------------------------------------------
490
+
491
+
492
+ class SFAuditError(SFError):
493
+ """Base class for all audit service errors.
494
+
495
+ Callers can write ``except SFAuditError`` to handle any audit-related
496
+ failure.
497
+ """
498
+
499
+
500
+ class SFAuditSchemaError(SFAuditError):
501
+ """Unknown or invalid audit schema key.
502
+
503
+ Raised by :meth:`~spanforge.sdk.audit.SFAuditClient.append` when
504
+ *schema_key* is not in the known registry and ``strict_schema=True``
505
+ (the default).
506
+
507
+ Args:
508
+ schema_key: The schema key that was rejected.
509
+ known_keys: The set of accepted schema keys.
510
+
511
+ Attributes:
512
+ schema_key: The rejected schema key.
513
+ """
514
+
515
+ def __init__(self, schema_key: str, known_keys: frozenset[str]) -> None:
516
+ self.schema_key = schema_key
517
+ keys_sample = ", ".join(sorted(known_keys)[:5])
518
+ more = len(known_keys) - 5
519
+ hint = f"{keys_sample}" + (f", … (+{more} more)" if more > 0 else "")
520
+ super().__init__(
521
+ f"Unknown audit schema key {schema_key!r}. "
522
+ f"Known keys include: {hint}. "
523
+ "Pass strict_schema=False to allow unknown keys."
524
+ )
525
+
526
+
527
+ class SFAuditAppendError(SFAuditError):
528
+ """An append operation to the audit store failed.
529
+
530
+ Args:
531
+ detail: Human-readable description of the failure.
532
+
533
+ Attributes:
534
+ detail: The detail message passed at construction time.
535
+ """
536
+
537
+ def __init__(self, detail: str) -> None:
538
+ self.detail = detail
539
+ super().__init__(f"Audit append failed: {detail}")
540
+
541
+
542
+ class SFAuditQueryError(SFAuditError):
543
+ """An audit store query operation failed.
544
+
545
+ Args:
546
+ detail: Human-readable description of the failure.
547
+
548
+ Attributes:
549
+ detail: The detail message passed at construction time.
550
+ """
551
+
552
+ def __init__(self, detail: str) -> None:
553
+ self.detail = detail
554
+ super().__init__(f"Audit query failed: {detail}")
555
+
556
+
557
+ # ---------------------------------------------------------------------------
558
+ # Phase 5 — Compliance Evidence Chain errors
559
+ # ---------------------------------------------------------------------------
560
+
561
+
562
+ class SFCECError(SFError):
563
+ """Base class for all Compliance Evidence Chain service errors.
564
+
565
+ Callers can write ``except SFCECError`` to handle any CEC-related failure.
566
+ """
567
+
568
+
569
+ class SFCECBuildError(SFCECError):
570
+ """Bundle assembly failed.
571
+
572
+ Raised by :meth:`~spanforge.sdk.cec.SFCECClient.build_bundle` when the
573
+ ZIP assembly, HMAC signing, or evidence collection fails.
574
+
575
+ Args:
576
+ detail: Human-readable description of the failure.
577
+
578
+ Attributes:
579
+ detail: The detail message passed at construction time.
580
+ """
581
+
582
+ def __init__(self, detail: str) -> None:
583
+ self.detail = detail
584
+ super().__init__(f"CEC bundle build failed: {detail}")
585
+
586
+
587
+ class SFCECVerifyError(SFCECError):
588
+ """Bundle verification failed.
589
+
590
+ Raised by :meth:`~spanforge.sdk.cec.SFCECClient.verify_bundle` when HMAC
591
+ verification, chain proof validation, or timestamp verification fails.
592
+
593
+ Args:
594
+ detail: Human-readable description of the failure.
595
+
596
+ Attributes:
597
+ detail: The detail message passed at construction time.
598
+ """
599
+
600
+ def __init__(self, detail: str) -> None:
601
+ self.detail = detail
602
+ super().__init__(f"CEC bundle verification failed: {detail}")
603
+
604
+
605
+ class SFCECExportError(SFCECError):
606
+ """Evidence record export failed.
607
+
608
+ Raised when audit record export or DPA generation encounters an error.
609
+
610
+ Args:
611
+ detail: Human-readable description of the failure.
612
+
613
+ Attributes:
614
+ detail: The detail message passed at construction time.
615
+ """
616
+
617
+ def __init__(self, detail: str) -> None:
618
+ self.detail = detail
619
+ super().__init__(f"CEC export failed: {detail}")
620
+
621
+
622
+ # ---------------------------------------------------------------------------
623
+ # Phase 6 — Observability Named SDK errors
624
+ # ---------------------------------------------------------------------------
625
+
626
+
627
+ class SFObserveError(SFError):
628
+ """Base class for all observability service errors.
629
+
630
+ Callers can write ``except SFObserveError`` to handle any sf-observe
631
+ failure.
632
+ """
633
+
634
+
635
+ class SFObserveExportError(SFObserveError):
636
+ """Span export failed.
637
+
638
+ Raised by :meth:`~spanforge.sdk.observe.SFObserveClient.export_spans`
639
+ when the export operation encounters an unrecoverable error.
640
+
641
+ Args:
642
+ detail: Human-readable description of the failure.
643
+
644
+ Attributes:
645
+ detail: The detail message passed at construction time.
646
+ """
647
+
648
+ def __init__(self, detail: str) -> None:
649
+ self.detail = detail
650
+ super().__init__(f"Observe export failed: {detail}")
651
+
652
+
653
+ class SFObserveEmitError(SFObserveError):
654
+ """Span emit failed.
655
+
656
+ Raised by :meth:`~spanforge.sdk.observe.SFObserveClient.emit_span`
657
+ when the span cannot be created or routed to the exporter.
658
+
659
+ Args:
660
+ detail: Human-readable description of the failure.
661
+
662
+ Attributes:
663
+ detail: The detail message passed at construction time.
664
+ """
665
+
666
+ def __init__(self, detail: str) -> None:
667
+ self.detail = detail
668
+ super().__init__(f"Observe emit failed: {detail}")
669
+
670
+
671
+ class SFObserveAnnotationError(SFObserveError):
672
+ """Annotation operation failed.
673
+
674
+ Raised by :meth:`~spanforge.sdk.observe.SFObserveClient.add_annotation`
675
+ or :meth:`~spanforge.sdk.observe.SFObserveClient.get_annotations` when
676
+ the annotation store encounters an error.
677
+
678
+ Args:
679
+ detail: Human-readable description of the failure.
680
+
681
+ Attributes:
682
+ detail: The detail message passed at construction time.
683
+ """
684
+
685
+ def __init__(self, detail: str) -> None:
686
+ self.detail = detail
687
+ super().__init__(f"Observe annotation error: {detail}")
688
+
689
+
690
+ # ---------------------------------------------------------------------------
691
+ # Phase 7 — Alert Routing Service errors
692
+ # ---------------------------------------------------------------------------
693
+
694
+
695
+ class SFAlertError(SFError):
696
+ """Base class for all alert routing service errors.
697
+
698
+ Callers can write ``except SFAlertError`` to handle any sf-alert
699
+ failure.
700
+ """
701
+
702
+
703
+ class SFAlertPublishError(SFAlertError):
704
+ """Alert publish failed due to an unrecoverable sink error.
705
+
706
+ Raised by :meth:`~spanforge.sdk.alert.SFAlertClient.publish` when all
707
+ configured sinks have open circuit breakers.
708
+
709
+ Args:
710
+ topic: The topic that could not be published.
711
+ detail: Human-readable description of the failure.
712
+ """
713
+
714
+ def __init__(self, topic: str, detail: str) -> None:
715
+ self.topic = topic
716
+ self.detail = detail
717
+ super().__init__(f"Alert publish failed for topic {topic!r}: {detail}")
718
+
719
+
720
+ class SFAlertRateLimitedError(SFAlertError):
721
+ """Alert publish blocked by per-project rate limit.
722
+
723
+ Raised when a project exceeds ``max_alerts_per_minute`` (default: 60).
724
+
725
+ Args:
726
+ project_id: The rate-limited project.
727
+ limit: The configured alerts-per-minute limit.
728
+ """
729
+
730
+ def __init__(self, project_id: str, limit: int) -> None:
731
+ self.project_id = project_id
732
+ self.limit = limit
733
+ super().__init__(f"Alert rate limit of {limit}/min exceeded for project {project_id!r}")
734
+
735
+
736
+ class SFAlertQueueFullError(SFAlertError):
737
+ """Alert publish blocked because the dispatch queue is full.
738
+
739
+ Raised when the in-process async queue has reached its maximum depth
740
+ of 1 000 items and the oldest item has been dropped.
741
+
742
+ Args:
743
+ depth: Current queue depth at the time of the overflow.
744
+ """
745
+
746
+ def __init__(self, depth: int) -> None:
747
+ self.depth = depth
748
+ super().__init__(f"Alert dispatch queue full (depth={depth}); oldest item dropped")
749
+
750
+
751
+ # ---------------------------------------------------------------------------
752
+ # Phase 8 — CI/CD Gate Pipeline errors
753
+ # ---------------------------------------------------------------------------
754
+
755
+
756
+ class SFGateError(SFError):
757
+ """Base class for all CI/CD Gate Pipeline service errors.
758
+
759
+ Callers can write ``except SFGateError`` to handle any sf-gate failure.
760
+ """
761
+
762
+
763
+ class SFGateEvaluationError(SFGateError):
764
+ """A gate evaluate() call failed.
765
+
766
+ Raised by :meth:`~spanforge.sdk.gate.SFGateClient.evaluate` when
767
+ gate evaluation encounters a fatal error (e.g. invalid gate_id, executor
768
+ crash, or artifact write failure).
769
+
770
+ Args:
771
+ detail: Human-readable description of the failure.
772
+
773
+ Attributes:
774
+ detail: The detail message passed at construction time.
775
+ """
776
+
777
+ def __init__(self, detail: str) -> None:
778
+ self.detail = detail
779
+ super().__init__(f"Gate evaluation failed: {detail}")
780
+
781
+
782
+ class SFGatePipelineError(SFGateError):
783
+ """A gate pipeline run failed with one or more blocking gate failures.
784
+
785
+ Raised by :class:`~spanforge.gate.GateRunner` when the pipeline exits
786
+ with a non-zero exit code.
787
+
788
+ Args:
789
+ failed_gates: List of gate IDs that produced FAIL verdicts.
790
+ detail: Optional additional context.
791
+
792
+ Attributes:
793
+ failed_gates: Gate identifiers of the blocking failures.
794
+ """
795
+
796
+ def __init__(self, failed_gates: list[str], detail: str = "") -> None:
797
+ self.failed_gates = failed_gates
798
+ super().__init__(
799
+ f"Gate pipeline failed — blocking gates: {failed_gates}"
800
+ + (f". {detail}" if detail else "")
801
+ )
802
+
803
+
804
+ class SFGateTrustFailedError(SFGateError):
805
+ """Trust gate checks failed (GAT-021).
806
+
807
+ Raised when the trust gate fails AND the caller requests strict mode.
808
+ The standard behaviour is to return a
809
+ :class:`~spanforge.sdk._types.TrustGateResult` with ``pass_=False``
810
+ rather than raising this exception.
811
+
812
+ Args:
813
+ failures: List of human-readable failure reasons.
814
+
815
+ Attributes:
816
+ failures: The failure reasons passed at construction time.
817
+ """
818
+
819
+ def __init__(self, failures: list[str]) -> None:
820
+ self.failures = failures
821
+ super().__init__("Trust gate failed: " + "; ".join(failures))
822
+
823
+
824
+ class SFGateSchemaError(SFGateError):
825
+ """Gate YAML configuration is invalid or contains an unknown gate type.
826
+
827
+ Raised by :class:`~spanforge.gate.GateRunner` when the YAML config file
828
+ is malformed, missing required fields, or references an unknown gate type.
829
+
830
+ Args:
831
+ detail: Human-readable description of the schema violation.
832
+
833
+ Attributes:
834
+ detail: The detail message passed at construction time.
835
+ """
836
+
837
+ def __init__(self, detail: str) -> None:
838
+ self.detail = detail
839
+ super().__init__(f"Gate YAML schema error: {detail}")
840
+
841
+
842
+ # ---------------------------------------------------------------------------
843
+ # Phase 9 — Integration Config & Local Fallback errors
844
+ # ---------------------------------------------------------------------------
845
+
846
+
847
+ class SFConfigError(SFError):
848
+ """Configuration error — invalid, missing, or unparseable config file.
849
+
850
+ Raised by :func:`~spanforge.sdk.config.load_config_file` when the
851
+ ``.halluccheck.toml`` file cannot be read or parsed.
852
+
853
+ Args:
854
+ detail: Human-readable description of the problem.
855
+
856
+ Attributes:
857
+ detail: The detail message passed at construction time.
858
+ """
859
+
860
+ def __init__(self, detail: str) -> None:
861
+ self.detail = detail
862
+ super().__init__(f"SpanForge config error: {detail}")
863
+
864
+
865
+ class SFConfigValidationError(SFConfigError):
866
+ """One or more config schema validation errors were found (CFG-007).
867
+
868
+ Raised by :func:`~spanforge.sdk.config.validate_config_strict` when any
869
+ field in the ``.halluccheck.toml`` fails schema validation.
870
+
871
+ Args:
872
+ errors: List of human-readable error descriptions.
873
+
874
+ Attributes:
875
+ errors: The list of validation errors.
876
+
877
+ Example::
878
+
879
+ errors = validate_config(block)
880
+ if errors:
881
+ raise SFConfigValidationError(errors)
882
+ """
883
+
884
+ def __init__(self, errors: list[str]) -> None:
885
+ self.errors = errors
886
+ bullet_list = "\n".join(f" - {e}" for e in errors)
887
+ super().__init__(f"Config validation failed ({len(errors)} error(s)):\n{bullet_list}")
888
+
889
+
890
+ # ---------------------------------------------------------------------------
891
+ # Phase 10 — T.R.U.S.T. Scorecard & HallucCheck Contract errors
892
+ # ---------------------------------------------------------------------------
893
+
894
+
895
+ class SFTrustError(SFError):
896
+ """Base class for all T.R.U.S.T. scorecard errors.
897
+
898
+ Callers can write ``except SFTrustError`` to handle any T.R.U.S.T.
899
+ scoring failure.
900
+ """
901
+
902
+
903
+ class SFTrustComputeError(SFTrustError):
904
+ """T.R.U.S.T. scorecard computation failed.
905
+
906
+ Raised when dimension score calculation fails due to insufficient data
907
+ or an internal error.
908
+
909
+ Args:
910
+ detail: Human-readable description of the failure.
911
+
912
+ Attributes:
913
+ detail: The detail message passed at construction time.
914
+ """
915
+
916
+ def __init__(self, detail: str) -> None:
917
+ self.detail = detail
918
+ super().__init__(f"Trust scorecard compute failed: {detail}")
919
+
920
+
921
+ class SFTrustGateFailedError(SFTrustError):
922
+ """Composite trust gate evaluation failed (TRS-020).
923
+
924
+ Raised when the composite trust gate fails in strict mode.
925
+
926
+ Args:
927
+ failures: List of human-readable failure reasons.
928
+
929
+ Attributes:
930
+ failures: The failure reasons.
931
+ """
932
+
933
+ def __init__(self, failures: list[str]) -> None:
934
+ self.failures = failures
935
+ super().__init__("Composite trust gate failed: " + "; ".join(failures))
936
+
937
+
938
+ class SFPipelineError(SFTrustError):
939
+ """A pipeline integration call failed (TRS-010 through TRS-014).
940
+
941
+ Args:
942
+ pipeline: Pipeline name that failed.
943
+ detail: Human-readable description.
944
+
945
+ Attributes:
946
+ pipeline: The pipeline name.
947
+ detail: The detail message.
948
+ """
949
+
950
+ def __init__(self, pipeline: str, detail: str) -> None:
951
+ self.pipeline = pipeline
952
+ self.detail = detail
953
+ super().__init__(f"Pipeline {pipeline!r} failed: {detail}")
954
+
955
+
956
+ # ---------------------------------------------------------------------------
957
+ # Phase 11 — Enterprise Hardening & Supply Chain Security errors
958
+ # ---------------------------------------------------------------------------
959
+
960
+
961
+ class SFEnterpriseError(SFError):
962
+ """Base class for all enterprise hardening errors.
963
+
964
+ Callers can write ``except SFEnterpriseError`` to handle any enterprise
965
+ or supply-chain security failure.
966
+ """
967
+
968
+
969
+ class SFIsolationError(SFEnterpriseError):
970
+ """Cross-project data isolation violation (ENT-001 / ENT-002).
971
+
972
+ Raised when a query attempts to access data outside its project scope
973
+ without the ``cross_project_read`` permission.
974
+
975
+ Args:
976
+ project_id: The project that was accessed.
977
+ detail: Human-readable description.
978
+
979
+ Attributes:
980
+ project_id: The project referenced.
981
+ detail: The detail message.
982
+ """
983
+
984
+ def __init__(self, project_id: str, detail: str) -> None:
985
+ self.project_id = project_id
986
+ self.detail = detail
987
+ super().__init__(f"Isolation violation for project {project_id!r}: {detail}")
988
+
989
+
990
+ class SFDataResidencyError(SFEnterpriseError):
991
+ """Data residency constraint violation (ENT-004 / ENT-005).
992
+
993
+ Raised when an operation would route data outside the configured
994
+ residency region.
995
+
996
+ Args:
997
+ region: Required residency region.
998
+ attempted: The region the data would have been routed to.
999
+
1000
+ Attributes:
1001
+ region: Required residency region.
1002
+ attempted: The attempted target region.
1003
+ """
1004
+
1005
+ def __init__(self, region: str, attempted: str) -> None:
1006
+ self.region = region
1007
+ self.attempted = attempted
1008
+ super().__init__(
1009
+ f"Data residency violation: project requires {region!r} "
1010
+ f"but data would route to {attempted!r}"
1011
+ )
1012
+
1013
+
1014
+ class SFEncryptionError(SFEnterpriseError):
1015
+ """Encryption or KMS operation failed (ENT-010 through ENT-013).
1016
+
1017
+ Args:
1018
+ detail: Human-readable description of the failure.
1019
+
1020
+ Attributes:
1021
+ detail: The detail message.
1022
+ """
1023
+
1024
+ def __init__(self, detail: str) -> None:
1025
+ self.detail = detail
1026
+ super().__init__(f"Encryption error: {detail}")
1027
+
1028
+
1029
+ class SFFIPSError(SFEnterpriseError):
1030
+ """FIPS 140-2 mode violation (ENT-013).
1031
+
1032
+ Raised at startup or during operation when a non-FIPS-approved algorithm
1033
+ or cipher is detected.
1034
+
1035
+ Args:
1036
+ detail: Description of the violation.
1037
+
1038
+ Attributes:
1039
+ detail: The detail message.
1040
+ """
1041
+
1042
+ def __init__(self, detail: str) -> None:
1043
+ self.detail = detail
1044
+ super().__init__(f"FIPS 140-2 violation: {detail}")
1045
+
1046
+
1047
+ class SFAirGapError(SFEnterpriseError):
1048
+ """Air-gap or offline mode error (ENT-020 / ENT-021).
1049
+
1050
+ Raised when a network operation is attempted in offline mode.
1051
+
1052
+ Args:
1053
+ detail: Human-readable description.
1054
+
1055
+ Attributes:
1056
+ detail: The detail message.
1057
+ """
1058
+
1059
+ def __init__(self, detail: str) -> None:
1060
+ self.detail = detail
1061
+ super().__init__(f"Air-gap mode error: {detail}")
1062
+
1063
+
1064
+ class SFSecurityScanError(SFEnterpriseError):
1065
+ """Security scan (vulnerability or static analysis) failed (ENT-033 / ENT-034).
1066
+
1067
+ Args:
1068
+ detail: Human-readable description of the failure.
1069
+
1070
+ Attributes:
1071
+ detail: The detail message.
1072
+ """
1073
+
1074
+ def __init__(self, detail: str) -> None:
1075
+ self.detail = detail
1076
+ super().__init__(f"Security scan error: {detail}")
1077
+
1078
+
1079
+ class SFSecretsInLogsError(SFEnterpriseError):
1080
+ """Secrets detected in log output (ENT-035).
1081
+
1082
+ Raised when the automated secrets-in-logs audit detects API keys,
1083
+ JWTs, or HMAC secrets in logged WARNING/ERROR lines.
1084
+
1085
+ Args:
1086
+ count: Number of secrets detected.
1087
+
1088
+ Attributes:
1089
+ count: Number of secrets found.
1090
+ """
1091
+
1092
+ def __init__(self, count: int) -> None:
1093
+ self.count = count
1094
+ super().__init__(
1095
+ f"Secrets detected in log output: {count} secret(s) found. Remediate before merge."
1096
+ )