feat(ovhcloud): hold Riposte's on-call ladder in a VoIP call queue (1.1.0)

Riposte's pager can hand its on-call ladder to a marketplace instance, so that a
number published to clients rings whoever is on duty without the call ever
crossing the SOC's own trunk — and without spending one of its simultaneous
calls, which are sold in blocks and are the reason the PBX-side mode is too
expensive for a small deployment.

Three commands, and they are a contract rather than this integration's own
vocabulary: set_oncall_ladder (ordered numbers + ring_seconds), get_oncall_ladder
(what the carrier ACTUALLY holds, so a comparison can be honest) and list_calls
(the journal, after the fact). The pager offers whichever instances declare the
first one; the README now documents the contract for whoever writes the next
router.

Extends the existing ovhcloud integration rather than adding a second one: it
already carries the signed-request authentication, and making an operator enter
the same application key twice would serve nobody. Three telephony_* config
fields are added; every existing command is untouched, and an instance that
leaves them empty is exactly the OVHcloud reader it was before.

set_oncall_ladder RECONCILES rather than rebuilds. Agents are created when a
number has none, reordered by position, and anybody who drops off the ladder is
LOGGED OUT and removed from the queue — never deleted, because deleting throws
away whatever else the operator configured on that agent and they are on the rota
again next week. It also enforces the sequential strategy: publishing an ordered
list into a queue set to ring everybody at once is a lie that is invisible from
Riposte.

Two details that would each have cost an evening: OVH accepts no "+" in a
phoneNumber field, so E.164 is converted to the 00-prefixed form both ways; and a
service holding several queues is REFUSED rather than guessed, because guessing
publishes the on-call ladder into whichever queue the API happened to list first.

Verified against a stubbed API for the first publication and for a rota handover.
The signature, the error codes and the exact number format are only proved by the
first real call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN1R4EnNFUV2Xwtd9q98tj
This commit is contained in:
2026-09-19 18:00:33 +00:00
parent 11089e9e80
commit 864300d7c5
5 changed files with 724 additions and 3 deletions
+33
View File
@@ -202,6 +202,39 @@ named by field, evaluated against the rule's last captured samples.
---
## Well-known commands: the on-call router
Most command ids are an integration's own business. **Three are a contract**, and
declaring them is how an integration becomes something Riposte's pager can hand its
on-call ladder to — a telephony carrier that will ring the on-call team when a
client calls the number you published to them.
| Command | Inputs | What it must do |
|---|---|---|
| `set_oncall_ladder` | `numbers` (ordered, E.164), `ring_seconds` | Ring those numbers, **in that order**, one after another. A carrier that can only hold ONE destination takes `numbers[0]` and says so in its description — a single rung that follows the rota still beats a redirect a human has to edit. Must be idempotent: Riposte republishes on a schedule. |
| `get_oncall_ladder` | — | Answer `{ numbers: [...] }`: what the carrier **actually holds**, not what was last sent. Optional, and what makes Riposte's comparison honest. |
| `list_calls` | `since`, `limit` | Answer `{ calls: [...] }` with `from_number`, `did`, `started_at`, `duration`. Optional; it fills the pager journal after the fact. |
**The id is the whole contract.** There is no registry, no manifest field to set and
no vendor list anywhere in Riposte: the pager's settings screen offers whichever
instances declare `set_oncall_ladder`.
**Declarative, never reactive.** Riposte says *who* to ring; the carrier decides
*when*. A contract shaped the other way round — the carrier calling us as each call
arrives — is a webhook, and a self-hosted SOAR on a VM with no inbound path from the
internet cannot receive one.
Two things a router should be honest about in its description, because they decide
whether an operator should use it at all: whether the carrier can **ask the answerer
to press a key** before connecting the caller (most cannot, so a mobile's voicemail
ends the search as though a person had answered), and whether ringing a rung costs
anything on the operator's own trunk.
`integrations/ovhcloud` implements all three against a VoIP call queue; read it
before writing another.
---
## Adding this catalog to Riposte
In Riposte → **Integrations → Marketplace → Add source**: