Skip to content

feat(chat): Discord bidirectional driver with attested approvals#1809

Merged
chernistry merged 1 commit into
mainfrom
feat/1795-discord-driver-attested-approvals
May 21, 2026
Merged

feat(chat): Discord bidirectional driver with attested approvals#1809
chernistry merged 1 commit into
mainfrom
feat/1795-discord-driver-attested-approvals

Conversation

@chernistry

@chernistry chernistry commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #1795.

Implements the bidirectional Discord chat driver to match the Telegram and Slack drivers, plus a shared channel-scoped scheduler partition helper that both Slack and Discord consume.

  • DiscordBridge connects via the gateway, dispatches application-command interactions through on_command, decodes Approve/Reject component clicks whose custom_id encodes approve:<id> / reject:<id>, debounces per-message edits to one update per second, signs every outbound message with the install's Ed25519 keypair, and appends each approval resolution to the HMAC-chained audit log as a chat.discord.approval event covering (approver, interaction_id, decision, tool_call_hash, worktree_id, partition_id).
  • Approvals are pinned both to a worktree and to a channel-scoped scheduling partition: cross-worktree resolutions raise CrossWorktreeApprovalError, cross-partition resolutions raise ChannelPartitionMismatchError, and either failure emits a chat.discord.approval_rejected audit entry whose details record both sides of the mismatch.
  • New helper at bernstein.core.orchestration.scheduler_partitions owns the canonical <platform>:<channel_id> label, the alias table for operator-defined channel groupings, and the structured PartitionEvent audit payload. Designed for reuse across chat drivers.
  • Optional bernstein[discord] extra pulls in discord.py; start() raises DiscordDependencyError with an install pointer when the SDK is absent.

Files touched

  • src/bernstein/core/chat/drivers/discord.py (was a stub, now the full driver)
  • src/bernstein/core/orchestration/scheduler_partitions.py (new shared helper)
  • src/bernstein/core/chat/__init__.py (docstring refresh)
  • src/bernstein/cli/commands/chat_cmd.py (Discord token guard, drop unreachable NotImplementedError branch)
  • pyproject.toml (new discord extra)
  • docs/operations/chat-bridges.md (Discord setup, channel-partition fence, message verification)
  • CHANGELOG.md (Unreleased entry)
  • tests/unit/core/chat/test_discord_driver.py (new, 15 cases)
  • tests/integration/test_discord_audit_chain_roundtrip.py (new, end-to-end replay)
  • tests/unit/test_scheduler_partitions.py (new, partition helper)
  • tests/unit/test_chat_drivers.py (drop stub assertions superseded by the real driver test module)

Acceptance criteria

  • DiscordBridge implements BridgeProtocol end to end with no NotImplementedError remaining.
  • Slash command handlers registered via on_command dispatch from Discord interaction events.
  • push_approval renders a button row whose custom_id encodes approve:<id> / reject:<id> and routes through on_button without extra state.
  • Each Discord approval lands as a signed entry in the audit chain whose digest covers (approver, interaction_id, decision, tool_call_hash, prev_chain_digest); replay over the exported chain reproduces post-approval scheduler state byte-identically.
  • Channel-scoped scheduling fence: a Discord channel id maps to a scheduler partition; tasks emitted from channel #ops cannot land on workers attached to #dev, and the chain entry proves the partition was enforced.
  • edit_message debounces edits per thread under a documented interval to stay inside Discord rate limits (1s, matching Slack).
  • Optional dependency wired through bernstein[discord] extra; start() raises DiscordDependencyError when the SDK is absent.
  • Tests cover slash dispatch, button decode, edit-debounce, SDK-missing error path, audit-chain entry shape, and channel-partition enforcement.

Test plan

  • uv run pytest tests/unit/core/chat/test_discord_driver.py (15 cases)
  • uv run pytest tests/unit/test_scheduler_partitions.py (9 cases)
  • uv run pytest tests/integration/test_discord_audit_chain_roundtrip.py (end-to-end replay)
  • uv run pytest tests/unit/core/chat/ tests/unit/test_chat_drivers.py tests/unit/test_chat_session.py tests/unit/test_handoff_chat.py tests/integration/test_slack_audit_chain_roundtrip.py (regression sweep -- 59 cases green)
  • uv run ruff check . && uv run ruff format . clean
  • uv run pyright src/bernstein/core/chat/drivers/discord.py clean

Summary by CodeRabbit

Release Notes

  • New Features

    • Discord chat bridge is now production-ready with full setup documentation and environment configuration (BERNSTEIN_DISCORD_TOKEN).
    • Added message signing and verification for Discord communications.
    • Channel-scoped scheduling enforces partition-based access control for Discord channels.
  • Documentation

    • Comprehensive Discord setup guide added to chat bridges documentation.

Review Change Stack

Connects to Discord via the gateway, dispatches application-command
interactions through ``on_command``, decodes Approve/Reject component
clicks whose ``custom_id`` encodes ``approve:<id>`` / ``reject:<id>``,
debounces per-message edits to one update per second, signs every
outbound message with the install's Ed25519 keypair, and appends each
approval resolution to the HMAC-chained audit log as a
``chat.discord.approval`` event covering ``(approver, interaction_id,
decision, tool_call_hash, worktree_id, partition_id)``.

Approvals are pinned both to a worktree and to a channel-scoped
scheduling partition: cross-worktree resolutions raise
``CrossWorktreeApprovalError``, cross-partition resolutions raise
``ChannelPartitionMismatchError``, and either failure emits a
``chat.discord.approval_rejected`` audit entry whose details cover
both sides of the mismatch.

Adds a shared partition helper at
``bernstein.core.orchestration.scheduler_partitions`` (consumed by
both the new Discord driver and, going forward, the Slack driver) that
owns the canonical ``<platform>:<channel_id>`` label, the alias table
for operator-defined channel groupings, and the structured
``PartitionEvent`` audit payload.

Optional ``bernstein[discord]`` extra pulls in ``discord.py``;
``start()`` raises ``DiscordDependencyError`` with an install pointer
when the SDK is absent.

Closes #1795
@chernistry chernistry enabled auto-merge (squash) May 21, 2026 20:12

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @chernistry, you have reached your weekly rate limit of 2500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file core cli docs tests labels May 21, 2026
@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR implements a full bidirectional Discord driver with auditable approvals, replacing the previous stub. It introduces channel-scoped partition helpers for scheduler-side dispatch fencing, wires Discord gateway interactions into slash-command and button-callback handlers, enforces worktree pinning and channel-partition boundaries, signs outbound messages with Ed25519, and emits audit-chain entries for all approvals and rejections. Unit tests cover slash dispatch, component interaction decoding, edit debounce, message signing, audit shape, and partition enforcement; an integration test replays the audit chain to verify scheduler state reproducibility.

Changes

Discord Bidirectional Driver & Audit Chain

Layer / File(s) Summary
Channel-partition scheduler fence helpers
src/bernstein/core/orchestration/scheduler_partitions.py, tests/unit/test_scheduler_partitions.py
partition_id_for_channel() normalizes platform-channel pairs; PartitionEvent dataclass holds partition context for audit payloads; ChannelPartitionMap supports operator aliasing; PartitionViolationError carries expected/actual partition ids for enforcement.
Discord driver contract and initialization
src/bernstein/core/chat/drivers/discord.py
DiscordBridge.__init__ takes required token plus keyword config (install_id, session_id, worktree_id, audit_log, key_dir). Adds error types DiscordDependencyError, CrossWorktreeApprovalError, ChannelPartitionMismatchError; introduces PendingApprovalRecord for server-side approval tracking; exports accessors for approved hashes, partition assignments, public key, and last signed envelope.
Discord gateway lifecycle and interaction dispatch wiring
src/bernstein/core/chat/drivers/discord.py
start() loads optional Discord SDK, constructs client with intents, registers on_interaction gateway event handler, runs connection in background task. stop() cancels runner, flushes edit queue, closes client, cleans references.
Outbound messaging: send, edit with debounce, approval button UI
src/bernstein/core/chat/drivers/discord.py
send_message() resolves channel, builds and stores signed envelope, sends, returns handle. edit_message() implements per-route debounce with global lock, collapsing rapid updates into single throttled edit. push_approval() renders two-button row with approve:<id>/reject:<id> custom_ids, signs, sends with UI view, records handles. Edit-throttle and SDK interaction helpers support flush with fallback fetch.
Inbound interaction dispatch and command/component routing
src/bernstein/core/chat/drivers/discord.py
dispatch_interaction() coerces type and routes to slash-command or component handler. _dispatch_slash_command() parses options into args, acknowledges, calls registered handler. _dispatch_component() decodes custom_id into decision and approval_id, validates, computes request partition, enforces worktree pinning and channel-partition fence, acknowledges, records resolved approval, removes pending, invokes button handler.
Approval resolution and audit-chain bookkeeping
src/bernstein/core/chat/drivers/discord.py
_record_resolved_approval() updates approved hashes and partition mapping, emits chat.discord.approval audit entries with decision, tool_call_hash, interaction_id, worktree_id, partition_id, install/session ids when audit log configured. _record_rejected_approval() emits chat.discord.approval_rejected with reason and partition/worktree context.
Ed25519 message signing and signature verification
src/bernstein/core/chat/drivers/discord.py
_ensure_keypair() lazily loads or generates Ed25519 keypair, persists with restrictive permissions, keeps PEM bytes in memory. _build_signed_envelope() computes content_hash, canonical attestation bytes (deterministic JSON with version), signs, returns JSON dict. Public verify_chat_signature() validates PEM type, base64-decodes signature, recomputes attestation, verifies, returns bool without raising on malformed input. Optional Discord SDK import helpers raise DiscordDependencyError with install instructions on failure.
CLI token validation, documentation, and optional dependency wiring
src/bernstein/cli/commands/chat_cmd.py, src/bernstein/core/chat/__init__.py, docs/operations/chat-bridges.md, pyproject.toml
chat serve validates Discord token is non-empty, raises click.UsageError if missing; removes NotImplementedError exception catch so unimplemented paths propagate. Chat module docstring mentions Discord alongside Telegram/Slack. pyproject.toml adds discord = ["discord.py>=2.4"] optional extra. Docs mark Discord as Production, document setup, channel-partition fence semantics, signature verification instructions, and SDK dependency error path.
Unit tests for Discord driver: slash/button dispatch, signing, audit shape, partition enforcement
tests/unit/core/chat/test_discord_driver.py
Synthetic discord.py module injected into sys.modules to avoid real SDK dependency. Covers token validation rejection, slash-command routing with thread/user mapping, component interaction decoding for approve/reject buttons, approval button rendering (exactly two buttons), edit debounce collapse, outbound signature envelope capture and cross-install verification, audit-chain entry shape with chained digest, worktree-pinning rejection with CrossWorktreeApprovalError, channel-partition fence rejection with reason and partition ids, and JSON serialization of interaction helpers.
Integration test: Discord approval chain replay reproduces scheduler state
tests/integration/test_discord_audit_chain_roundtrip.py
End-to-end test with synthetic Discord surface. Starts DiscordBridge with AuditLog, registers pending approvals across two partitions, dispatches component interactions for approve/reject, captures live approved hashes and partition assignments, verifies on-disk audit HMAC chain, reconstructs approval state from exported JSONL decision entries, asserts reconstructed set and partition mapping match live scheduler state byte-identically.
Remove Discord stub tests from legacy test module
tests/unit/test_chat_drivers.py
Removes DiscordBridge and DISCORD_STUB_MESSAGE imports, deletes three stub test functions (test_discord_start_raises_stub_message, test_discord_on_command_raises_stub_message, test_discord_on_button_raises_stub_message) and _async_noop helper. Updates module docstring to reference Discord tests in dedicated module.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Bot
    participant AuditLog
    participant Handler
    
    User->>Bot: component interaction (approve)
    Bot->>Bot: decode custom_id & validate
    Bot->>Bot: enforce partition match
    alt partition mismatch
        Bot->>AuditLog: emit approval_rejected
        Bot->>User: acknowledge
    else success
        Bot->>AuditLog: emit approval entry
        Bot->>Handler: call on_button
        Bot->>User: acknowledge
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • sipyourdrink-ltd/bernstein#1795: Discord bidirectional driver feature request with attested approvals, channel-partition fencing, and audit-chain reproducibility—this PR fully implements the specified objectives.

Possibly related PRs

  • sipyourdrink-ltd/bernstein#1751: Prior stub-removal or Discord driver stub test cleanup that this PR supersedes with the full bidirectional implementation.

Suggested labels

size/xl, core, cli, docs, tests

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title concisely summarizes the main change: implementing a bidirectional Discord driver with attested approvals, matching the PR's core contribution.
Description check ✅ Passed Description is comprehensive with clear sections (Summary, Files touched, Acceptance criteria, Test plan), links the closed issue, and provides sufficient context for reviewers.
Linked Issues check ✅ Passed All coding requirements from issue #1795 are addressed: bidirectional driver, auditable approvals with signed audit-chain entries, channel-partition enforcement, reusable partition helper, and tests/documentation updates.
Out of Scope Changes check ✅ Passed All changes directly support the objectives in #1795: Discord driver implementation, partition helper, CLI guard, docs, packaging, tests. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1795-discord-driver-attested-approvals

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

Review-bot acknowledgement summary

  • Must-address findings: 0 (0 acknowledged, 0 open)
  • Informational findings: 0

All must-address findings are resolved or acknowledged.

@github-actions

Copy link
Copy Markdown
Contributor

Sonar insights (advisory, no merge-block)

Snapshot of bernstein on the configured Sonar instance:

Metric Value
Coverage 13.5
Code smells 125
Bugs 11
Vulnerabilities 2
Security hotspots 87

Run bernstein doctor sonar locally for the full surface.

This comment is a soft signal. The Sonar scan runs on push to main; the PR check itself never fails on smells.

@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ❌ 1 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 1 package(s) with unknown licenses.
See the Details below.

License Issues

uv.lock

PackageVersionLicenseIssue Type
audioop-lts0.2.2Python-2.0 AND Python-2.0 AND BSD-3-Clause AND Python-2.0 AND BSD-3-Clause AND 0BSDIncompatible License
discord-py2.7.1NullUnknown License
Allowed Licenses: Apache-2.0, MIT, BSD-2-Clause, BSD-3-Clause, ISC, MPL-2.0, Python-2.0, Unlicense, CC0-1.0
Excluded from license check: pkg:githubactions/SonarSource/sonarqube-scan-action

OpenSSF Scorecard

PackageVersionScoreDetails
pip/audioop-lts 0.2.2 UnknownUnknown
pip/discord-py 2.7.1 UnknownUnknown

Scanned Files

  • uv.lock

@chernistry chernistry merged commit 4a8e40a into main May 21, 2026
35 of 38 checks passed
@chernistry chernistry deleted the feat/1795-discord-driver-attested-approvals branch May 21, 2026 20:15
@github-actions

Copy link
Copy Markdown
Contributor

bernstein doctor observe for PR #1809 (feat/1795-discord-driver-attested-approvals): ok=0, warn=2, fail=0, error=0, skipped=2

sonar -- WARN (project bernstein)

metric value delta threshold status
coverage_pct 13.5% new 80.0% fail
code_smells 125 new 50 warn
bugs 11 new 0 fail
vulnerabilities 2 new 0 warn
security_hotspots 87 new 0 fail

code-scanning -- WARN (15 open alert(s))

metric value delta threshold status
open_alerts 15 new 0 fail
critical_alerts 0 new 0 ok
high_alerts 2 new 0 warn
medium_alerts 0 new - ok
low_alerts 0 new - ok
Skipped backends (credentials not configured)
  • glitchtip: BERNSTEIN_GLITCHTIP_TOKEN not set
  • dt: DTRACK_URL/TOKEN/PROJECT not set

See docs/observability/unified-doctor.md for backend setup notes.

@github-actions

Copy link
Copy Markdown
Contributor

Contract drift detected - proposed patch

Inline autofix push failed (failure). Apply the patch below manually.

Three contract tests act as drift detectors against the public CLI / API surface:

  • tests/unit/test_readme_api_coverage.py::test_all_cli_commands_are_documented
  • tests/unit/test_api_v1_routing.py::TestVersionedRoutesParity::test_every_root_route_has_v1_counterpart
  • tests/unit/test_cli_run_params.py::test_run_params_match_cli_call

One or more failed on this PR. scripts/regen_contract_drift.py produced the patch below (2 LOC, cap: 30).

Files changed:

tests/unit/test_readme_api_coverage.py

How to apply

Either run the regen script locally:

uv run python scripts/regen_contract_drift.py --fixture all
git add -A && git commit -m "chore(ci): regenerate contract drift allow-lists"
git push

Or apply the patch directly:

gh pr checkout 1809
git apply <<'PATCH'
diff --git a/tests/unit/test_readme_api_coverage.py b/tests/unit/test_readme_api_coverage.py
index f492845b..8151f2de 100644
--- a/tests/unit/test_readme_api_coverage.py
+++ b/tests/unit/test_readme_api_coverage.py
@@ -237,6 +237,8 @@ DOCUMENTED_COMMANDS: frozenset[str] = frozenset(
         "interop",
         # Bot-added: drift autofix (regen_contract_drift.py)
         "desktop-register",
+        # Bot-added: drift autofix (regen_contract_drift.py)
+        "schedule",
     }
 )
PATCH
git add -A && git commit -m "chore(ci): regenerate contract drift allow-lists"
git push
Full diff
diff --git a/tests/unit/test_readme_api_coverage.py b/tests/unit/test_readme_api_coverage.py
index f492845b..8151f2de 100644
--- a/tests/unit/test_readme_api_coverage.py
+++ b/tests/unit/test_readme_api_coverage.py
@@ -237,6 +237,8 @@ DOCUMENTED_COMMANDS: frozenset[str] = frozenset(
         "interop",
         # Bot-added: drift autofix (regen_contract_drift.py)
         "desktop-register",
+        # Bot-added: drift autofix (regen_contract_drift.py)
+        "schedule",
     }
 )

Source CI run: https://github.com/sipyourdrink-ltd/bernstein/actions/runs/26250594928

Refs #1273.


def approved_tool_call_hashes(self) -> set[str]:
"""Return a snapshot of every tool-call hash that has been approved."""
return set(self._approved_tool_call_hashes)

def partition_assignments(self) -> dict[str, str]:
"""Return ``tool_call_hash -> partition_id`` for every approved call."""
return dict(self._approved_partition_for)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli core dependencies Pull requests that update a dependency file docs size/xl tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discord bidirectional driver with attested approvals

2 participants