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>
136 lines
4.0 KiB
Go
136 lines
4.0 KiB
Go
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
|
|
}
|