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