spanforge 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spanforge/__init__.py +815 -0
- spanforge/_ansi.py +93 -0
- spanforge/_batch_exporter.py +409 -0
- spanforge/_cli.py +2094 -0
- spanforge/_cli_audit.py +639 -0
- spanforge/_cli_compliance.py +711 -0
- spanforge/_cli_cost.py +243 -0
- spanforge/_cli_ops.py +791 -0
- spanforge/_cli_phase11.py +356 -0
- spanforge/_hooks.py +337 -0
- spanforge/_server.py +1708 -0
- spanforge/_span.py +1036 -0
- spanforge/_store.py +288 -0
- spanforge/_stream.py +664 -0
- spanforge/_trace.py +335 -0
- spanforge/_tracer.py +254 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +469 -0
- spanforge/auto.py +464 -0
- spanforge/baseline.py +335 -0
- spanforge/cache.py +635 -0
- spanforge/compliance.py +325 -0
- spanforge/config.py +532 -0
- spanforge/consent.py +228 -0
- spanforge/consumer.py +377 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1254 -0
- spanforge/cost.py +600 -0
- spanforge/debug.py +548 -0
- spanforge/deprecations.py +205 -0
- spanforge/drift.py +482 -0
- spanforge/egress.py +58 -0
- spanforge/eval.py +648 -0
- spanforge/event.py +1064 -0
- spanforge/exceptions.py +240 -0
- spanforge/explain.py +178 -0
- spanforge/export/__init__.py +69 -0
- spanforge/export/append_only.py +337 -0
- spanforge/export/cloud.py +357 -0
- spanforge/export/datadog.py +497 -0
- spanforge/export/grafana.py +320 -0
- spanforge/export/jsonl.py +195 -0
- spanforge/export/openinference.py +158 -0
- spanforge/export/otel_bridge.py +294 -0
- spanforge/export/otlp.py +811 -0
- spanforge/export/otlp_bridge.py +233 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/siem_schema.py +98 -0
- spanforge/export/siem_splunk.py +264 -0
- spanforge/export/siem_syslog.py +212 -0
- spanforge/export/webhook.py +299 -0
- spanforge/exporters/__init__.py +30 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/exporters/sqlite.py +142 -0
- spanforge/gate.py +1150 -0
- spanforge/governance.py +181 -0
- spanforge/hitl.py +295 -0
- spanforge/http.py +187 -0
- spanforge/inspect.py +427 -0
- spanforge/integrations/__init__.py +45 -0
- spanforge/integrations/_pricing.py +280 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/azure_openai.py +133 -0
- spanforge/integrations/bedrock.py +292 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +351 -0
- spanforge/integrations/groq.py +442 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/langgraph.py +306 -0
- spanforge/integrations/llamaindex.py +373 -0
- spanforge/integrations/ollama.py +287 -0
- spanforge/integrations/openai.py +368 -0
- spanforge/integrations/together.py +483 -0
- spanforge/io.py +214 -0
- spanforge/lint.py +322 -0
- spanforge/metrics.py +417 -0
- spanforge/metrics_export.py +343 -0
- spanforge/migrate.py +402 -0
- spanforge/model_registry.py +278 -0
- spanforge/models.py +389 -0
- spanforge/namespaces/__init__.py +254 -0
- spanforge/namespaces/audit.py +256 -0
- spanforge/namespaces/cache.py +237 -0
- spanforge/namespaces/chain.py +77 -0
- spanforge/namespaces/confidence.py +72 -0
- spanforge/namespaces/consent.py +92 -0
- spanforge/namespaces/cost.py +179 -0
- spanforge/namespaces/decision.py +143 -0
- spanforge/namespaces/diff.py +157 -0
- spanforge/namespaces/drift.py +80 -0
- spanforge/namespaces/eval_.py +251 -0
- spanforge/namespaces/feedback.py +241 -0
- spanforge/namespaces/fence.py +193 -0
- spanforge/namespaces/guard.py +105 -0
- spanforge/namespaces/hitl.py +91 -0
- spanforge/namespaces/latency.py +72 -0
- spanforge/namespaces/prompt.py +190 -0
- spanforge/namespaces/redact.py +173 -0
- spanforge/namespaces/retrieval.py +379 -0
- spanforge/namespaces/runtime_governance.py +494 -0
- spanforge/namespaces/template.py +208 -0
- spanforge/namespaces/tool_call.py +77 -0
- spanforge/namespaces/trace.py +1029 -0
- spanforge/normalizer.py +171 -0
- spanforge/plugins.py +82 -0
- spanforge/presidio_backend.py +349 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +418 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +914 -0
- spanforge/regression.py +192 -0
- spanforge/runtime_policy.py +159 -0
- spanforge/sampling.py +511 -0
- spanforge/schema.py +183 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/sdk/__init__.py +625 -0
- spanforge/sdk/_base.py +584 -0
- spanforge/sdk/_base.pyi +71 -0
- spanforge/sdk/_exceptions.py +1096 -0
- spanforge/sdk/_types.py +2184 -0
- spanforge/sdk/alert.py +1514 -0
- spanforge/sdk/alert.pyi +56 -0
- spanforge/sdk/audit.py +1196 -0
- spanforge/sdk/audit.pyi +67 -0
- spanforge/sdk/cec.py +1215 -0
- spanforge/sdk/cec.pyi +37 -0
- spanforge/sdk/config.py +641 -0
- spanforge/sdk/config.pyi +55 -0
- spanforge/sdk/enterprise.py +714 -0
- spanforge/sdk/enterprise.pyi +79 -0
- spanforge/sdk/explain.py +170 -0
- spanforge/sdk/fallback.py +432 -0
- spanforge/sdk/feedback.py +351 -0
- spanforge/sdk/gate.py +874 -0
- spanforge/sdk/gate.pyi +51 -0
- spanforge/sdk/identity.py +2114 -0
- spanforge/sdk/identity.pyi +47 -0
- spanforge/sdk/lineage.py +175 -0
- spanforge/sdk/observe.py +1065 -0
- spanforge/sdk/observe.pyi +50 -0
- spanforge/sdk/operator.py +338 -0
- spanforge/sdk/pii.py +1473 -0
- spanforge/sdk/pii.pyi +119 -0
- spanforge/sdk/pipelines.py +458 -0
- spanforge/sdk/pipelines.pyi +39 -0
- spanforge/sdk/policy.py +930 -0
- spanforge/sdk/rag.py +594 -0
- spanforge/sdk/rbac.py +280 -0
- spanforge/sdk/registry.py +430 -0
- spanforge/sdk/registry.pyi +46 -0
- spanforge/sdk/scope.py +279 -0
- spanforge/sdk/secrets.py +293 -0
- spanforge/sdk/secrets.pyi +25 -0
- spanforge/sdk/security.py +560 -0
- spanforge/sdk/security.pyi +57 -0
- spanforge/sdk/trust.py +472 -0
- spanforge/sdk/trust.pyi +41 -0
- spanforge/secrets.py +799 -0
- spanforge/signing.py +1179 -0
- spanforge/stats.py +100 -0
- spanforge/stream.py +560 -0
- spanforge/testing.py +378 -0
- spanforge/testing_mocks.py +1052 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +300 -0
- spanforge/validate.py +379 -0
- spanforge-1.0.0.dist-info/METADATA +1509 -0
- spanforge-1.0.0.dist-info/RECORD +174 -0
- spanforge-1.0.0.dist-info/WHEEL +4 -0
- spanforge-1.0.0.dist-info/entry_points.txt +5 -0
- spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""spanforge.namespaces.runtime_governance - GA runtime governance payloads.
|
|
2
|
+
|
|
3
|
+
These payloads freeze the canonical event contracts for the May 2, 2026 GA
|
|
4
|
+
runtime-governance feature set:
|
|
5
|
+
|
|
6
|
+
- explanation
|
|
7
|
+
- grounding
|
|
8
|
+
- lineage
|
|
9
|
+
- scope
|
|
10
|
+
- rbac
|
|
11
|
+
|
|
12
|
+
They intentionally model runtime control decisions rather than generic
|
|
13
|
+
observability spans so Phase 1 service clients can build on stable contracts.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ExplanationFactor",
|
|
23
|
+
"ExplanationPayload",
|
|
24
|
+
"GroundingClaim",
|
|
25
|
+
"GroundingPayload",
|
|
26
|
+
"LineagePayload",
|
|
27
|
+
"RBACDecisionPayload",
|
|
28
|
+
"ScopeDecisionPayload",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
_VALID_POLICY_ACTIONS = frozenset({"allow", "allow+log", "redact", "block", "human_review"})
|
|
32
|
+
_VALID_DECISION_ACTIONS = frozenset({"allow", "block", "escalate", "human_review", "redact"})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ExplanationFactor:
|
|
37
|
+
"""One contributing factor for a runtime explanation record."""
|
|
38
|
+
|
|
39
|
+
factor_name: str
|
|
40
|
+
weight: float
|
|
41
|
+
contribution: float
|
|
42
|
+
evidence: str
|
|
43
|
+
confidence: float | None = None
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
if not self.factor_name:
|
|
47
|
+
raise ValueError("ExplanationFactor.factor_name must be non-empty")
|
|
48
|
+
if not (0.0 <= self.weight <= 1.0):
|
|
49
|
+
raise ValueError("ExplanationFactor.weight must be in [0.0, 1.0]")
|
|
50
|
+
if not (-1.0 <= self.contribution <= 1.0):
|
|
51
|
+
raise ValueError("ExplanationFactor.contribution must be in [-1.0, 1.0]")
|
|
52
|
+
if not self.evidence:
|
|
53
|
+
raise ValueError("ExplanationFactor.evidence must be non-empty")
|
|
54
|
+
if self.confidence is not None and not (0.0 <= self.confidence <= 1.0):
|
|
55
|
+
raise ValueError("ExplanationFactor.confidence must be in [0.0, 1.0]")
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict[str, Any]:
|
|
58
|
+
data: dict[str, Any] = {
|
|
59
|
+
"factor_name": self.factor_name,
|
|
60
|
+
"weight": self.weight,
|
|
61
|
+
"contribution": self.contribution,
|
|
62
|
+
"evidence": self.evidence,
|
|
63
|
+
}
|
|
64
|
+
if self.confidence is not None:
|
|
65
|
+
data["confidence"] = self.confidence
|
|
66
|
+
return data
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_dict(cls, data: dict[str, Any]) -> ExplanationFactor:
|
|
70
|
+
return cls(
|
|
71
|
+
factor_name=data["factor_name"],
|
|
72
|
+
weight=float(data["weight"]),
|
|
73
|
+
contribution=float(data["contribution"]),
|
|
74
|
+
evidence=data["evidence"],
|
|
75
|
+
confidence=float(data["confidence"]) if "confidence" in data else None,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class ExplanationPayload:
|
|
81
|
+
"""Canonical explanation event payload for runtime decisions."""
|
|
82
|
+
|
|
83
|
+
explanation_id: str
|
|
84
|
+
trace_id: str
|
|
85
|
+
decision_id: str
|
|
86
|
+
agent_id: str
|
|
87
|
+
summary: str
|
|
88
|
+
policy_action: str
|
|
89
|
+
generated_at: str
|
|
90
|
+
factors: list[ExplanationFactor] = field(default_factory=list)
|
|
91
|
+
model_id: str | None = None
|
|
92
|
+
confidence: float | None = None
|
|
93
|
+
policy_id: str | None = None
|
|
94
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
95
|
+
|
|
96
|
+
def __post_init__(self) -> None:
|
|
97
|
+
if not self.explanation_id:
|
|
98
|
+
raise ValueError("ExplanationPayload.explanation_id must be non-empty")
|
|
99
|
+
if not self.trace_id:
|
|
100
|
+
raise ValueError("ExplanationPayload.trace_id must be non-empty")
|
|
101
|
+
if not self.decision_id:
|
|
102
|
+
raise ValueError("ExplanationPayload.decision_id must be non-empty")
|
|
103
|
+
if not self.agent_id:
|
|
104
|
+
raise ValueError("ExplanationPayload.agent_id must be non-empty")
|
|
105
|
+
if not self.summary:
|
|
106
|
+
raise ValueError("ExplanationPayload.summary must be non-empty")
|
|
107
|
+
if self.policy_action not in _VALID_POLICY_ACTIONS:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"ExplanationPayload.policy_action must be one of {sorted(_VALID_POLICY_ACTIONS)}"
|
|
110
|
+
)
|
|
111
|
+
if not self.generated_at:
|
|
112
|
+
raise ValueError("ExplanationPayload.generated_at must be non-empty")
|
|
113
|
+
if self.confidence is not None and not (0.0 <= self.confidence <= 1.0):
|
|
114
|
+
raise ValueError("ExplanationPayload.confidence must be in [0.0, 1.0]")
|
|
115
|
+
|
|
116
|
+
def to_dict(self) -> dict[str, Any]:
|
|
117
|
+
data: dict[str, Any] = {
|
|
118
|
+
"explanation_id": self.explanation_id,
|
|
119
|
+
"trace_id": self.trace_id,
|
|
120
|
+
"decision_id": self.decision_id,
|
|
121
|
+
"agent_id": self.agent_id,
|
|
122
|
+
"summary": self.summary,
|
|
123
|
+
"policy_action": self.policy_action,
|
|
124
|
+
"generated_at": self.generated_at,
|
|
125
|
+
"factors": [factor.to_dict() for factor in self.factors],
|
|
126
|
+
}
|
|
127
|
+
if self.model_id is not None:
|
|
128
|
+
data["model_id"] = self.model_id
|
|
129
|
+
if self.confidence is not None:
|
|
130
|
+
data["confidence"] = self.confidence
|
|
131
|
+
if self.policy_id is not None:
|
|
132
|
+
data["policy_id"] = self.policy_id
|
|
133
|
+
if self.metadata:
|
|
134
|
+
data["metadata"] = self.metadata
|
|
135
|
+
return data
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_dict(cls, data: dict[str, Any]) -> ExplanationPayload:
|
|
139
|
+
return cls(
|
|
140
|
+
explanation_id=data["explanation_id"],
|
|
141
|
+
trace_id=data["trace_id"],
|
|
142
|
+
decision_id=data["decision_id"],
|
|
143
|
+
agent_id=data["agent_id"],
|
|
144
|
+
summary=data["summary"],
|
|
145
|
+
policy_action=data["policy_action"],
|
|
146
|
+
generated_at=data["generated_at"],
|
|
147
|
+
factors=[ExplanationFactor.from_dict(item) for item in data.get("factors", [])],
|
|
148
|
+
model_id=data.get("model_id"),
|
|
149
|
+
confidence=float(data["confidence"]) if "confidence" in data else None,
|
|
150
|
+
policy_id=data.get("policy_id"),
|
|
151
|
+
metadata=dict(data.get("metadata", {})),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class GroundingClaim:
|
|
157
|
+
"""One claim-level grounding assessment."""
|
|
158
|
+
|
|
159
|
+
claim_id: str
|
|
160
|
+
claim_text: str
|
|
161
|
+
grounded: bool
|
|
162
|
+
score: float
|
|
163
|
+
source_ids: list[str] = field(default_factory=list)
|
|
164
|
+
|
|
165
|
+
def __post_init__(self) -> None:
|
|
166
|
+
if not self.claim_id:
|
|
167
|
+
raise ValueError("GroundingClaim.claim_id must be non-empty")
|
|
168
|
+
if not self.claim_text:
|
|
169
|
+
raise ValueError("GroundingClaim.claim_text must be non-empty")
|
|
170
|
+
if not (0.0 <= self.score <= 1.0):
|
|
171
|
+
raise ValueError("GroundingClaim.score must be in [0.0, 1.0]")
|
|
172
|
+
|
|
173
|
+
def to_dict(self) -> dict[str, Any]:
|
|
174
|
+
return {
|
|
175
|
+
"claim_id": self.claim_id,
|
|
176
|
+
"claim_text": self.claim_text,
|
|
177
|
+
"grounded": self.grounded,
|
|
178
|
+
"score": self.score,
|
|
179
|
+
"source_ids": list(self.source_ids),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def from_dict(cls, data: dict[str, Any]) -> GroundingClaim:
|
|
184
|
+
return cls(
|
|
185
|
+
claim_id=data["claim_id"],
|
|
186
|
+
claim_text=data["claim_text"],
|
|
187
|
+
grounded=bool(data["grounded"]),
|
|
188
|
+
score=float(data["score"]),
|
|
189
|
+
source_ids=list(data.get("source_ids", [])),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class GroundingPayload:
|
|
195
|
+
"""Canonical grounding event payload for RAG reliability controls."""
|
|
196
|
+
|
|
197
|
+
grounding_id: str
|
|
198
|
+
trace_id: str
|
|
199
|
+
decision_id: str
|
|
200
|
+
session_id: str
|
|
201
|
+
status: str
|
|
202
|
+
average_score: float
|
|
203
|
+
threshold: float
|
|
204
|
+
policy_action: str
|
|
205
|
+
assessed_at: str
|
|
206
|
+
claims: list[GroundingClaim] = field(default_factory=list)
|
|
207
|
+
model_id: str | None = None
|
|
208
|
+
retriever_name: str | None = None
|
|
209
|
+
|
|
210
|
+
def __post_init__(self) -> None:
|
|
211
|
+
if not self.grounding_id:
|
|
212
|
+
raise ValueError("GroundingPayload.grounding_id must be non-empty")
|
|
213
|
+
if not self.trace_id:
|
|
214
|
+
raise ValueError("GroundingPayload.trace_id must be non-empty")
|
|
215
|
+
if not self.decision_id:
|
|
216
|
+
raise ValueError("GroundingPayload.decision_id must be non-empty")
|
|
217
|
+
if not self.session_id:
|
|
218
|
+
raise ValueError("GroundingPayload.session_id must be non-empty")
|
|
219
|
+
if self.status not in {"grounded", "partially_grounded", "ungrounded"}:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
"GroundingPayload.status must be 'grounded', 'partially_grounded', or 'ungrounded'"
|
|
222
|
+
)
|
|
223
|
+
if not (0.0 <= self.average_score <= 1.0):
|
|
224
|
+
raise ValueError("GroundingPayload.average_score must be in [0.0, 1.0]")
|
|
225
|
+
if not (0.0 <= self.threshold <= 1.0):
|
|
226
|
+
raise ValueError("GroundingPayload.threshold must be in [0.0, 1.0]")
|
|
227
|
+
if self.policy_action not in _VALID_POLICY_ACTIONS:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"GroundingPayload.policy_action must be one of {sorted(_VALID_POLICY_ACTIONS)}"
|
|
230
|
+
)
|
|
231
|
+
if not self.assessed_at:
|
|
232
|
+
raise ValueError("GroundingPayload.assessed_at must be non-empty")
|
|
233
|
+
|
|
234
|
+
def to_dict(self) -> dict[str, Any]:
|
|
235
|
+
data: dict[str, Any] = {
|
|
236
|
+
"grounding_id": self.grounding_id,
|
|
237
|
+
"trace_id": self.trace_id,
|
|
238
|
+
"decision_id": self.decision_id,
|
|
239
|
+
"session_id": self.session_id,
|
|
240
|
+
"status": self.status,
|
|
241
|
+
"average_score": self.average_score,
|
|
242
|
+
"threshold": self.threshold,
|
|
243
|
+
"policy_action": self.policy_action,
|
|
244
|
+
"assessed_at": self.assessed_at,
|
|
245
|
+
"claims": [claim.to_dict() for claim in self.claims],
|
|
246
|
+
}
|
|
247
|
+
if self.model_id is not None:
|
|
248
|
+
data["model_id"] = self.model_id
|
|
249
|
+
if self.retriever_name is not None:
|
|
250
|
+
data["retriever_name"] = self.retriever_name
|
|
251
|
+
return data
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def from_dict(cls, data: dict[str, Any]) -> GroundingPayload:
|
|
255
|
+
return cls(
|
|
256
|
+
grounding_id=data["grounding_id"],
|
|
257
|
+
trace_id=data["trace_id"],
|
|
258
|
+
decision_id=data["decision_id"],
|
|
259
|
+
session_id=data["session_id"],
|
|
260
|
+
status=data["status"],
|
|
261
|
+
average_score=float(data["average_score"]),
|
|
262
|
+
threshold=float(data["threshold"]),
|
|
263
|
+
policy_action=data["policy_action"],
|
|
264
|
+
assessed_at=data["assessed_at"],
|
|
265
|
+
claims=[GroundingClaim.from_dict(item) for item in data.get("claims", [])],
|
|
266
|
+
model_id=data.get("model_id"),
|
|
267
|
+
retriever_name=data.get("retriever_name"),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass
|
|
272
|
+
class LineagePayload:
|
|
273
|
+
"""Canonical provenance payload for data and decision lineage."""
|
|
274
|
+
|
|
275
|
+
lineage_id: str
|
|
276
|
+
trace_id: str
|
|
277
|
+
decision_id: str
|
|
278
|
+
subject_type: str
|
|
279
|
+
subject_id: str
|
|
280
|
+
operation: str
|
|
281
|
+
recorded_at: str
|
|
282
|
+
input_refs: list[str] = field(default_factory=list)
|
|
283
|
+
output_refs: list[str] = field(default_factory=list)
|
|
284
|
+
parent_lineage_ids: list[str] = field(default_factory=list)
|
|
285
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
286
|
+
|
|
287
|
+
def __post_init__(self) -> None:
|
|
288
|
+
if not self.lineage_id:
|
|
289
|
+
raise ValueError("LineagePayload.lineage_id must be non-empty")
|
|
290
|
+
if not self.trace_id:
|
|
291
|
+
raise ValueError("LineagePayload.trace_id must be non-empty")
|
|
292
|
+
if not self.decision_id:
|
|
293
|
+
raise ValueError("LineagePayload.decision_id must be non-empty")
|
|
294
|
+
if not self.subject_type:
|
|
295
|
+
raise ValueError("LineagePayload.subject_type must be non-empty")
|
|
296
|
+
if not self.subject_id:
|
|
297
|
+
raise ValueError("LineagePayload.subject_id must be non-empty")
|
|
298
|
+
if not self.operation:
|
|
299
|
+
raise ValueError("LineagePayload.operation must be non-empty")
|
|
300
|
+
if not self.recorded_at:
|
|
301
|
+
raise ValueError("LineagePayload.recorded_at must be non-empty")
|
|
302
|
+
|
|
303
|
+
def to_dict(self) -> dict[str, Any]:
|
|
304
|
+
data: dict[str, Any] = {
|
|
305
|
+
"lineage_id": self.lineage_id,
|
|
306
|
+
"trace_id": self.trace_id,
|
|
307
|
+
"decision_id": self.decision_id,
|
|
308
|
+
"subject_type": self.subject_type,
|
|
309
|
+
"subject_id": self.subject_id,
|
|
310
|
+
"operation": self.operation,
|
|
311
|
+
"recorded_at": self.recorded_at,
|
|
312
|
+
"input_refs": list(self.input_refs),
|
|
313
|
+
"output_refs": list(self.output_refs),
|
|
314
|
+
"parent_lineage_ids": list(self.parent_lineage_ids),
|
|
315
|
+
}
|
|
316
|
+
if self.metadata:
|
|
317
|
+
data["metadata"] = self.metadata
|
|
318
|
+
return data
|
|
319
|
+
|
|
320
|
+
@classmethod
|
|
321
|
+
def from_dict(cls, data: dict[str, Any]) -> LineagePayload:
|
|
322
|
+
return cls(
|
|
323
|
+
lineage_id=data["lineage_id"],
|
|
324
|
+
trace_id=data["trace_id"],
|
|
325
|
+
decision_id=data["decision_id"],
|
|
326
|
+
subject_type=data["subject_type"],
|
|
327
|
+
subject_id=data["subject_id"],
|
|
328
|
+
operation=data["operation"],
|
|
329
|
+
recorded_at=data["recorded_at"],
|
|
330
|
+
input_refs=list(data.get("input_refs", [])),
|
|
331
|
+
output_refs=list(data.get("output_refs", [])),
|
|
332
|
+
parent_lineage_ids=list(data.get("parent_lineage_ids", [])),
|
|
333
|
+
metadata=dict(data.get("metadata", {})),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@dataclass
|
|
338
|
+
class ScopeDecisionPayload:
|
|
339
|
+
"""Canonical runtime payload for agent scope checks."""
|
|
340
|
+
|
|
341
|
+
scope_id: str
|
|
342
|
+
trace_id: str
|
|
343
|
+
agent_id: str
|
|
344
|
+
resource: str
|
|
345
|
+
action_name: str
|
|
346
|
+
allowed: bool
|
|
347
|
+
outcome: str
|
|
348
|
+
reason: str
|
|
349
|
+
checked_at: str
|
|
350
|
+
capability: str | None = None
|
|
351
|
+
policy_id: str | None = None
|
|
352
|
+
policy_action: str | None = None
|
|
353
|
+
|
|
354
|
+
def __post_init__(self) -> None:
|
|
355
|
+
if not self.scope_id:
|
|
356
|
+
raise ValueError("ScopeDecisionPayload.scope_id must be non-empty")
|
|
357
|
+
if not self.trace_id:
|
|
358
|
+
raise ValueError("ScopeDecisionPayload.trace_id must be non-empty")
|
|
359
|
+
if not self.agent_id:
|
|
360
|
+
raise ValueError("ScopeDecisionPayload.agent_id must be non-empty")
|
|
361
|
+
if not self.resource:
|
|
362
|
+
raise ValueError("ScopeDecisionPayload.resource must be non-empty")
|
|
363
|
+
if not self.action_name:
|
|
364
|
+
raise ValueError("ScopeDecisionPayload.action_name must be non-empty")
|
|
365
|
+
if self.outcome not in _VALID_DECISION_ACTIONS:
|
|
366
|
+
raise ValueError(
|
|
367
|
+
f"ScopeDecisionPayload.outcome must be one of {sorted(_VALID_DECISION_ACTIONS)}"
|
|
368
|
+
)
|
|
369
|
+
if not self.reason:
|
|
370
|
+
raise ValueError("ScopeDecisionPayload.reason must be non-empty")
|
|
371
|
+
if not self.checked_at:
|
|
372
|
+
raise ValueError("ScopeDecisionPayload.checked_at must be non-empty")
|
|
373
|
+
if self.policy_action is not None and self.policy_action not in _VALID_POLICY_ACTIONS:
|
|
374
|
+
raise ValueError(
|
|
375
|
+
f"ScopeDecisionPayload.policy_action must be one of {sorted(_VALID_POLICY_ACTIONS)}"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def to_dict(self) -> dict[str, Any]:
|
|
379
|
+
data: dict[str, Any] = {
|
|
380
|
+
"scope_id": self.scope_id,
|
|
381
|
+
"trace_id": self.trace_id,
|
|
382
|
+
"agent_id": self.agent_id,
|
|
383
|
+
"resource": self.resource,
|
|
384
|
+
"action_name": self.action_name,
|
|
385
|
+
"allowed": self.allowed,
|
|
386
|
+
"outcome": self.outcome,
|
|
387
|
+
"reason": self.reason,
|
|
388
|
+
"checked_at": self.checked_at,
|
|
389
|
+
}
|
|
390
|
+
if self.capability is not None:
|
|
391
|
+
data["capability"] = self.capability
|
|
392
|
+
if self.policy_id is not None:
|
|
393
|
+
data["policy_id"] = self.policy_id
|
|
394
|
+
if self.policy_action is not None:
|
|
395
|
+
data["policy_action"] = self.policy_action
|
|
396
|
+
return data
|
|
397
|
+
|
|
398
|
+
@classmethod
|
|
399
|
+
def from_dict(cls, data: dict[str, Any]) -> ScopeDecisionPayload:
|
|
400
|
+
return cls(
|
|
401
|
+
scope_id=data["scope_id"],
|
|
402
|
+
trace_id=data["trace_id"],
|
|
403
|
+
agent_id=data["agent_id"],
|
|
404
|
+
resource=data["resource"],
|
|
405
|
+
action_name=data["action_name"],
|
|
406
|
+
allowed=bool(data["allowed"]),
|
|
407
|
+
outcome=data["outcome"],
|
|
408
|
+
reason=data["reason"],
|
|
409
|
+
checked_at=data["checked_at"],
|
|
410
|
+
capability=data.get("capability"),
|
|
411
|
+
policy_id=data.get("policy_id"),
|
|
412
|
+
policy_action=data.get("policy_action"),
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@dataclass
|
|
417
|
+
class RBACDecisionPayload:
|
|
418
|
+
"""Canonical runtime payload for RBAC authorization checks."""
|
|
419
|
+
|
|
420
|
+
check_id: str
|
|
421
|
+
trace_id: str
|
|
422
|
+
actor_id: str
|
|
423
|
+
resource: str
|
|
424
|
+
action_name: str
|
|
425
|
+
allowed: bool
|
|
426
|
+
outcome: str
|
|
427
|
+
reason: str
|
|
428
|
+
checked_at: str
|
|
429
|
+
required_roles: list[str] = field(default_factory=list)
|
|
430
|
+
effective_roles: list[str] = field(default_factory=list)
|
|
431
|
+
policy_id: str | None = None
|
|
432
|
+
policy_action: str | None = None
|
|
433
|
+
|
|
434
|
+
def __post_init__(self) -> None:
|
|
435
|
+
if not self.check_id:
|
|
436
|
+
raise ValueError("RBACDecisionPayload.check_id must be non-empty")
|
|
437
|
+
if not self.trace_id:
|
|
438
|
+
raise ValueError("RBACDecisionPayload.trace_id must be non-empty")
|
|
439
|
+
if not self.actor_id:
|
|
440
|
+
raise ValueError("RBACDecisionPayload.actor_id must be non-empty")
|
|
441
|
+
if not self.resource:
|
|
442
|
+
raise ValueError("RBACDecisionPayload.resource must be non-empty")
|
|
443
|
+
if not self.action_name:
|
|
444
|
+
raise ValueError("RBACDecisionPayload.action_name must be non-empty")
|
|
445
|
+
if self.outcome not in _VALID_DECISION_ACTIONS:
|
|
446
|
+
raise ValueError(
|
|
447
|
+
f"RBACDecisionPayload.outcome must be one of {sorted(_VALID_DECISION_ACTIONS)}"
|
|
448
|
+
)
|
|
449
|
+
if not self.reason:
|
|
450
|
+
raise ValueError("RBACDecisionPayload.reason must be non-empty")
|
|
451
|
+
if not self.checked_at:
|
|
452
|
+
raise ValueError("RBACDecisionPayload.checked_at must be non-empty")
|
|
453
|
+
if self.policy_action is not None and self.policy_action not in _VALID_POLICY_ACTIONS:
|
|
454
|
+
raise ValueError(
|
|
455
|
+
f"RBACDecisionPayload.policy_action must be one of {sorted(_VALID_POLICY_ACTIONS)}"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def to_dict(self) -> dict[str, Any]:
|
|
459
|
+
data: dict[str, Any] = {
|
|
460
|
+
"check_id": self.check_id,
|
|
461
|
+
"trace_id": self.trace_id,
|
|
462
|
+
"actor_id": self.actor_id,
|
|
463
|
+
"resource": self.resource,
|
|
464
|
+
"action_name": self.action_name,
|
|
465
|
+
"allowed": self.allowed,
|
|
466
|
+
"outcome": self.outcome,
|
|
467
|
+
"reason": self.reason,
|
|
468
|
+
"checked_at": self.checked_at,
|
|
469
|
+
"required_roles": list(self.required_roles),
|
|
470
|
+
"effective_roles": list(self.effective_roles),
|
|
471
|
+
}
|
|
472
|
+
if self.policy_id is not None:
|
|
473
|
+
data["policy_id"] = self.policy_id
|
|
474
|
+
if self.policy_action is not None:
|
|
475
|
+
data["policy_action"] = self.policy_action
|
|
476
|
+
return data
|
|
477
|
+
|
|
478
|
+
@classmethod
|
|
479
|
+
def from_dict(cls, data: dict[str, Any]) -> RBACDecisionPayload:
|
|
480
|
+
return cls(
|
|
481
|
+
check_id=data["check_id"],
|
|
482
|
+
trace_id=data["trace_id"],
|
|
483
|
+
actor_id=data["actor_id"],
|
|
484
|
+
resource=data["resource"],
|
|
485
|
+
action_name=data["action_name"],
|
|
486
|
+
allowed=bool(data["allowed"]),
|
|
487
|
+
outcome=data["outcome"],
|
|
488
|
+
reason=data["reason"],
|
|
489
|
+
checked_at=data["checked_at"],
|
|
490
|
+
required_roles=list(data.get("required_roles", [])),
|
|
491
|
+
effective_roles=list(data.get("effective_roles", [])),
|
|
492
|
+
policy_id=data.get("policy_id"),
|
|
493
|
+
policy_action=data.get("policy_action"),
|
|
494
|
+
)
|