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.
- spanforge/__init__.py +815 -0
- spanforge/_ansi.py +93 -0
- spanforge/_batch_exporter.py +409 -0
- spanforge/_cli.py +2094 -0
- spanforge/_cli_audit.py +639 -0
- spanforge/_cli_compliance.py +711 -0
- spanforge/_cli_cost.py +243 -0
- spanforge/_cli_ops.py +791 -0
- spanforge/_cli_phase11.py +356 -0
- spanforge/_hooks.py +337 -0
- spanforge/_server.py +1708 -0
- spanforge/_span.py +1036 -0
- spanforge/_store.py +288 -0
- spanforge/_stream.py +664 -0
- spanforge/_trace.py +335 -0
- spanforge/_tracer.py +254 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +469 -0
- spanforge/auto.py +464 -0
- spanforge/baseline.py +335 -0
- spanforge/cache.py +635 -0
- spanforge/compliance.py +325 -0
- spanforge/config.py +532 -0
- spanforge/consent.py +228 -0
- spanforge/consumer.py +377 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1254 -0
- spanforge/cost.py +600 -0
- spanforge/debug.py +548 -0
- spanforge/deprecations.py +205 -0
- spanforge/drift.py +482 -0
- spanforge/egress.py +58 -0
- spanforge/eval.py +648 -0
- spanforge/event.py +1064 -0
- spanforge/exceptions.py +240 -0
- spanforge/explain.py +178 -0
- spanforge/export/__init__.py +69 -0
- spanforge/export/append_only.py +337 -0
- spanforge/export/cloud.py +357 -0
- spanforge/export/datadog.py +497 -0
- spanforge/export/grafana.py +320 -0
- spanforge/export/jsonl.py +195 -0
- spanforge/export/openinference.py +158 -0
- spanforge/export/otel_bridge.py +294 -0
- spanforge/export/otlp.py +811 -0
- spanforge/export/otlp_bridge.py +233 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/siem_schema.py +98 -0
- spanforge/export/siem_splunk.py +264 -0
- spanforge/export/siem_syslog.py +212 -0
- spanforge/export/webhook.py +299 -0
- spanforge/exporters/__init__.py +30 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/exporters/sqlite.py +142 -0
- spanforge/gate.py +1150 -0
- spanforge/governance.py +181 -0
- spanforge/hitl.py +295 -0
- spanforge/http.py +187 -0
- spanforge/inspect.py +427 -0
- spanforge/integrations/__init__.py +45 -0
- spanforge/integrations/_pricing.py +280 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/azure_openai.py +133 -0
- spanforge/integrations/bedrock.py +292 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +351 -0
- spanforge/integrations/groq.py +442 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/langgraph.py +306 -0
- spanforge/integrations/llamaindex.py +373 -0
- spanforge/integrations/ollama.py +287 -0
- spanforge/integrations/openai.py +368 -0
- spanforge/integrations/together.py +483 -0
- spanforge/io.py +214 -0
- spanforge/lint.py +322 -0
- spanforge/metrics.py +417 -0
- spanforge/metrics_export.py +343 -0
- spanforge/migrate.py +402 -0
- spanforge/model_registry.py +278 -0
- spanforge/models.py +389 -0
- spanforge/namespaces/__init__.py +254 -0
- spanforge/namespaces/audit.py +256 -0
- spanforge/namespaces/cache.py +237 -0
- spanforge/namespaces/chain.py +77 -0
- spanforge/namespaces/confidence.py +72 -0
- spanforge/namespaces/consent.py +92 -0
- spanforge/namespaces/cost.py +179 -0
- spanforge/namespaces/decision.py +143 -0
- spanforge/namespaces/diff.py +157 -0
- spanforge/namespaces/drift.py +80 -0
- spanforge/namespaces/eval_.py +251 -0
- spanforge/namespaces/feedback.py +241 -0
- spanforge/namespaces/fence.py +193 -0
- spanforge/namespaces/guard.py +105 -0
- spanforge/namespaces/hitl.py +91 -0
- spanforge/namespaces/latency.py +72 -0
- spanforge/namespaces/prompt.py +190 -0
- spanforge/namespaces/redact.py +173 -0
- spanforge/namespaces/retrieval.py +379 -0
- spanforge/namespaces/runtime_governance.py +494 -0
- spanforge/namespaces/template.py +208 -0
- spanforge/namespaces/tool_call.py +77 -0
- spanforge/namespaces/trace.py +1029 -0
- spanforge/normalizer.py +171 -0
- spanforge/plugins.py +82 -0
- spanforge/presidio_backend.py +349 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +418 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +914 -0
- spanforge/regression.py +192 -0
- spanforge/runtime_policy.py +159 -0
- spanforge/sampling.py +511 -0
- spanforge/schema.py +183 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/sdk/__init__.py +625 -0
- spanforge/sdk/_base.py +584 -0
- spanforge/sdk/_base.pyi +71 -0
- spanforge/sdk/_exceptions.py +1096 -0
- spanforge/sdk/_types.py +2184 -0
- spanforge/sdk/alert.py +1514 -0
- spanforge/sdk/alert.pyi +56 -0
- spanforge/sdk/audit.py +1196 -0
- spanforge/sdk/audit.pyi +67 -0
- spanforge/sdk/cec.py +1215 -0
- spanforge/sdk/cec.pyi +37 -0
- spanforge/sdk/config.py +641 -0
- spanforge/sdk/config.pyi +55 -0
- spanforge/sdk/enterprise.py +714 -0
- spanforge/sdk/enterprise.pyi +79 -0
- spanforge/sdk/explain.py +170 -0
- spanforge/sdk/fallback.py +432 -0
- spanforge/sdk/feedback.py +351 -0
- spanforge/sdk/gate.py +874 -0
- spanforge/sdk/gate.pyi +51 -0
- spanforge/sdk/identity.py +2114 -0
- spanforge/sdk/identity.pyi +47 -0
- spanforge/sdk/lineage.py +175 -0
- spanforge/sdk/observe.py +1065 -0
- spanforge/sdk/observe.pyi +50 -0
- spanforge/sdk/operator.py +338 -0
- spanforge/sdk/pii.py +1473 -0
- spanforge/sdk/pii.pyi +119 -0
- spanforge/sdk/pipelines.py +458 -0
- spanforge/sdk/pipelines.pyi +39 -0
- spanforge/sdk/policy.py +930 -0
- spanforge/sdk/rag.py +594 -0
- spanforge/sdk/rbac.py +280 -0
- spanforge/sdk/registry.py +430 -0
- spanforge/sdk/registry.pyi +46 -0
- spanforge/sdk/scope.py +279 -0
- spanforge/sdk/secrets.py +293 -0
- spanforge/sdk/secrets.pyi +25 -0
- spanforge/sdk/security.py +560 -0
- spanforge/sdk/security.pyi +57 -0
- spanforge/sdk/trust.py +472 -0
- spanforge/sdk/trust.pyi +41 -0
- spanforge/secrets.py +799 -0
- spanforge/signing.py +1179 -0
- spanforge/stats.py +100 -0
- spanforge/stream.py +560 -0
- spanforge/testing.py +378 -0
- spanforge/testing_mocks.py +1052 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +300 -0
- spanforge/validate.py +379 -0
- spanforge-1.0.0.dist-info/METADATA +1509 -0
- spanforge-1.0.0.dist-info/RECORD +174 -0
- spanforge-1.0.0.dist-info/WHEEL +4 -0
- spanforge-1.0.0.dist-info/entry_points.txt +5 -0
- 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
|
+
)
|