CareSupport’s safety layer isn’t optional—it’s mechanical. Every message passes through four enforcement gates:
role_filter.py — Pre-filters context by access level (agent never sees restricted data)
phi_audit.py — Logs every PHI access for HIPAA compliance
approval_pipeline.py — Requires coordinator confirmation for medication/member changes
message_lock.py — Serializes message processing per family (prevents race conditions)
Enforcement is code, not prompts. The agent is ALSO told to respect access levels in its system prompt, but the filter catches what the agent misses. Defense in depth.
If a section isn’t in your allowed list, you don’t see it. Period.
# From role_filter.py:126-157def filter_family_context(family_md: str, access_level: str) -> str: config = ACCESS_MATRIX.get(access_level) allowed = config["sections"] if "*" in allowed: return family_md # Full access, no filtering # Parse file into sections header, sections = parse_family_sections(family_md) # Keep only allowed sections filtered_parts = [header] for section in sections: if section.key in allowed: filtered_parts.append(section.content) return "\n\n".join(filtered_parts)
The agent never sees restricted sections. If Solan (schedule-only) asks “What medications is she on?”, the agent’s context doesn’t include the medications section. It can’t leak what it doesn’t have.
Even after pre-filtering, the agent might accidentally mention restricted information (hallucinated medication names, condition keywords). The post-check catches this:
How Leakage Detection Works
# From role_filter.py:235-275def check_outbound_message(message: str, access_level: str) -> LeakageResult: allowed = ACCESS_MATRIX[access_level]["sections"] if "*" in allowed: return LeakageResult(is_clean=True) # Full access, no restrictions leaked_categories = [] leaked_terms = [] # Check medication leakage (for members who can't see medications) if "medications" not in allowed: med_terms = scan_for_medication_leakage(message) if med_terms: leaked_categories.append("medications") leaked_terms.extend(med_terms) # Check condition leakage (for members who can't see care_recipient) if "care_recipient" not in allowed: condition_terms = scan_for_condition_leakage(message) if condition_terms: leaked_categories.append("conditions") leaked_terms.extend(condition_terms) return LeakageResult( is_clean=(len(leaked_categories) == 0), leaked_categories=leaked_categories, leaked_terms=leaked_terms )
Medication patterns detected:
Drug name suffixes: -pril, -sartan, -statin, -formin, -olol
# From sms_handler.py:1065-1097leakage = check_outbound_message(sms_response, access_level)if not leakage.is_clean: # BLOCKED — agent tried to share restricted information _audit.log_response_blocked( family_id=family_id, recipient_phone=from_phone, access_level=access_level, leaked_categories=leakage.leaked_categories, leaked_terms=leakage.leaked_terms, ) # Replace with safe response sms_response = "I'm sorry, I can't share that information with your access level. Please contact the care coordinator if you need more details." return {"success": True, "response": sms_response, ...}
The user receives:
I'm sorry, I can't share that information with your access level.Please contact the care coordinator if you need more details.
Certain changes require explicit YES/NO approval from a full-access member:
What Requires Approval
Approval Flow
Detection Logic
# From approval_pipeline.py:40-47APPROVAL_REQUIRED = { ("medications", "append"), # Adding a new medication ("medications", "prepend"), # Adding a new medication ("medications", "replace"), # Changing dosage, schedule, etc. ("care_recipient", "replace"), # Changing conditions, emergency contact ("members", "append"), # Adding a new member ("members", "replace"), # Changing member details}
Why these? High-stakes changes. A typo in medication dosage or a wrong phone number for a care team member could cause harm.Everything else auto-applies: Schedule updates, recent event notes, active issue resolutions.
Stale lock recovery: If lock is older than 120 seconds, assume process crashed and force-acquire.
# From sms_handler.py:958with family_lock(family_id, phone=from_phone): return await _process_message( member, family_id, family_dir, access_level, from_phone, body, dry_run, service )
Effect: All reads and writes to family.md, schedule.md, medications.md, and pending_approvals.json are serialized. Only one message per family is processed at a time.
What if poll_inbound and handle_sms both call family_lock in the same thread?
# Thread-local tracking prevents deadlock_thread_local = threading.local()def _get_held_locks() -> set: if not hasattr(_thread_local, "held_locks"): _thread_local.held_locks = set() return _thread_local.held_locks# In family_lock():held = _get_held_locks()if family_id in held: # Already held by this thread — reentrant, just yield yield None return
Different threads track independently. Thread A can hold lock for family “kano” while Thread B holds lock for family “martinez” simultaneously.
Scenario 1: Restricted Member Asks About Medications
User: Solan (schedule-only access)
Solan: What medications is Degitu on?CareSupport: I don't have access to medication details for your role. Please reach out to Liban if you need this information.
What happened:
filter_family_context() removed medications section before agent saw it
Agent’s context doesn’t include medications
Agent responds that it doesn’t have that information
log_context_load() logged that Solan requested context (sections_loaded: [“schedule”, “availability”])
Scenario 2: Agent Accidentally Leaks Medication Name
User: Solan (schedule-only access)
Solan: How is Degitu feeling today?
Agent tries to respond:
She's doing well. Make sure she takes her Lisinopril this morning.
User: Roman (schedule+meds access, but cannot approve)
Roman: The doctor said to increase Lisinopril to 20mgCareSupport: I've noted the medication change. Sending to Liban for approval.[To Liban]⚠️ Approval needed: Change Lisinopril dosage to 20mgRequested by Roman.Reply YES or NO (ref: a3f8c21d)Liban: YESCareSupport: ✅ Approved: Change Lisinopril dosage to 20mg. Change applied.
Want to see enforcement in action? Read sms_handler.py:914-1228 (handle_sms function). Follow the inline comments—every enforcement gate is called explicitly with error handling.