Clinic voice agent

A voice agent books appointments through PolicyArc. Move its CertOps scorecard and watch what the policy allows.

1. Call simulator

Caller: nobody identified

Tools

Try to break it

2. API gateway

The latest call: the request, the policy decision, then the response. A refusal reaches the agent only as "403 Access denied".

    Agent and certification

    Policy tier checking
    Certification
    cert-voice-001
    Status
    …

    Scorecard (tick = baseline)

    Drift of 0.10 or more removes booking. Drift of 0.25 or more, or a revoked certification, hands the caller to a person.

    3. Clinic calendar

    The clinic's own view of the day, read from the clinic system. It shows what was really booked, so a refused booking leaves it unchanged.

    Clinic patients

    You can type any name, but only these patients exist. Anyone else is refused as not matched. Press Use to fill the form.

      Call log

      Every call so far, newest first. Each row has three columns: the request, the policy decision, and the response. Open one for the detail.

        Design

        PolicyArc sits between a voice agent and the clinic system. The agent never holds a patient's identity on its own: it proves who it is with a CertOps-backed registration, then asks PolicyArc for a token that serves exactly one patient. Four layers of policy apply: registration once, then the token, the patient and the trust tier on each call.

        Sequence

        Solid arrows are requests, dashed arrows are replies. Every call after step 3 is judged again by OPA, using the token's stored input plus what the call asked for, so a policy change reaches tokens that were issued earlier.

        Two tokens, one patient

        Agent token

        From client_credentials. Holds the full ceiling of actions and no patient, so it can list slots and ask for a handoff, but not read a patient or book.

        authorization_details = [{
          "type": "urn:demo:medplum",
          "actions": ["appt:read", "appt:write",
                      "patient:read", "handoff:request"]
        }]

        Patient token

        From an RFC 8693 token exchange, with the agent token as the subject. The caller's identifiers ride on the clinic resource's own entry, where the agent cannot change them. Policy resolves them to one patient, at the exchange and on every call.

        authorization_details = [{
          "type": "urn:demo:medplum",
          "actions": [ ...same or fewer... ],
          "family": "Reyes",
          "birthdate": "1990-04-02",
          "phone": "..."          // optional
        }]

        The request carries identifiers, never a patient id, so the agent cannot name a patient it has not matched. Zero matches or several are refused with patient_not_matched, and no list of near matches is returned.

        Policy, in four layers

        LayerRuns atWhat it decidesDeny codesDefined in
        1. Registration gate/registerOnly a statement carrying a certops_cert_id that CertOps has approved may register for the clinic resource.registration deniedregister_gate.rego
        2. Token entitlements/tokenThe agent token holds the action ceiling. An exchanged token may hold only what its parent holds.token_exchange_actions_exceededthe AS's baseline policy
        3. Patient scopingthe exchange, then every callThe identifiers resolve to exactly one patient. Patient routes need a patient token. The patient named in a call must be the token's patient.patient_not_matched patient_required patient_mismatchmedplum_voice.rego
        4. Trust tierevery callHow far the agent is trusted right now, from its CertOps scorecard (below).write_not_permitted_while_attenuated handoff_to_humanmedplum_voice.rego

        The trust tier

        Drift is the largest drop from baseline across the scorecard dimensions. CertOps pushes the scorecards and the two thresholds as policy data. The tier is decided in policy and nowhere else.

        TierWhenWhat the agent can do
        fullapproved, drift below 0.10All four tools.
        attenuatedapproved, drift 0.10 up to 0.25Reads and patient tokens still work. Booking (appt:write) is refused.
        handoffdrift 0.25 or more, a revoked or unknown cert, or any missing dataOnly request_human_handoff, which tells the agent to transfer the caller to a person. It needs no patient lookup, so a failing lookup cannot block it.

        Good to know