# Exult Voice Agent Runbook

This package implements the safe code-side pieces of the RingCentral backup
scheduling voice agent. It does not create RingCentral extensions, edit routing
rules, or write AdvancedMD appointments by itself.

## Gate 0

Run:

```bash
bun run readiness
bun run mcp:smoke
```

`readiness` checks transport shape only. `mcp:smoke` lists tools and runs the
read-only Gate 0 tools without printing raw payloads. It must pass for:

- `advancedmd` Streamable HTTP MCP
- `ringcentral` Streamable HTTP MCP
- `ringcentral-admin` Streamable HTTP MCP

RingCentral is not ready until the VM has real OAuth credentials for
`ringcentral` and the admin server has `EXULT_RC_CONFIG` or the expected
credential file. AdvancedMD PHI lookups require HTTPS plus bearer auth.

Optional AMD smoke inputs:

- `AMD_SMOKE_LOGIN=1` to force a fresh login check. Leave unset for routine
  smokes to avoid AMD rate limits.
- `AMD_SMOKE_SEARCHTERM`
- `AMD_SMOKE_PATIENT_ID`
- `AMD_SMOKE_VISIT_DATE`

Optional RingCentral smoke input:

- `RC_SMOKE_QUEUE_ID`
- `RC_SMOKE_EXTENSION_ID`
- `RC_SMOKE_DEVICE_ID`

## SIP Discovery

Run:

```bash
bun run sip:discover
```

By default this searches for an enabled extension named `AI Scheduling Backup`,
then an `Other Phone` device under it, then reads SIP info with the hosted
RingCentral Admin MCP. It redacts the SIP password in stdout.

Useful inputs:

- `RC_AI_EXTENSION_NAME`
- `RC_SIP_EXTENSION_ID`
- `RC_SIP_DEVICE_ID`
- `RC_SIP_WRITE_ENV` to write a 0600 env file with live SIP credentials

Current live preflight status on 2026-06-01:

- Hosted MCP transport: ready
- `ringcentral-softphone`: installed and test-passing
- `OPENAI_API_KEY`: configured in the VM voice-agent runtime environment;
  OpenAI Realtime smoke passed with audio and transcript after credits were
  added
- `XAI_API_KEY`: configured, but xAI currently rejects API calls until the new
  team has credits/licenses; PHI failover remains disabled
- `AI Scheduling Backup`: enabled RingCentral extension with an assigned direct
  number and dedicated `Other Phone` SIP device. Production resource IDs and
  callable numbers are intentionally kept in the private deploy env / ops notes,
  not in version control.
- `agent@exulthealthcare.com`: received the RingCentral invite/welcome emails

## Approval Request Template

To advance from preflight to a real RingCentral SIP registration, ask Gautam for
this exact approval before making any RingCentral write:

> Approve these RingCentral writes only:
> 1. Create a new enabled RingCentral extension named `AI Scheduling Backup`,
>    extension number `9001`, using `agent@exulthealthcare.com` if RingCentral
>    requires an email, with no direct phone number assignment.
> 2. Add one `Existing Phone` / `Other Phone` device under extension `9001`,
>    named `AI Scheduling Backup SIP`, for Cloud Phone SDK SIP registration.
> 3. Do not change queue 55, IVR 2000, after-hours routing, greetings,
>    forwarding, caller ID, or phone-number assignments.

After approval and provisioning:

```bash
MCP_BEARER_TOKEN=<token> bun run sip:discover
MCP_BEARER_TOKEN=<token> RC_SIP_EXTENSION_ID=<extensionId> RC_SIP_WRITE_ENV=.env.sip bun run sip:discover
set -a
source .env.sip
set +a
OPENAI_API_KEY=<key> MCP_BEARER_TOKEN=<token> bun run preflight:live
OPENAI_API_KEY=<key> MCP_BEARER_TOKEN=<token> bun run start:live
```

Do not request queue 55 overflow, after-hours, or manual failover routing until
the SIP registration and a direct live test call have both been verified.

You can generate the current checked plan and approval text with:

```bash
MCP_BEARER_TOKEN=<token> bun run provision:plan
```

After Gautam approves the exact text, the extension creation can be rehearsed
without writes:

```bash
RC_APPROVAL_TEXT_FILE=/path/to/approval.txt RC_PROVISION_DRY_RUN=1 MCP_BEARER_TOKEN=<token> bun run provision:plan
```

The actual extension creation still needs two gates at once:

- `EXULT_RC_ALLOW_WRITES=1` on the hosted `ringcentral-admin` MCP process.
- `RC_PROVISION_EXECUTE=1` plus `RC_APPROVAL_TEXT_FILE` or `RC_APPROVAL_TEXT`
  matching the exact checked approval text.

The tool only creates the extension. Device provisioning remains an Admin
Console step until RingCentral's device-create schema is confirmed and tested.

For the approved extension write, use the one-shot write-window helper on the
VM. It flips `EXULT_RC_ALLOW_WRITES=1`, runs exactly one provision command, and
then flips writes back off in a shell trap:

```bash
I_UNDERSTAND_THIS_WRITES_RINGCENTRAL=1 \
RC_APPROVAL_TEXT_FILE=/tmp/voice-agent-approval.txt \
bash deploy/approved-rc-write-window.sh
```

Do not use this helper for routing, IVR, queue, greeting, forwarding, caller ID,
or phone-number assignment changes.

## VM Service

The deploy scaffold is in `deploy/`:

- `deploy/voice-agent.env.example`
- `deploy/run-voice-agent.sh`
- `deploy/voice-agent.service`

Install only after `preflight:live` passes manually:

```bash
cp deploy/voice-agent.env.example ~/.config/voice-agent.env
chmod 600 ~/.config/voice-agent.env
# fill OPENAI_API_KEY, MCP_BEARER_TOKEN, and RINGCENTRAL_SIP_* values
mkdir -p ~/.config/systemd/user
cp deploy/voice-agent.service ~/.config/systemd/user/voice-agent.service
systemctl --user daemon-reload
systemctl --user enable --now voice-agent.service
```

The supervisor runs `bun run preflight:live` before starting the process and
refuses to start if SIP/provider/MCP checks fail.

To stage the service without enabling it:

```bash
bash deploy/install-disabled-service.sh
```

## Live Write Gates

RingCentral writes need explicit Gautam approval per request with the exact
source rule and exact destination before any PUT/POST/DELETE. That includes:

- Creating the `AI Scheduling Backup` extension/device
- Queue 55 overflow routing
- After-hours routing
- Manual failover routing
- Any external forwarding rule

AdvancedMD writes need explicit approval before any appointment mutation. The
preferred reschedule sequence is create-and-verify replacement, then delete or
cancel the old appointment. If AMD rejects concurrent appointments and forces
DELETE-then-POST, autonomous rescheduling remains disabled until that specific
risk is approved.

The voice-agent scheduling port also enforces patient verification as a local
write gate: `request_reschedule` fails closed unless `verify_patient_identity`
has positively identified the same patient during the same call. Keep the hosted
AMD MCP's broad `raw_amd_call` write path disabled for the voice agent; connect
only narrow appointment-write tools.

## Voice Providers

`VoiceProvider` defaults to OpenAI Realtime. Grok Voice is available only when
`XAI_PHI_ALLOWED=1` is set after compliance approval.

The voice model receives only the policy proxy tools. It does not receive raw
MCP credentials or unrestricted AdvancedMD/RingCentral access.

The OpenAI adapter uses the current GA `session.update` shape with
`session.type = "realtime"` and audio config under `session.audio`. The Grok
adapter uses the same provider abstraction but emits the legacy-compatible
session shape because xAI documents OpenAI Realtime compatibility with event
differences.

The default provider PCM format is 24 kHz mono. If the RingCentral media SDK
emits a different codec or sample rate, wire a `RingCentralAudioTransform`
transcoder at the media bridge boundary before production traffic.

## Local Simulator

Run:

```bash
bun run simulate -- "I need to reschedule my appointment"
```

The simulator runs the RingCentral bridge against a fake media client and fake
voice provider so the policy/tool path can be tested without patient data,
RingCentral routing changes, or AdvancedMD writes. It proves the call is
answered, caller text reaches the voice provider, tool calls are routed through
the policy proxy, and an audit record is emitted.

## Live RingCentral Media

The `RingCentralSoftphoneAdapter` is an optional Cloud Phone SDK adapter. It
requires the npm package `ringcentral-softphone` plus SIP credentials from an
`Existing Phone` / `Other Phone` RingCentral device:

- `RINGCENTRAL_SIP_DOMAIN`
- `RINGCENTRAL_SIP_OUTBOUND_PROXY` (`proxyTLS`, not plain `proxy`)
- `RINGCENTRAL_SIP_USERNAME`
- `RINGCENTRAL_SIP_PASSWORD`
- `RINGCENTRAL_SIP_AUTHORIZATION_ID`

Do not run it against production routing until the dedicated AI extension/device
and routing rule have been explicitly approved.

To start the live process after SIP, MCP, and provider credentials are present:

```bash
bun run preflight:live
bun run start:live
```

## RingCentral Reporting Notes

Use detailed RingCentral call logs for reconciliation, because the detailed view
contains `sessionId`, `telephonySessionId`, and call legs. Recording metadata,
when present, includes `recording.contentUri`; store only the URI/metadata in
the agent audit table unless recording retention has been approved. Forwarding
targets and state-rule patches are routing writes and stay behind Gautam's
approval gate.

References:

- https://developers.ringcentral.com/guide/voice/call-log/api
- https://developers.ringcentral.com/guide/voice/call-log/recordings
- https://developers.ringcentral.com/guide/voice/call-routing/user-call-handling/user-call-forwarding-targets
