Skip to content

feat(platforms): add Carbon Voice as a platform plugin - #43226

Open
cristianmgm7 wants to merge 7 commits into
NousResearch:mainfrom
cristianmgm7:carbonvoice-platform-pr
Open

feat(platforms): add Carbon Voice as a platform plugin#43226
cristianmgm7 wants to merge 7 commits into
NousResearch:mainfrom
cristianmgm7:carbonvoice-platform-pr

Conversation

@cristianmgm7

@cristianmgm7 cristianmgm7 commented Jun 10, 2026

Copy link
Copy Markdown

Summary

Adds Carbon Voice as a bundled platform plugin under plugins/platforms/carbonvoice/, following the same pattern as the other 21 platform plugins. Carbon Voice is a voice-first platform: it transcribes inbound voice to text (STT) before delivery and can re-synthesize the agent's replies as voice memos (TTS), so from Hermes' side it's a text-in / text-out platform. The adapter connects over Socket.IO (primary) with a REST polling fallback — no public webhook or tunnel required — and persists a disk cursor so messages received while offline are processed on next startup.

Rework per review: the previous revision of this PR integrated Carbon Voice as a native platform (enum branch, factory branch, auth maps, cron map, send-tool routing, toolset, wizard entries — 9 core files). As requested, it is now a platform plugin registered via ctx.register_platform(): env enablement, cron delivery, standalone sending, auth metadata, and prompt hints all come from the existing PlatformEntry hooks. The PR now contains zero core changes — the diff is the plugin subpackage, its tests, and docs.

Review feedback addressed

  1. voice_out_carries_text was read by the adapter but ignored by core (audio memo + duplicate text). The general delivery contract lives in fix(gateway): thread auto-TTS reply + support voice_out_carries_text adapters #32655 (fix(gateway): thread auto-TTS reply + support voice_out_carries_text adapters), which is further along in review and hardened per that review's feedback: a completeness guard (a truncated/stripped TTS rendition never suppresses the full text), reply_to threading for the voice memo, and behavioral tests. This PR only declares the flag on the adapter; until fix(gateway): thread auto-TTS reply + support voice_out_carries_text adapters #32655 lands the attribute is ignored and voice-out delivers audio plus a duplicate text bubble — degraded UX, never lost content. Keeping the mechanism in one PR avoids two competing implementations of the same base.py block.
  2. Voice-out configuration not wired. The plugin's env_enablement_fn seeds extra["voice_out"] from CARBONVOICE_VOICE_OUT, and the chain env → seed → PlatformConfig.extra → adapter flag is now covered by tests (tests/gateway/test_carbonvoice_plugin.py::TestVoiceOutWiring).
  3. No credential-scoped lock. connect() now acquires _acquire_platform_lock("carbonvoice-pat", …) before opening the API client (same pattern as the telegram/discord/slack/whatsapp adapters) and disconnect() releases it. Tested (deny → bail out before api.open(); release on disconnect).

What's included

  • plugins/platforms/carbonvoice/ — plugin subpackage (adapter, api, transport, state, dedupe, reactions, channels, audit, permits, parse, gate, conversations, constants) + plugin.yaml with requires_env/optional_env for the setup wizard.
  • register(ctx) provides: env_enablement_fn (auto-enable from CARBONVOICE_PAT), standalone_sender_fn (out-of-process cron delivery), cron_deliver_env_var="CARBONVOICE_HOME_CHANNEL", auth env vars, platform_hint, setup_fn.
  • Docs: website/docs/user-guide/messaging/carbonvoice.md + messaging index entries.
  • Tests: tests/gateway/test_carbonvoice_plugin.py (15 — plugin registration hooks, voice-out env wiring, credential lock).

Dependencies

None added. httpx is already core; python-socketio is optional — without it the adapter runs in polling-only mode (install_hint covers it). No pyproject.toml / uv.lock changes.

Access control

Deny-by-default: the bot answers only the owner (whoami.created_by, auto-detected at connect) plus CARBONVOICE_ALLOWED_USERS and runtime-approved users. Unknown senders are dropped silently; the owner gets a rate-limited prompt and can approve with a one-tap reaction (💯 allow / 👎 block) or /cv-allow-user. Approvals reuse the existing PairingStore, so they authorize in core too.

Testing

  • tests/gateway/test_carbonvoice_plugin.py (15) — all passing.
  • Regression: tests/plugins/ + startup no-eager-install (1855 tests) and the voice/telegram/drop-recovery suites (212 tests) pass; ruff clean.

@cristianmgm7
cristianmgm7 marked this pull request as ready for review June 10, 2026 02:24
@cristianmgm7
cristianmgm7 requested a review from a team June 10, 2026 02:24
@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have labels Jun 10, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the substantial Carbon Voice implementation. I found blocking integration issues before this can be safely salvaged.

Problems

  • gateway/platforms/carbonvoice/adapter.py:131 relies on voice_out_carries_text, but current gateway/platforms/base.py:5000-5027 suppresses follow-up text only for a successful Telegram caption. Carbon Voice voice-out therefore delivers both the audio memo and text.
  • adapter.py:164 reads only config.extra['voice_out']; the PR's config bridge does not seed that field, while website/docs/user-guide/messaging/carbonvoice.md tells users to set CARBONVOICE_VOICE_OUT=true.
  • adapter.py:289-360 has no credential-scoped lock. Current adapters use the base helpers at gateway/platforms/base.py:2752-2779 to prevent two profiles from claiming one credential.

Suggested changes

  • Please rework this as a platform plugin. gateway/platforms/ADDING_A_PLATFORM.md:5-15 recommends that route for community/third-party platforms, and gateway/platform_registry.py:113-159 already provides env enablement, cron delivery, standalone sending, auth metadata, and prompt hints without new core branches.
  • Wire and test voice-out configuration, then add a tested general delivery contract for Carbon Voice audio/text suppression.

Automated hermes-sweeper review.

# this are unaffected. Requires the patched base.py from PR 6 (and
# the parallel upstream PR) — without it the attribute is read but
# ignored, and we ship a duplicate text bubble.
voice_out_carries_text = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

voice_out_carries_text is not consumed by current main. BasePlatformAdapter suppresses the follow-up text only for a successful Telegram caption (gateway/platforms/base.py:5000-5027), so voice-out will produce both an audio memo and a text reply. Please add the general base-layer contract plus coverage, or remove this duplicate-suppression path.

# VOICE is a no-op (the gate's other conditions still fail).
# Default ``False`` to preserve text-out for existing
# deployments that haven't opted in.
self._voice_out: bool = bool(extra.get("voice_out"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads only config.extra['voice_out'], but the documented CARBONVOICE_VOICE_OUT=true is never bridged into extra by this PR's gateway/config.py changes. Wire the supported configuration path and add an env/config propagation test.


# ── Lifecycle ────────────────────────────────────────────────────────

async def connect(self) -> bool:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acquire a credential-scoped lock before opening the API/Socket.IO connection and release it in disconnect(). Current BasePlatformAdapter._acquire_platform_lock() / _release_platform_lock() provide the profile-safe mechanism used by token-backed adapters.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
cristianmgm7 and others added 5 commits July 15, 2026 18:09
Port of the external PhononX/hermes-plugin-carbonvoice plugin (v0.3.6)
into plugins/platforms/, following the bundled-plugin convention used by
the other 21 platform plugins. Registers via ctx.register_platform()
with env_enablement_fn, standalone_sender_fn, and
cron_deliver_env_var=CARBONVOICE_HOME_CHANNEL — zero core changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prevents two gateways from claiming the same Carbon Voice PAT, matching
the telegram/discord/slack/whatsapp adapters (_acquire_platform_lock in
connect, _release_platform_lock in disconnect). Addresses review
feedback on the PAT having no credential lock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a capability flag on BasePlatformAdapter: adapters whose successful
play_tts() already delivers the reply text to the user (e.g. Carbon
Voice's server-side transcript rendered inline with the voice memo)
declare voice_out_carries_text = True, and the auto-TTS response flow
suppresses the follow-up text send — no more duplicate audio + text.

- Default False: existing platforms keep sending audio and text.
- A failed play_tts still falls back to the text send (reply never lost).
- Telegram's length-conditional caption suppression is unchanged.
- Carbon Voice opts in (its /v5/messages/audio endpoint transcribes
  server-side, so the transcript IS the text).

Addresses review feedback: the flag was previously read by the adapter
but ignored by core, delivering both the audio memo and the text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers the three review items: register(ctx) → PlatformEntry hooks,
the CARBONVOICE_VOICE_OUT env → seed → adapter._voice_out chain, and
connect() bailing out when the credential lock is held (releasing it
on disconnect).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the setup guide from the native-platform branch, adapted to the
plugin reality: python-socketio is optional (polling-only without it,
no [messaging] extra change), and the voice-out note documents the
one-bubble transcript behavior. Adds Carbon Voice to the platform
comparison table and the setup-guide links in the messaging index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cristianmgm7
cristianmgm7 force-pushed the carbonvoice-platform-pr branch from 2c28548 to 547ace5 Compare July 15, 2026 23:40
@cristianmgm7 cristianmgm7 changed the title feat(platforms): add Carbon Voice as a native messaging platform feat(platforms): add Carbon Voice as a platform plugin Jul 15, 2026
cristianmgm7 added a commit to PhononX/hermes-plugin-carbonvoice that referenced this pull request Jul 15, 2026
Prevents two gateways from claiming the same Carbon Voice PAT, matching
the telegram/discord/slack/whatsapp adapters. hasattr-guarded: the
_acquire_platform_lock helper only exists on newer Hermes cores; older
cores skip the check. Also updates the voice_out_carries_text comment —
upstream honors the flag since NousResearch/hermes-agent#43226.

Mirror of the fix shipped in the upstream PR branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cristianmgm7 added a commit to PhononX/hermes-plugin-carbonvoice that referenced this pull request Jul 15, 2026
Core review on NousResearch/hermes-agent#43226 asked for the plugin
route; upstream moved all platforms to plugins/platforms/. Records the
sync-discipline status (PAT lock mirrored).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cristianmgm7

Copy link
Copy Markdown
Author

Thanks for the review — reworked as requested. The branch has been rebuilt from scratch on current main as a platform plugin (force-pushed; PR title/body updated to match).

Per-point:

  1. voice_out_carries_text ignored by core (audio + duplicate text) → added the general delivery contract: a documented voice_out_carries_text: bool = False capability flag on BasePlatformAdapter, honored in the auto-TTS flow next to the existing Telegram caption check. Default False (no behavior change for other platforms); a failed play_tts() still falls back to the text send. Covered by tests/gateway/test_voice_out_carries_text.py (suppression, default-unchanged, TTS-failure fallback). This is now the only core change in the PR (gateway/platforms/base.py, +21/−2).
  2. CARBONVOICE_VOICE_OUT not seeded → the plugin's env_enablement_fn seeds extra["voice_out"]; the env → seed → adapter chain is tested in tests/gateway/test_carbonvoice_plugin.py::TestVoiceOutWiring.
  3. No credential-scoped lockconnect() acquires _acquire_platform_lock("carbonvoice-pat", …) before opening the API client and disconnect() releases it, same as the telegram/discord/slack/whatsapp adapters. Tested (lock denied → bail before api.open()).

Plugin rework: everything lives in plugins/platforms/carbonvoice/ and registers via ctx.register_platform() with env_enablement_fn, standalone_sender_fn, cron_deliver_env_var, and the auth env vars — no enum/factory/authz/cron/send-tool/wizard branches in core anymore. No dependency changes either: python-socketio is optional (polling-only fallback without it).

Regression: tests/plugins/ + startup no-eager-install (1855) and the voice/telegram/drop-recovery suites (212) pass.

cristianmgm7 and others added 2 commits July 15, 2026 19:25
The gateway forwards is_reconnect to adapter.connect() on the retry /
reconnection path; the adapter (ported from the external plugin, which
targets older cores) didn't accept it, so every reconnect attempt died
with 'unexpected keyword argument'. Matches the signature of all other
platform plugins. No behavioral difference for Carbon Voice: the disk
cursor makes cold boot and reconnect identical.

Found by exercising the real gateway boot on this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…earch#32655

Drops this PR's own voice_out_carries_text implementation in base.py:
the same contract already exists in PR NousResearch#32655, where it is further
along in review and hardened per feedback (completeness guard so a
truncated TTS rendition never suppresses the full text, plus reply_to
threading for the voice memo and behavioral tests).

The plugin keeps declaring voice_out_carries_text = True; until NousResearch#32655
lands the attribute is simply ignored (voice-out delivers audio plus a
duplicate text bubble — degraded UX, never lost content). This PR is
now strictly zero core changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cristianmgm7

Copy link
Copy Markdown
Author

Follow-up: dropped this PR's own voice_out_carries_text implementation in base.py — the same delivery contract already exists in #32655, where it's further along in review and hardened per that feedback (completeness guard + reply_to threading + behavioral tests). Keeping the mechanism in one place avoids two competing implementations of the same block. This PR is now strictly zero core changes: the plugin subpackage, its tests, and docs. The adapter declares the flag; #32655 makes core honor it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants