Repository navigation
feat: add SignalWire, ElevenLabs, Vapi and Pipecat adapters; remove twilio_adapter shim - #14
Merged
Merged
Conversation
The top-level twilio_adapter/ package was a backwards-compatibility layer
superseded by adapters/twilio/. Nothing on main imports it (verified by
grep); the top-level tests/test_{config,builder,webhook}.py already target
adapters.twilio, not this shim. Also drops it from the Dockerfile COPY and
pyproject's packaging/coverage config.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Webhook-based: VAPI posts an end-of-call-report server message per call. Authenticated via constant-time comparison of the shared secret VAPI echoes back in x-vapi-secret; fails closed like every other webhook adapter (config raises at startup unless the secret is set or ALLOW_UNSIGNED_WEBHOOKS=true). VAPI also offers HMAC and OAuth auth with a configurable header; this implements the simpler, documented shared-secret header, not the HMAC variant (see README). Wired to core.lawful_basis and core.media_publisher; ADAPTER_SOURCE attachments use mediatype (draft-04), not mimetype. Not registered against core.base_builder.BaseVconBuilder: VAPI's payload carries the whole conversation (per-turn messages, transcript, analysis) in one event rather than one recording per event, so the builder is written directly against vcon-lib following the same conventions instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In-process observer, not a webhook receiver: Pipecat runs as a library inside the caller's own voice-AI pipeline process, so there is no inbound request to authenticate. VconConversationObserver accumulates per-turn state via frame-processor hooks and builds a vCon on end-of-conversation; as_frame_processor() wraps it as a real pipecat FrameProcessor, importing pipecat-ai lazily so the rest of the monorepo doesn't need it installed. `python main.py pipecat` starts a health-check-only FastAPI app so the adapter registers and containerizes consistently with the others. Wired to core.lawful_basis and core.media_publisher like the other new adapters; written directly against vcon-lib rather than core.base_builder.BaseVconBuilder since a Pipecat conversation is multi-turn text plus one optional audio dialog, not a single recording download. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Webhook-based, not a poller: ElevenLabs Conversational AI sends post_call_transcription and post_call_audio as independent webhook calls, authenticated with an HMAC signature in elevenlabs-signature. The earlier rescue/cursor-multi-mode-wip draft polled GET /convai/conversations instead; the documented webhook is the more direct integration (no poll lag, richer payload), so this version receives it directly like every other webhook adapter here, and fails closed the same way (config raises at startup unless the secret is set or ALLOW_UNSIGNED_WEBHOOKS=true). Signature format caveat: ElevenLabs' public docs confirm the header name and that it's an HMAC signature with a timestamp, but not the exact signed-string byte format. This implements the widely-used t=<timestamp>,v0=<hex_hmac_sha256> convention the header shape resembles; flagged in the README as unverified against a real signed request. A mismatch fails closed (rejects), it does not silently accept. Because the two webhook types arrive independently, this builds one vCon per webhook rather than merging them, tagged with a shared conversation_id for downstream correlation. Wired to core.lawful_basis and core.media_publisher; written directly against vcon-lib rather than core.base_builder.BaseVconBuilder for the same reason as the VAPI/Pipecat builders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Poller, not a webhook receiver: SignalWire's Compatibility API has no "recording complete" push, so this polls GET /Recordings.json on an interval. SIGNALWIRE_AUTH_TOKEN is outbound Basic Auth to SignalWire's own API, not a webhook secret, so there is no VALIDATE_*_WEBHOOK setting here. Fixes the dedupe/watermark bug an audit found in the standalone rescue/cursor-multi-mode-wip draft: that poller marked a call processed unconditionally after attempting delivery, including when the POST to the conserver failed, and separately advanced its poll watermark to now() every cycle regardless of outcome -- so a failed delivery was dropped silently, and even if that weren't true, the failed call would fall out of the query window on the very next poll and never be retried at all. This version only marks a call processed (via core.tracker.StateTracker) once HttpPoster.post() returns True, and persists a watermark (<state_file>.watermark) that never advances past a call still pending, so a failed build or post leaves that call inside the next poll's fetch window until it succeeds. One vCon per call with one dialog per recording (SignalWire can return several recordings per call in one poll), wired to core.lawful_basis and core.media_publisher, written directly against vcon-lib rather than core.base_builder.BaseVconBuilder for the same multi-dialog reason as the other three new adapters. `python main.py signalwire` runs the poller on a background thread behind a /health endpoint, matching the container healthcheck the other adapters get for free from being webhook receivers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds each adapter's run_*_adapter() to main.py's ADAPTERS registry so `python main.py <name>` and the Docker image (ENTRYPOINT/CMD unchanged) work for all four. Documents each in the README's platform list, adds a config-table section per adapter (required vars, validation defaults, signature-scheme caveats), updates the source tree, notes the webhook vs. poller vs. in-process split in the "Webhook authentication" section, and adds their env vars to .env.example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…patch targets Adds config/builder/webhook (or observer/poller) tests for the four new adapters: signature valid/bad/missing-header cases and missing-secret refusal for the two webhook adapters (vapi, elevenlabs), lawful-basis set/unset for every new builder, and the SignalWire poller's dedupe and watermark behavior -- specifically that a failed build or a failed POST to the conserver leaves the call unmarked and inside the next poll's fetch window rather than being silently dropped (the CON-703 audit finding). Also fixes six tests in tests/test_builder.py and one in tests/test_webhook.py that patched twilio_adapter.builder.requests.get / twilio_adapter.webhook.VconBuilder -- the deleted shim module, not adapters.twilio (which is what create_app() and TwilioVconBuilder.build() actually use). Deleting the shim surfaced this: those patches had no effect on the code under test, so the tests were passing without actually exercising the mocked HTTP call. Retargeted to adapters.twilio.builder.requests.get and adapters.twilio.webhook.TwilioVconBuilder. Baseline (origin/main): 539 passed. After this change: 619 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ported from a private vcon-freeswitch-tester repo (cloned read-only to a
scratch dir, reviewed file by file, deleted after copying). One line in
setup_mac_mini.sh naming a specific machine and its LAN IP/username was
redacted to a generic comment (setup_local.sh here); everything else
(mock_conserver.py, the FreeSWITCH ESL/dialplan/Lua configs, and the
test_*.sh/test_ws_client.py scripts) only referenced localhost, Homebrew's
own /opt/homebrew paths, FreeSWITCH's documented default ESL password
("ClueCon", not a real credential), and already-fake phone numbers matching
the rest of this repo's fixtures.
Excluded from the default pytest run via collect_ignore_glob in
tests/e2e/freeswitch/conftest.py: it needs a live FreeSWITCH + Redis + this
repo's own adapter running locally, which CI doesn't have, and
test_ws_client.py imports websockets, which isn't a project dependency.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schema-validated a sample SignalWire vCon (LAWFUL_BASIS=consent,
MEDIA_BACKEND=filesystem) against the WG JSON schema and found
recording.get("duration") was passed straight through as the string
SignalWire's Recordings.json actually returns (e.g. "30"), failing
validation on dialog[0].duration. Coerces to float, logging and omitting
the field on anything unparseable rather than raising. Adds regression
tests for both the coercion and the omit-on-failure path.
Also fixes ruff/black findings in tests/e2e/freeswitch/ (an f-string with
no placeholders, a bare `raise` inside an except that should chain `from
None`, formatting) so `ruff check .` / `black --check .` stay clean across
the whole repo even though this directory is excluded from the pytest run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the SignalWire, ElevenLabs, Vapi and Pipecat adapters to the monorepo, built on the current
core/patterns: fail-closed webhook validation, lawful basis, the media publisher and draft-04 output. They existed only as parked work-in-progress based on an oldermain; this rewrites them against today's code rather than copying the old versions, which had no shared config, no lawful basis, no media publisher and still usedmimetype. Also removes the deadtwilio_adapter/shim and brings in the FreeSWITCH end-to-end harness.Adapters
end-of-call-report)x-vapi-secret, constant-time compare (Vapi server authentication docs)BaseVconBuilderpost_call_transcription,post_call_audio)elevenlabs-signature: t=<ts>,v0=<hex>, HMAC-SHA256 over"{ts}.{body}"conversation_idVconConversationObservercollects turns from pipeline frames;pipecat-aiis imported lazily.python main.py pipecatserves/healthonlycore.tracker.StateTracker: a call is marked processed only when the POST succeeds, and a persisted watermark never moves past a call still waiting to send. The parked version marked calls processed on a failed POST and advanced the window regardless, so failed calls were never retriedEvery webhook adapter validates by default and refuses to start without its secret unless
ALLOW_UNSIGNED_WEBHOOKS=true. Every builder takes the lawful basis and media publisher from config, writesmediatype, and emits draft-04 attachments. All four are registered inmain.pyand the README config tables.Other changes
twilio_adapter/removed. Nothing imports it. Seven tests patchedtwilio_adapter.*paths, so their mocks never applied; they now patchadapters.twilio.*. Dockerfile and packaging updated.tests/e2e/freeswitch/: the FreeSWITCH end-to-end harness (mock conserver, ESL, dialplan and Lua configs, test scripts). Needs a live FreeSWITCH, so it is excluded from the default pytest run by its ownconftest.py. A machine-specific setup script was generalised.durationarrives as a numeric string; it is now converted to a number (found by schema validation).Tests
pip install -e ".[dev,s3]":ruff check .andblack --check .clean;pytest621 passed (539 on main).LAWFUL_BASIS=consent,MEDIA_BACKEND=filesystem) validates against the vCon working group schema.Not in this PR
Refs CON-703
🤖 Generated with Claude Code