FEAT-OPS-001: Audit Protected Emission
| Reference Number | Version | Effective Date | Supersedes | Authority Level |
|---|---|---|---|---|
| FEAT-OPS-001 | 1.0 | 2026-05-10 | N/A | Normative |
I. Purpose
Define the required execution contract for audit_protected() — the canonical mechanism by which any FEAT attaches tamper-evident audit lineage to a protected row after writing it to the database, in compliance with INV-ARC-016 and DOM-OPS-002.
II. Scope
Applies to every FEAT that writes to a protected table. A protected table is any table listed in the PROTECTED_FIELDS_BY_TABLE registry defined in DOM-OPS-002. The current registry includes transactions; additional tables are added as Phase 3b migrations land.
III. Authority Level
Normative. Subordinate to INV-ARC-016, DOM-OPS-002, and FEAT-CORE-000.
IV. Dependencies
INV-ARC-016_LAWFUL_EXISTENCE_AND_AUDIT_LINEAGE.mdDOM-OPS-002_AUDIT_LINEAGE_INTEGRITY.mdFEAT-CORE-000_FEATURE_EXECUTION_CONSTITUTIONAL_DIRECTIVE.md
II. Execution Context
1. Required Inputs
table_name: Name of the protected table being written (must appear inPROTECTED_FIELDS_BY_TABLE)row: The flushed SQLAlchemy model instance (must have a non-null primary key)operation: One ofINSERT/UPDATE/DELETE/TRANSITIONfields: List of field names to include in the payload digest (must match the registry entry fortable_name)
2. Resolved Context (from Active FEAT)
class_id: Resolved fromFEATContext.class_idfeat_id: Resolved fromFEATContext.active_featcorrelation_id: Resolved fromFEATContext.correlation_ididempotency_key: Resolved fromFEATContext.idempotency_keyactor_type,actor_id_hash: Resolved from the actor bound to the FEAT context
III. Orchestration Logic
1. Preconditions (verified before emit)
- An active FEAT context must be present.
audit_protected()shall raiseAuditContextErrorif called outside a FEAT context orSystemAuditAuthority. db.session.flush()must have been called prior toaudit_protected(), ensuringrow.idis assigned and non-null.- The owning FEAT transaction must not have committed.
audit_protected()does not commit; it participates in the caller’s transaction boundary. AUDIT_HMAC_KEYmust be present in the environment.
2. Mutation Sequence (Atomic Within Caller’s Transaction)
- Resolve
class_id,feat_id,correlation_id,idempotency_key,actor_type,actor_id_hashfrom the active FEAT context. - Call
emit_audit_event(table_name, row_pk=str(row.id), operation, protected_fields, class_id=..., feat_id=..., ...). - Inside
emit_audit_event(): a. Resolve chain scope:"class:{class_id}"or"system". b. AcquireChainHeadwithSELECT FOR UPDATE. c. BootstrapChainHeadif absent (lazy per-class genesis). d. Computenext_sequence = head.latest_sequence + 1. e. Computepayload_digestfrom the canonical payload of the protected fields. f. Computecontext_digestfrom actor/feat context fields. g. Computeevent_hash(HMAC-SHA256) over pipe-delimited chain fields. h. Construct and flush theAuditEventrow. i. UpdateChainHead.latest_hash,latest_sequence,event_count,last_updated_utc. emit_audit_event()returns the flushedAuditEventinstance (.idis now assigned).- Set
row.lineage_event_id = event.id. - Set
row.lineage_token = event.hmac_signature. - Set
row.lineage_version = event.signature_version. - Return. The caller’s FEAT commits the entire transaction.
IV. Invariants and Constraints
-
Must be called after flush, before commit.
audit_protected()shall be called afterdb.session.flush()(sorow.idis available) and before the owning FEAT’s transaction commits. -
Must not commit internally.
audit_protected()andemit_audit_event()shall not calldb.session.commit(). The caller’s transaction boundary owns the commit. -
AuditContextErroris not swallowable. Any caller that catchesAuditContextErrorand continues without re-raising violatesINV-ARC-016. The error shall propagate and abort the transaction. -
lineage_tokenis a pointer, not proof. Settingrow.lineage_tokenis a fast provenance marker only. Canonical proof of lawful existence requires a full chain walk by the verifier (verify_chain) confirming both HMAC continuity and payload digest match. -
One emit per row per state change.
audit_protected()shall be called exactly once per protected row creation or update. Calling it multiple times for the same row without an intervening state change is prohibited. -
Fields must match the registry. The
fieldsargument must exactly match the entry inPROTECTED_FIELDS_BY_TABLEfortable_name. Deviations produce falseINVALIDresults on subsequent verification.
V. Idempotency
audit_protected() is not idempotent by design — each call produces a new AuditEvent and increments the chain sequence. Idempotency for the business mutation is the responsibility of the owning FEAT (via idempotency_key check before the mutation phase). If the owning FEAT returns early due to an idempotency hit, audit_protected() must not be called.
VI. Audit Requirements
DOM-OPS-002 records the following for every audit_protected() call:
| Field | Source |
|---|---|
chain_scope |
Resolved from class_id |
sequence_number |
Assigned atomically from ChainHead |
event_hash |
HMAC-SHA256 over chain fields |
payload_digest |
SHA-256 of canonical protected field values |
context_digest |
SHA-256 of actor/feat context fields |
feat_id |
Active FEAT name |
correlation_id |
Propagated from FEAT context |
idempotency_key |
Propagated from FEAT context |
actor_type / actor_id_hash |
Actor bound to FEAT context |
class_id / seat_id / teacher_id |
From FEAT context |
table_name / row_pk / operation |
From caller inputs |
created_at_utc |
UTC timestamp at emit time |
signer_key_id / signature_version |
From current HMAC key configuration |
Last updated: 2026-06-14