Skip to content

Commit bdbd08d

Browse files
committed
fix(callcenter): stamp audit events with chain's configured super_domain
Per chatgpt-codex-connector review on PR #364: emit_audit was calling super_domain_for_family(owl.family()) to look up the super-domain via FAMILY_TO_SUPER_DOMAIN — a private all-Unknown static with no mutation API in the current repo. Every event was therefore stamped Unknown even when the caller wired .with_audit_chain(SuperDomain::Healthcare, …), silently breaking compliance partitioning on the audit stream. AuditChain already carries the configured super_domain (the same field fed into the merkle salt), so the chain itself is the single source of truth. Stamp the event from chain.super_domain instead of the static lookup, holding the chain lock across the read+advance so the event and the chain commit agree by construction. The FAMILY_TO_SUPER_DOMAIN reverse-lookup table stays in place — it lands hydrated in sprint 5 (D-SDR-3b family-hydration worker) once a loader from the registry exists. Until then, the chain's configured domain is the authoritative stamp. Test updated to assert SuperDomain::WorkOrderBilling (the chain's configured value) instead of the placeholder Unknown. 96/96 callcenter lib tests pass. Refs: PR #364 review threads (P2 priority).
1 parent b12e357 commit bdbd08d

1 file changed

Lines changed: 20 additions & 14 deletions

File tree

crates/lance-graph-callcenter/src/unified_bridge.rs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use lance_graph_ontology::proposal::MappingRow;
4545
use lance_graph_rbac::access::AccessDecision;
4646
use lance_graph_rbac::policy::{Operation, Policy};
4747

48-
use crate::super_domain::{super_domain_for_family, SuperDomain};
48+
use crate::super_domain::SuperDomain;
4949
use crate::unified_audit::{
5050
AuditChain, AuditMerkleRoot, AuthDecision, AuthOp, NoopUnifiedAuditSink, UnifiedAuditEvent,
5151
UnifiedAuditSink,
@@ -383,25 +383,31 @@ impl<B: NamespaceBridge> UnifiedBridge<B> {
383383
/// the merkle chain and emit to the configured sink.
384384
fn emit_audit(&self, schema_ptr: &SchemaPtr, op: AuthOp, decision: AuthDecision) {
385385
let owl = owl_from_schema_ptr(schema_ptr);
386-
let super_domain = super_domain_for_family(owl.family());
386+
// Hold the chain lock across stamping so the event's super_domain
387+
// and the chain it commits into are guaranteed to agree.
388+
// FAMILY_TO_SUPER_DOMAIN is an all-Unknown static today (the family
389+
// → super-domain hydration table lands in sprint 5); until then,
390+
// trust the super_domain the caller wired into the chain via
391+
// with_audit_chain(...) as the single source of truth.
392+
let mut chain = match self.audit_chain.lock() {
393+
Ok(g) => g,
394+
// Mutex poisoned by a panicking holder — keep audit emission
395+
// best-effort by advancing through the poisoned guard.
396+
Err(poisoned) => poisoned.into_inner(),
397+
};
387398
let event = UnifiedAuditEvent {
388399
ts_unix_ms: now_unix_ms(),
389400
tenant: self.tenant,
390-
super_domain,
401+
super_domain: chain.super_domain,
391402
owl,
392403
op,
393404
decision,
394405
actor_role_hash: self.actor_role_hash,
395406
// overwritten by AuditChain::advance
396407
merkle_root: AuditMerkleRoot::GENESIS,
397408
};
398-
let stamped = match self.audit_chain.lock() {
399-
Ok(mut chain) => chain.advance(event),
400-
// Mutex poisoned by a panicking holder — fall back to advancing
401-
// through the poisoned guard so audit emission stays best-effort
402-
// (we'd rather have a slightly-off chain than skip the event).
403-
Err(poisoned) => poisoned.into_inner().advance(event),
404-
};
409+
let stamped = chain.advance(event);
410+
drop(chain);
405411
self.audit_sink.emit(&stamped);
406412
}
407413
}
@@ -661,10 +667,10 @@ mod tests {
661667
assert_eq!(events[0].decision, AuthDecision::Allow);
662668
assert_eq!(events[0].op, AuthOp::Read);
663669
assert_eq!(events[0].tenant, TenantId(7));
664-
// Event super_domain comes from FAMILY_TO_SUPER_DOMAIN reverse lookup,
665-
// which is all-`Unknown` until TTL hydration (D-SDR-3b). The chain's
666-
// configured super-domain/salt drives merkle hashing, not this field.
667-
assert_eq!(events[0].super_domain, SuperDomain::Unknown);
670+
// Event super_domain is stamped from the configured AuditChain so
671+
// compliance partitioning works before FAMILY_TO_SUPER_DOMAIN is
672+
// hydrated (D-SDR-3b, sprint 5).
673+
assert_eq!(events[0].super_domain, SuperDomain::WorkOrderBilling);
668674
assert_ne!(events[0].merkle_root, AuditMerkleRoot::GENESIS);
669675
}
670676

0 commit comments

Comments
 (0)