sdk(P2.2): typed compliance events + attest/session + Article 12 report
Typed events as SDK-side conventions (no backend change): ModelDecision / HumanOverride / IncidentReport (NIS2 field names) / DataAccess as Python dataclasses, TypeScript builders, and Go structs — each serializing to a plain payload with regulation_refs (EU AI Act Art.12/14, NIS2 Art.23, AI-Act Art.62, GDPR Art.30/6) and self-validating against the committed-payload number policy. Python ergonomics: @attest(client, stream_id=...) wraps any function — one event per call with commitments over args/kwargs and result (raw values never leave the process), .last_receipt on the wrapper, exceptions log an IncidentReport-shaped event (commitment over the traceback) and re-raise; logging failures never break the workload (log-and-continue; strict=True is the only raising mode — all test-enforced). session(...) groups typed events under shared session_id/actor_ref metadata. Evidence report: attesto.reports.article12(...) in Python and `attesto report article12 --stream ... --output report.md` in the Go CLI — deterministic templating (never LLM-generated) built only from existing tenant endpoints: Art.12(2) coverage table, per-type event counts, P1.3 completeness verdict, checkpoint -> anchor-tx -> block path, and replayable verification commands. Claims discipline test-enforced in both languages: the words "compliant"/"compliance guaranteed" never appear — the report states evidence recorded and independently verifiable. The mock emulators now expose event_type in tenant listings so report tests run end-to-end against P2.3. Sweep green: Python 94, TS 59, Go all packages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
135
events.go
Normal file
135
events.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package attesto
|
||||
|
||||
// [P2.2] Typed compliance events — SDK-side conventions, no backend change.
|
||||
// Each ToPayload() returns a plain payload map with regulation_refs included,
|
||||
// validated against the committed-payload number policy. Recording these never
|
||||
// claims conformity: Attesto attests records.
|
||||
|
||||
// Typed event type identifiers.
|
||||
const (
|
||||
EventTypeModelDecision = "attesto.model_decision"
|
||||
EventTypeHumanOverride = "attesto.human_override"
|
||||
EventTypeIncidentReport = "attesto.incident_report"
|
||||
EventTypeDataAccess = "attesto.data_access"
|
||||
)
|
||||
|
||||
// Regulation references per typed event (conventions for reports/auditors).
|
||||
var (
|
||||
RefsModelDecision = []string{"EU-AI-Act:Art.12", "EU-AI-Act:Art.14"}
|
||||
RefsHumanOverride = []string{"EU-AI-Act:Art.14"}
|
||||
RefsIncidentReport = []string{"NIS2:Art.23", "EU-AI-Act:Art.62"}
|
||||
RefsDataAccess = []string{"GDPR:Art.30", "GDPR:Art.6"}
|
||||
)
|
||||
|
||||
func finishPayload(payload M, refs []string, extra M) (M, error) {
|
||||
for key, value := range extra {
|
||||
payload[key] = value
|
||||
}
|
||||
out := M{}
|
||||
for key, value := range payload {
|
||||
if value != nil && value != "" {
|
||||
out[key] = value
|
||||
}
|
||||
}
|
||||
out["regulation_refs"] = refs
|
||||
if err := AssertCommitmentSafeNumbers(map[string]any(out), "$"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ModelDecision is one model-driven decision (commitments only; raw inputs and
|
||||
// outputs never leave your process).
|
||||
type ModelDecision struct {
|
||||
Model string
|
||||
InputCommitment map[string]string
|
||||
OutputCommitment map[string]string
|
||||
Decision string
|
||||
ConfidenceBp int // basis points keep integers commitment-safe
|
||||
HumanInLoop bool
|
||||
OperatorRef string
|
||||
Extra M
|
||||
}
|
||||
|
||||
func (e ModelDecision) EventType() string { return EventTypeModelDecision }
|
||||
|
||||
func (e ModelDecision) ToPayload() (M, error) {
|
||||
return finishPayload(M{
|
||||
"model": e.Model,
|
||||
"input_commitment": orNil(e.InputCommitment),
|
||||
"output_commitment": orNil(e.OutputCommitment),
|
||||
"decision": e.Decision,
|
||||
"confidence_bp": e.ConfidenceBp,
|
||||
"human_in_loop": e.HumanInLoop,
|
||||
"operator_ref": e.OperatorRef,
|
||||
}, RefsModelDecision, e.Extra)
|
||||
}
|
||||
|
||||
// HumanOverride records a human overriding a model decision (Art. 14).
|
||||
type HumanOverride struct {
|
||||
OriginalEventRef string
|
||||
OperatorRef string
|
||||
JustificationCommitment map[string]string
|
||||
NewDecision string
|
||||
Extra M
|
||||
}
|
||||
|
||||
func (e HumanOverride) EventType() string { return EventTypeHumanOverride }
|
||||
|
||||
func (e HumanOverride) ToPayload() (M, error) {
|
||||
return finishPayload(M{
|
||||
"original_event_ref": e.OriginalEventRef,
|
||||
"operator_ref": e.OperatorRef,
|
||||
"justification_commitment": orNil(e.JustificationCommitment),
|
||||
"new_decision": e.NewDecision,
|
||||
}, RefsHumanOverride, e.Extra)
|
||||
}
|
||||
|
||||
// IncidentReport is a reportable incident with NIS2-style field names.
|
||||
type IncidentReport struct {
|
||||
Severity string
|
||||
Category string
|
||||
DetectedAt string
|
||||
SummaryCommitment map[string]string
|
||||
AffectedService string
|
||||
Extra M
|
||||
}
|
||||
|
||||
func (e IncidentReport) EventType() string { return EventTypeIncidentReport }
|
||||
|
||||
func (e IncidentReport) ToPayload() (M, error) {
|
||||
return finishPayload(M{
|
||||
"severity": e.Severity,
|
||||
"category": e.Category,
|
||||
"detected_at": e.DetectedAt,
|
||||
"summary_commitment": orNil(e.SummaryCommitment),
|
||||
"affected_service": e.AffectedService,
|
||||
}, RefsIncidentReport, e.Extra)
|
||||
}
|
||||
|
||||
// DataAccess records an access to personal or regulated data.
|
||||
type DataAccess struct {
|
||||
SubjectRefCommitment map[string]string
|
||||
Purpose string
|
||||
LegalBasis string
|
||||
AccessorRef string
|
||||
Extra M
|
||||
}
|
||||
|
||||
func (e DataAccess) EventType() string { return EventTypeDataAccess }
|
||||
|
||||
func (e DataAccess) ToPayload() (M, error) {
|
||||
return finishPayload(M{
|
||||
"subject_ref_commitment": orNil(e.SubjectRefCommitment),
|
||||
"purpose": e.Purpose,
|
||||
"legal_basis": e.LegalBasis,
|
||||
"accessor_ref": e.AccessorRef,
|
||||
}, RefsDataAccess, e.Extra)
|
||||
}
|
||||
|
||||
func orNil(m map[string]string) any {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
Reference in New Issue
Block a user