Skip to content

feat(trogon-gateway): consolidate sources into unified binary#104

Merged
yordis merged 1 commit intomainfrom
yordis/unified-gateway
Apr 8, 2026
Merged

feat(trogon-gateway): consolidate sources into unified binary#104
yordis merged 1 commit intomainfrom
yordis/unified-gateway

Conversation

@yordis
Copy link
Copy Markdown
Member

@yordis yordis commented Apr 6, 2026

  • Reduce operational overhead of building, deploying, and managing 6 separate source binaries
  • Single binary with TOML config + automatic env var layering eliminates per-source boilerplate
  • Source library crates remain decoupled; only the entrypoint is unified

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 6, 2026

PR Summary

High Risk
High risk because it replaces multiple independently deployed source services with a single trogon-gateway process and new config/env surface area, which can break webhook ingestion and stream provisioning for all sources if misconfigured.

Overview
Introduces a new trogon-gateway crate and Docker Compose service that runs all configured sources behind one HTTP server, exposes /-/liveness + /-/readiness, provisions JetStream streams, and optionally runs Discord in gateway (WebSocket) mode in-process.

Updates local dev infrastructure (compose.yml, .env.example, ngrok) to route all webhooks through the gateway and standardizes env vars under TROGON_SOURCE_<SOURCE>_* plus TROGON_GATEWAY_PORT, removing the per-source compose services and their docs/build artifacts.

Refactors source crates (e.g., GitHub/Discord/GitLab) toward being pure libraries (removes [[bin]] + main.rs, drops per-source health endpoints/serve wrappers, moves env parsing to gateway config, and introduces typed/redacted secret wrappers + StreamMaxAge/NonZeroDuration-based settings).

Improves telemetry shutdown semantics by returning/propagating errors from acp_telemetry::shutdown_otel() and adds an error source() chain for ClaimResolveError.

Reviewed by Cursor Bugbot for commit 407878d. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The PR introduces a unified trogon-gateway service that consolidates platform webhooks (GitHub, Discord, GitLab, Linear, Slack, Telegram) into a single HTTP server with NATS JetStream claim-check-based payload offloading. New trogon-source crates for Discord and Telegram, enhanced object-store integration, refactored sources to use claim-check publishers, Docker Compose consolidation, and standardized environment variables across platforms.

Changes

Cohort / File(s) Summary
Gateway Service
rsworkspace/crates/trogon-gateway/Cargo.toml, rsworkspace/crates/trogon-gateway/src/main.rs, rsworkspace/crates/trogon-gateway/src/cli.rs, rsworkspace/crates/trogon-gateway/src/config.rs, rsworkspace/crates/trogon-gateway/src/serve.rs
New unified gateway binary with CLI parsing, configuration loading from environment/file, NATS connection setup, JetStream stream provisioning, and HTTP routing for all enabled source providers with health endpoint.
Discord Source
rsworkspace/crates/trogon-source-discord/Cargo.toml, rsworkspace/crates/trogon-source-discord/src/config.rs, rsworkspace/crates/trogon-source-discord/src/constants.rs, rsworkspace/crates/trogon-source-discord/src/signature.rs, rsworkspace/crates/trogon-source-discord/src/server.rs, rsworkspace/crates/trogon-source-discord/src/gateway.rs, rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs, rsworkspace/crates/trogon-source-discord/src/lib.rs
New Discord webhook receiver crate supporting both gateway (WebSocket) and webhook (HTTP POST) modes with ed25519 signature verification, NATS publishing, and extensive configuration/error handling.
Telegram Source
rsworkspace/crates/trogon-source-telegram/Cargo.toml, rsworkspace/crates/trogon-source-telegram/src/config.rs, rsworkspace/crates/trogon-source-telegram/src/constants.rs, rsworkspace/crates/trogon-source-telegram/src/signature.rs, rsworkspace/crates/trogon-source-telegram/src/server.rs, rsworkspace/crates/trogon-source-telegram/src/lib.rs
New Telegram webhook receiver crate with SHA256-based HMAC signature validation, update type detection, NATS publishing with deduplication headers, and configurable request body size limits.
GitLab Source
rsworkspace/crates/trogon-source-gitlab/Cargo.toml, rsworkspace/crates/trogon-source-gitlab/src/config.rs, rsworkspace/crates/trogon-source-gitlab/src/constants.rs, rsworkspace/crates/trogon-source-gitlab/src/signature.rs, rsworkspace/crates/trogon-source-gitlab/src/webhook_secret.rs, rsworkspace/crates/trogon-source-gitlab/src/server.rs, rsworkspace/crates/trogon-source-gitlab/src/lib.rs
New GitLab webhook receiver crate with token-based signature verification, event normalization, NATS publishing with optional idempotency key, unroutable event DLQ, and redacted secret storage.
NATS Claim-Check & Object Store
rsworkspace/crates/trogon-nats/Cargo.toml, rsworkspace/crates/trogon-nats/src/jetstream/claim_check.rs, rsworkspace/crates/trogon-nats/src/jetstream/object_store.rs, rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs, rsworkspace/crates/trogon-nats/src/jetstream/mod.rs, rsworkspace/crates/trogon-nats/src/jetstream/publish.rs
New claim-check pattern for payload offloading: MaxPayload threshold calculation, ClaimCheckPublisher wrapper, object-store abstraction traits (ObjectStorePut, ObjectStoreGet), NATS JetStream implementation, MockObjectStore for testing, and updated publish outcomes.
GitHub Source Updates
rsworkspace/crates/trogon-source-github/Cargo.toml, rsworkspace/crates/trogon-source-github/src/config.rs, rsworkspace/crates/trogon-source-github/src/constants.rs, rsworkspace/crates/trogon-source-github/src/server.rs, rsworkspace/crates/trogon-source-github/src/main.rs
Removed standalone binary entrypoint, eliminated per-source max_body_size config, introduced HttpBodySizeMax constant, refactored server to use ClaimCheckPublisher instead of raw JetStream publisher, and updated body-limiting middleware.
Linear Source Updates
rsworkspace/crates/trogon-source-linear/Cargo.toml, rsworkspace/crates/trogon-source-linear/src/lib.rs, rsworkspace/crates/trogon-source-linear/src/constants.rs, rsworkspace/crates/trogon-source-linear/src/server.rs, rsworkspace/crates/trogon-source-linear/src/main.rs
Removed standalone binary entrypoint, added HTTP_BODY_SIZE_MAX and reject-reason constants, refactored server to use ClaimCheckPublisher, introduced unroutable event publishing with reject headers, and updated body-limiting middleware.
Slack Source Updates
rsworkspace/crates/trogon-source-slack/Cargo.toml, rsworkspace/crates/trogon-source-slack/src/config.rs, rsworkspace/crates/trogon-source-slack/src/constants.rs, rsworkspace/crates/trogon-source-slack/src/server.rs, rsworkspace/crates/trogon-source-slack/src/main.rs
Removed standalone binary entrypoint and per-source max_body_size config, introduced HTTP_BODY_SIZE_MAX constant, refactored server to use ClaimCheckPublisher, and replaced tower_http body-limiting with Axum DefaultBodyLimit.
Standard Library Updates
rsworkspace/crates/trogon-std/Cargo.toml, rsworkspace/crates/trogon-std/src/http.rs, rsworkspace/crates/trogon-std/src/lib.rs, rsworkspace/crates/trogon-std/src/time/system.rs
Added bytesize dependency, new http module with HttpBodySizeMax wrapper type and ByteSize re-export, module re-exports in lib.rs, and system clock test coverage.
Telemetry & Infrastructure
rsworkspace/crates/acp-telemetry/src/service_name.rs, rsworkspace/crates/acp-nats-ws/src/main.rs, rsworkspace/crates/AGENTS.md
Extended ServiceName enum with TrogonGateway, TrogonSourceDiscord, TrogonSourceGitlab, TrogonSourceTelegram; bounded connection thread drain in NATS WebSocket; added error typing and production trait implementation rules to engineering guidance.
Docker & Environment
devops/docker/compose/.env.example, devops/docker/compose/compose.yml, devops/docker/compose/services/trogon-gateway/Dockerfile, devops/docker/compose/services/trogon-gateway/README.md, devops/docker/compose/services/trogon-source-github/README.md, devops/docker/compose/services/trogon-source-linear/..., devops/docker/compose/services/trogon-source-slack/...
Consolidated Docker services from per-source containers to unified trogon-gateway, standardized environment variables to TROGON_SOURCE_<SOURCE>_* prefix scheme, introduced TROGON_GATEWAY_PORT, added gateway README with setup instructions, and removed obsolete per-source Dockerfiles and documentation.
Workspace Configuration
rsworkspace/Cargo.toml
Added internal path dependencies for all trogon-source crates, introduced confique with YAML support and pinned serde_yaml, added Discord-specific twilight-gateway and twilight-model dependencies with rustls configuration.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Webhook Client
    participant Gateway as Trogon Gateway
    participant JetStream as NATS JetStream
    participant ObjectStore as Object Store (S3/FilePath)
    
    Client->>Gateway: POST /discord/webhook (payload)
    activate Gateway
    Gateway->>Gateway: Verify ed25519 signature
    Gateway->>Gateway: Parse & validate interaction
    
    alt Payload size <= Threshold
        Gateway->>JetStream: publish_event(subject, headers, payload)
        JetStream-->>Gateway: ack
    else Payload size > Threshold
        Gateway->>ObjectStore: put(key, payload_bytes)
        ObjectStore-->>Gateway: ok
        Gateway->>JetStream: publish_event(subject, headers, claim_check)
        JetStream-->>Gateway: ack
    end
    
    deactivate Gateway
    Gateway-->>Client: 200 OK
    
    Note over JetStream,ObjectStore: Claim Check Pattern:<br/>Large payloads stored in Object Store,<br/>small claim metadata published to JetStream
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #100: Adds the same claim-check JetStream implementation (MaxPayload, ClaimCheckPublisher, object-store abstractions) and refactors all source crates to use ClaimCheckPublisher instead of raw publishers.
  • PR #99: Introduces the GitLab source crate with configuration, server, and webhook secret handling.
  • PR #98: Adds the Discord source crate with gateway/webhook mode support and ed25519 signature verification.

Suggested labels

rust:coverage-baseline-reset

Poem

🐰 Hoppy whiskers twitching with glee,
A gateway unified, now serves all three,
Discord, Telegram, GitLab too—
Claim checks offload, old webhooks are through!
⚡ Unified inbound, NATS flows with ease,
One hop forward, the sources all please!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yordis/unified-gateway

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
rsworkspace/crates/trogon-gateway/src/config.rs (2)

400-406: Uses {e:?} (Debug) instead of {e} (Display) for NatsToken errors.

Lines 403 and 413 format NatsToken validation errors using Debug ({e:?}) rather than Display ({e}). If the error type implements Display, prefer it for user-facing messages. If it doesn't, this is a sign the error type needs a Display impl.

-            errors.push(format!("discord: invalid subject_prefix: {e:?}"));
+            errors.push(format!("discord: invalid subject_prefix: {e}"));
...
-            errors.push(format!("discord: invalid stream_name: {e:?}"));
+            errors.push(format!("discord: invalid stream_name: {e}"));

This pattern repeats for slack (464, 474), gitlab (575, 585), and linear (634, 644).

Also applies to: 410-416

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-gateway/src/config.rs` around lines 400 - 406, The
error messages use debug formatting `{e:?}` for NatsToken validation which
should use display formatting `{e}` (or ensure the error type implements
Display); update the match arms where NatsToken::new(subject_prefix) is handled
(and the equivalent blocks for slack, gitlab, and linear validations) to log
errors with `{e}` instead of `{e:?}` and push messages into the errors vector
using the Display representation, e.g. in the subject_prefix handling, change
the error push and return in the Err(e) branch to use `{e}`; repeat the same
swap for the corresponding variables and functions handling slack, gitlab, and
linear token/field validation so all user-facing messages use Display
formatting.

11-17: ConfigError::Validation uses Vec<String> instead of typed errors.

Per coding guidelines, errors should be typed. The Validation variant collects error messages as strings, discarding the original error types from parsing failures (e.g., NatsToken validation, parse_intents, parse_public_key).

This design enables reporting multiple validation errors at once, which is valuable UX. A typed alternative could use:

Typed validation error approach
#[derive(Debug)]
pub enum ValidationError {
    MissingField { source: &'static str, field: &'static str },
    InvalidSubjectPrefix { source: &'static str, value: String },
    InvalidStreamName { source: &'static str, value: String },
    InvalidDiscordMode { value: String },
    InvalidDiscordIntents { source: trogon_source_discord::config::UnknownIntentError },
    InvalidDiscordPublicKey { source: String }, // or wrap the actual error type
    InvalidGitlabWebhookSecret { source: String },
    // ... etc
}

pub enum ConfigError {
    ReadFile(std::io::Error),
    Interpolation(interpolation::UndefinedVarError),
    Yaml(serde_yaml::Error),
    Validation(Vec<ValidationError>),
}

This preserves type information while still supporting multiple-error reporting. As per coding guidelines: "Errors must be typed—use structs or enums, never String or format!()."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-gateway/src/config.rs` around lines 11 - 17, The
ConfigError::Validation variant currently uses Vec<String>; replace it with a
typed Vec<ValidationError> and introduce a new ValidationError enum/structs to
capture concrete error kinds (e.g., MissingField { source: &'static str, field:
&'static str }, InvalidSubjectPrefix { source: &'static str, value: String },
InvalidDiscordIntents { source:
trogon_source_discord::config::UnknownIntentError }, InvalidDiscordPublicKey {
source: /* wrap parse_public_key error type */ }, etc.) so callers can aggregate
multiple typed validation failures; update the ConfigError definition to pub
enum ConfigError { ReadFile(std::io::Error),
Interpolation(interpolation::UndefinedVarError), Yaml(serde_yaml::Error),
Validation(Vec<ValidationError>), } and adapt creation sites (NatsToken
validation, parse_intents, parse_public_key) to emit the corresponding
ValidationError variants instead of formatted Strings.
rsworkspace/crates/trogon-gateway/src/serve.rs (1)

18-18: Error type uses String instead of typed errors.

Per coding guidelines, errors should be typed structs/enums rather than String. The SourceResult alias and all run_* functions convert typed errors to strings via format!(), discarding the original error's type information.

Since these errors are only logged (not propagated to callers), this is low-impact, but it sets a precedent that could spread. Consider introducing a simple enum:

Proposed typed error approach
enum SourceError {
    Provision(Box<dyn std::error::Error + Send + Sync>),
    Serve(Box<dyn std::error::Error + Send + Sync>),
}

impl fmt::Display for SourceError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Provision(e) => write!(f, "provision failed: {e}"),
            Self::Serve(e) => write!(f, "serve failed: {e}"),
        }
    }
}

type SourceResult = (&'static str, Result<(), SourceError>);

This preserves the error chain for debugging while satisfying the guideline. As per coding guidelines: "Errors must be typed—use structs or enums, never String or format!()."

Also applies to: 158-165

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-gateway/src/serve.rs` at line 18, Replace the
current SourceResult alias that uses Result<(), String> with a typed error enum
(e.g., SourceError) and change SourceResult to type SourceResult = (&'static
str, Result<(), SourceError>); implement Display for SourceError to format
messages but preserve the underlying error via variants like Provision(Box<dyn
std::error::Error + Send + Sync>) and Serve(Box<dyn std::error::Error + Send +
Sync>); then update all run_* functions (e.g., run_provision, run_serve) to
return SourceError by boxing their original errors into the appropriate variant
instead of calling format!() and String::from, so logs still get readable
messages while preserving typed error information.
rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs (1)

47-61: Gateway loop handles errors but doesn't distinguish recoverable vs fatal conditions.

The loop correctly handles Message::Close and stream termination, but on Some(Err(source)) it only logs a warning and continues. Depending on the error type (e.g., authentication failure vs transient network hiccup), some errors should trigger a reconnect or an exit.

This is acceptable for an initial implementation, but consider adding error classification in a follow-up to avoid infinite warn loops on persistent failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs` around lines
47 - 61, The gateway loop currently treats all errors from poll_next the same;
update the Some(Err(source)) arm to classify the error (using the error's
type/variants from the websocket/shard poll result) and take appropriate action:
for fatal/authentication errors (e.g., token invalid, protocol errors) log
details and break/return to trigger a full reconnect or shutdown, for transient
errors (e.g., IO/timeouts) perform a retry with backoff and continue, and for
unknown cases escalate to warn with context; modify the match arm that handles
Some(Err(source)) (referencing poll_fn, shard, and bridge.dispatch) to implement
this classification and invoke the reconnect/shutdown path or retry/backoff
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rsworkspace/crates/trogon-gateway/src/config.rs`:
- Around line 310-327: The GithubConfig (and similarly DiscordConfig)
initialization currently sets nats: NatsConfig::from_env(env) even though
sources' serve functions get NATS infra separately; remove the unused assignment
to NatsConfig::from_env(env) from the resolve_github (and resolve_discord)
config construction so the nats field is no longer populated there, leaving the
rest of the GithubConfig initialization (webhook_secret, port, subject_prefix,
stream_name, stream_max_age, nats_ack_timeout) intact and ensuring code compiles
without referencing NatsConfig::from_env in those functions.
- Around line 521-537: The TelegramSourceConfig currently uses plain String for
subject_prefix and stream_name; change the struct fields in
trogon-source-telegram::config::TelegramSourceConfig to use NatsToken (the same
domain type used by Slack/GitLab/Linear) and update
TelegramSourceConfig::from_env to parse/validate the env/or_env! results into
NatsToken instances (use the same NatsToken parsing/constructor helper the other
sources use and apply the default strings by converting them into NatsToken
values). Also update the gateway resolver code that builds
Some(trogon_source_telegram::TelegramSourceConfig { ... }) to pass the validated
NatsToken values instead of raw Strings so invalid NATS tokens fail fast during
config construction.

In `@rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs`:
- Around line 33-41: The spawned health-server task currently panics on bind
failure via TcpListener::bind(...).await.expect(...) which silently aborts the
task; change this to propagate or handle the error: remove the expect and
propagate the Err back to the caller by making run(...) return Result (match the
webhook mode's serve signature), have the spawned block return a Result and
bubble that up so callers (update serve.rs to accept && handle the Result from
gateway_runner::run) can react to bind failures; if you prefer a smaller change,
replace expect with an explicit match that logs an error via error!/fatal!
including health_addr and the bind error and then cleanly exits the task instead
of panicking. Ensure you update references to the tokio::spawn block,
TcpListener::bind, health_addr and the run function to reflect the new
Result-based flow.

---

Nitpick comments:
In `@rsworkspace/crates/trogon-gateway/src/config.rs`:
- Around line 400-406: The error messages use debug formatting `{e:?}` for
NatsToken validation which should use display formatting `{e}` (or ensure the
error type implements Display); update the match arms where
NatsToken::new(subject_prefix) is handled (and the equivalent blocks for slack,
gitlab, and linear validations) to log errors with `{e}` instead of `{e:?}` and
push messages into the errors vector using the Display representation, e.g. in
the subject_prefix handling, change the error push and return in the Err(e)
branch to use `{e}`; repeat the same swap for the corresponding variables and
functions handling slack, gitlab, and linear token/field validation so all
user-facing messages use Display formatting.
- Around line 11-17: The ConfigError::Validation variant currently uses
Vec<String>; replace it with a typed Vec<ValidationError> and introduce a new
ValidationError enum/structs to capture concrete error kinds (e.g., MissingField
{ source: &'static str, field: &'static str }, InvalidSubjectPrefix { source:
&'static str, value: String }, InvalidDiscordIntents { source:
trogon_source_discord::config::UnknownIntentError }, InvalidDiscordPublicKey {
source: /* wrap parse_public_key error type */ }, etc.) so callers can aggregate
multiple typed validation failures; update the ConfigError definition to pub
enum ConfigError { ReadFile(std::io::Error),
Interpolation(interpolation::UndefinedVarError), Yaml(serde_yaml::Error),
Validation(Vec<ValidationError>), } and adapt creation sites (NatsToken
validation, parse_intents, parse_public_key) to emit the corresponding
ValidationError variants instead of formatted Strings.

In `@rsworkspace/crates/trogon-gateway/src/serve.rs`:
- Line 18: Replace the current SourceResult alias that uses Result<(), String>
with a typed error enum (e.g., SourceError) and change SourceResult to type
SourceResult = (&'static str, Result<(), SourceError>); implement Display for
SourceError to format messages but preserve the underlying error via variants
like Provision(Box<dyn std::error::Error + Send + Sync>) and Serve(Box<dyn
std::error::Error + Send + Sync>); then update all run_* functions (e.g.,
run_provision, run_serve) to return SourceError by boxing their original errors
into the appropriate variant instead of calling format!() and String::from, so
logs still get readable messages while preserving typed error information.

In `@rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs`:
- Around line 47-61: The gateway loop currently treats all errors from poll_next
the same; update the Some(Err(source)) arm to classify the error (using the
error's type/variants from the websocket/shard poll result) and take appropriate
action: for fatal/authentication errors (e.g., token invalid, protocol errors)
log details and break/return to trigger a full reconnect or shutdown, for
transient errors (e.g., IO/timeouts) perform a retry with backoff and continue,
and for unknown cases escalate to warn with context; modify the match arm that
handles Some(Err(source)) (referencing poll_fn, shard, and bridge.dispatch) to
implement this classification and invoke the reconnect/shutdown path or
retry/backoff accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bb7dd4f6-5be3-473e-933d-ef018fabd6e0

📥 Commits

Reviewing files that changed from the base of the PR and between 8bd6020 and c3bce29.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • rsworkspace/Cargo.toml
  • rsworkspace/crates/acp-telemetry/src/service_name.rs
  • rsworkspace/crates/trogon-gateway/Cargo.toml
  • rsworkspace/crates/trogon-gateway/src/cli.rs
  • rsworkspace/crates/trogon-gateway/src/config.rs
  • rsworkspace/crates/trogon-gateway/src/interpolation.rs
  • rsworkspace/crates/trogon-gateway/src/main.rs
  • rsworkspace/crates/trogon-gateway/src/serve.rs
  • rsworkspace/crates/trogon-source-discord/Cargo.toml
  • rsworkspace/crates/trogon-source-discord/src/config.rs
  • rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs
  • rsworkspace/crates/trogon-source-discord/src/lib.rs
  • rsworkspace/crates/trogon-source-discord/src/main.rs
  • rsworkspace/crates/trogon-source-github/Cargo.toml
  • rsworkspace/crates/trogon-source-github/src/main.rs
  • rsworkspace/crates/trogon-source-gitlab/Cargo.toml
  • rsworkspace/crates/trogon-source-gitlab/src/main.rs
  • rsworkspace/crates/trogon-source-linear/Cargo.toml
  • rsworkspace/crates/trogon-source-linear/src/lib.rs
  • rsworkspace/crates/trogon-source-linear/src/main.rs
  • rsworkspace/crates/trogon-source-slack/Cargo.toml
  • rsworkspace/crates/trogon-source-slack/src/main.rs
  • rsworkspace/crates/trogon-source-telegram/src/main.rs
💤 Files with no reviewable changes (11)
  • rsworkspace/crates/trogon-source-github/Cargo.toml
  • rsworkspace/crates/trogon-source-linear/Cargo.toml
  • rsworkspace/crates/trogon-source-slack/Cargo.toml
  • rsworkspace/crates/trogon-source-gitlab/Cargo.toml
  • rsworkspace/crates/trogon-source-gitlab/src/main.rs
  • rsworkspace/crates/trogon-source-telegram/src/main.rs
  • rsworkspace/crates/trogon-source-slack/src/main.rs
  • rsworkspace/crates/trogon-source-discord/src/main.rs
  • rsworkspace/crates/trogon-source-linear/src/main.rs
  • rsworkspace/crates/trogon-source-github/src/main.rs
  • rsworkspace/crates/trogon-source-discord/Cargo.toml

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
rsworkspace/crates/trogon-gateway/src/config.rs (1)

278-286: ⚠️ Potential issue | 🟠 Major

Change subject_prefix and stream_name from String to NatsToken to match Discord, Slack, and Linear resolvers and comply with the domain-specific value objects guideline.

GithubConfig passes raw strings for subject_prefix and stream_name, while Discord, Slack, and Linear configs use NatsToken. This inconsistency bypasses validation at construction and violates the coding guideline to "Prefer domain-specific value objects over primitives."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-gateway/src/config.rs` around lines 278 - 286, The
GithubConfig construction is passing raw strings for subject_prefix and
stream_name; change those fields and their usages to use the NatsToken value
object instead of String (update trogon_source_github::GithubConfig definition
to use NatsToken for subject_prefix and stream_name), and when building the
config in this function convert/validate the strings from section (e.g. with
NatsToken::try_from or NatsToken::new) and propagate or handle any validation
error; also update any downstream call sites that expect String to accept
NatsToken so the domain-level validation is enforced at construction.
🧹 Nitpick comments (1)
devops/docker/compose/compose.yml (1)

75-78: Consider adding a healthcheck for the unified gateway.

The previous per-source services had healthchecks. Without one, Docker/orchestrators can't verify the gateway is actually serving traffic (beyond process-running). Each source exposes /health on its respective port.

Example healthcheck targeting one source port
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8081/health"]
      interval: 10s
      timeout: 5s
      start_period: 15s
      retries: 3

Alternatively, check multiple ports or add a dedicated gateway-level health endpoint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devops/docker/compose/compose.yml` around lines 75 - 78, Add a Docker
healthcheck block for the unified gateway service in compose.yml so
orchestrators can verify it is actually serving traffic (not just running);
insert a healthcheck key under the gateway service (alongside depends_on and
restart) that uses a CMD curl -f against one of the source /health endpoints
(e.g., http://localhost:8081/health) and configure interval, timeout,
start_period and retries (e.g., 10s, 5s, 15s, 3) or expand to probe multiple
ports or a dedicated gateway-level /health as appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rsworkspace/crates/trogon-gateway/src/serve.rs`:
- Around line 184-198: The gateway path currently calls
trogon_source_discord::gateway_runner::run which returns () so failures are
swallowed; change gateway_runner::run to return Result<(), E> (E: impl
std::error::Error or boxed error), update its internal error returns
accordingly, and then update this match arm to await the Result and propagate
errors (e.g., trogon_source_discord::gateway_runner::run(p, &cfg, bot_token,
intents).await.map_err(|e| format!("discord: {e}"))), matching the webhook arm's
error mapping so gateway failures contribute to the overall failure count.

In `@rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs`:
- Around line 51-65: The loop is awaiting bridge.dispatch(&text).await which can
block the shard poll and cause heartbeat timeouts; make dispatch fire-and-forget
by cloning the GatewayBridge and spawning the dispatch call instead. Derive
Clone for the GatewayBridge struct (add #[derive(Clone)] to GatewayBridge) since
its fields are cloneable, then in the loop clone the bridge (e.g., let bridge =
bridge.clone()) and use tokio::spawn or equivalent to call
bridge_clone.dispatch(text).await so NATS publish (publish_event) happens off
the poll thread; alternatively you can decouple publishing via a bounded channel
if you prefer backpressure control.

---

Duplicate comments:
In `@rsworkspace/crates/trogon-gateway/src/config.rs`:
- Around line 278-286: The GithubConfig construction is passing raw strings for
subject_prefix and stream_name; change those fields and their usages to use the
NatsToken value object instead of String (update
trogon_source_github::GithubConfig definition to use NatsToken for
subject_prefix and stream_name), and when building the config in this function
convert/validate the strings from section (e.g. with NatsToken::try_from or
NatsToken::new) and propagate or handle any validation error; also update any
downstream call sites that expect String to accept NatsToken so the domain-level
validation is enforced at construction.

---

Nitpick comments:
In `@devops/docker/compose/compose.yml`:
- Around line 75-78: Add a Docker healthcheck block for the unified gateway
service in compose.yml so orchestrators can verify it is actually serving
traffic (not just running); insert a healthcheck key under the gateway service
(alongside depends_on and restart) that uses a CMD curl -f against one of the
source /health endpoints (e.g., http://localhost:8081/health) and configure
interval, timeout, start_period and retries (e.g., 10s, 5s, 15s, 3) or expand to
probe multiple ports or a dedicated gateway-level /health as appropriate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: af044062-3d81-47a6-8945-a24836f34f87

📥 Commits

Reviewing files that changed from the base of the PR and between c3bce29 and 01060bb.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • devops/docker/compose/.env.example
  • devops/docker/compose/compose.yml
  • devops/docker/compose/services/trogon-gateway/Dockerfile
  • devops/docker/compose/services/trogon-gateway/README.md
  • devops/docker/compose/services/trogon-source-discord/Dockerfile
  • devops/docker/compose/services/trogon-source-discord/README.md
  • devops/docker/compose/services/trogon-source-github/Dockerfile
  • devops/docker/compose/services/trogon-source-github/README.md
  • devops/docker/compose/services/trogon-source-gitlab/Dockerfile
  • devops/docker/compose/services/trogon-source-gitlab/README.md
  • devops/docker/compose/services/trogon-source-linear/Dockerfile
  • devops/docker/compose/services/trogon-source-linear/README.md
  • devops/docker/compose/services/trogon-source-slack/README.md
  • devops/docker/compose/services/trogon-source-telegram/Dockerfile
  • rsworkspace/Cargo.toml
  • rsworkspace/crates/trogon-gateway/Cargo.toml
  • rsworkspace/crates/trogon-gateway/src/config.rs
  • rsworkspace/crates/trogon-gateway/src/main.rs
  • rsworkspace/crates/trogon-gateway/src/serve.rs
  • rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs
  • rsworkspace/crates/trogon-source-linear/src/lib.rs
💤 Files with no reviewable changes (10)
  • devops/docker/compose/services/trogon-source-discord/README.md
  • devops/docker/compose/services/trogon-source-linear/Dockerfile
  • devops/docker/compose/services/trogon-source-gitlab/README.md
  • devops/docker/compose/services/trogon-source-github/README.md
  • devops/docker/compose/services/trogon-source-linear/README.md
  • devops/docker/compose/services/trogon-source-discord/Dockerfile
  • devops/docker/compose/services/trogon-source-github/Dockerfile
  • devops/docker/compose/services/trogon-source-telegram/Dockerfile
  • devops/docker/compose/services/trogon-source-gitlab/Dockerfile
  • devops/docker/compose/services/trogon-source-slack/README.md
✅ Files skipped from review due to trivial changes (3)
  • devops/docker/compose/services/trogon-gateway/README.md
  • rsworkspace/crates/trogon-gateway/src/main.rs
  • rsworkspace/crates/trogon-gateway/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (2)
  • rsworkspace/crates/trogon-source-linear/src/lib.rs
  • rsworkspace/Cargo.toml

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

badge

Code Coverage Summary

Details
Filename                                                                      Stmts    Miss  Cover    Missing
--------------------------------------------------------------------------  -------  ------  -------  ---------------------------------------------------------------------------------------------
crates/trogon-source-linear/src/config.rs                                        17       0  100.00%
crates/trogon-source-linear/src/signature.rs                                     54       1  98.15%   16
crates/trogon-source-linear/src/server.rs                                       392       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/global_all.rs                    11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_session.rs                   18       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_client.rs                    11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent.rs                     11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_session.rs                   11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/prompt_wildcard.rs               11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_agent.rs                     18       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent_ext.rs                 11       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_client.rs                    18       0  100.00%
crates/acp-nats/src/nats/subjects/responses/cancelled.rs                         18       0  100.00%
crates/acp-nats/src/nats/subjects/responses/response.rs                          20       0  100.00%
crates/acp-nats/src/nats/subjects/responses/update.rs                            27       0  100.00%
crates/acp-nats/src/nats/subjects/responses/prompt_response.rs                   27       0  100.00%
crates/acp-nats/src/nats/subjects/responses/ext_ready.rs                         15       0  100.00%
crates/acp-nats/src/jetstream/consumers.rs                                       99       0  100.00%
crates/acp-nats/src/jetstream/streams.rs                                        194       4  97.94%   254-256, 266
crates/acp-nats/src/jetstream/ext_policy.rs                                      26       0  100.00%
crates/acp-nats/src/jetstream/provision.rs                                       61       0  100.00%
crates/trogon-std/src/time/mock.rs                                              129       0  100.00%
crates/trogon-std/src/time/system.rs                                             35       0  100.00%
crates/acp-nats/src/client/session_update.rs                                     55       0  100.00%
crates/acp-nats/src/client/fs_read_text_file.rs                                 384       0  100.00%
crates/acp-nats/src/client/fs_write_text_file.rs                                451       0  100.00%
crates/acp-nats/src/client/terminal_create.rs                                   294       0  100.00%
crates/acp-nats/src/client/terminal_wait_for_exit.rs                            396       0  100.00%
crates/acp-nats/src/client/rpc_reply.rs                                          71       0  100.00%
crates/acp-nats/src/client/terminal_output.rs                                   223       0  100.00%
crates/acp-nats/src/client/request_permission.rs                                338       0  100.00%
crates/acp-nats/src/client/terminal_kill.rs                                     309       0  100.00%
crates/acp-nats/src/client/terminal_release.rs                                  357       0  100.00%
crates/acp-nats/src/client/ext.rs                                               365       8  97.81%   193-204, 229-240
crates/acp-nats/src/client/ext_session_prompt_response.rs                       157       0  100.00%
crates/acp-nats/src/client/mod.rs                                              2987       0  100.00%
crates/acp-nats-stdio/src/config.rs                                              72       0  100.00%
crates/acp-nats-stdio/src/main.rs                                               141      27  80.85%   64, 116-123, 129-131, 148, 179-200
crates/trogon-source-slack/src/config.rs                                         17       0  100.00%
crates/trogon-source-slack/src/signature.rs                                      80       0  100.00%
crates/trogon-source-slack/src/server.rs                                        954       0  100.00%
crates/trogon-nats/src/jetstream/claim_check.rs                                 368       0  100.00%
crates/trogon-nats/src/jetstream/stream_max_age.rs                               18       0  100.00%
crates/trogon-nats/src/jetstream/publish.rs                                      64       0  100.00%
crates/trogon-nats/src/jetstream/mocks.rs                                       551       0  100.00%
crates/trogon-nats/src/auth.rs                                                  119       0  100.00%
crates/trogon-nats/src/client.rs                                                 25      25  0.00%    50-89
crates/trogon-nats/src/connect.rs                                               105      11  89.52%   22-24, 37, 49, 68-73
crates/trogon-nats/src/nats_token.rs                                            161       0  100.00%
crates/trogon-nats/src/messaging.rs                                             552       2  99.64%   132, 142
crates/trogon-nats/src/token.rs                                                   8       0  100.00%
crates/trogon-nats/src/mocks.rs                                                 304       0  100.00%
crates/trogon-std/src/dirs/system.rs                                             76       0  100.00%
crates/trogon-std/src/dirs/fixed.rs                                              84       0  100.00%
crates/acp-nats/src/telemetry/metrics.rs                                         65       0  100.00%
crates/trogon-source-telegram/src/server.rs                                     387       0  100.00%
crates/trogon-source-telegram/src/signature.rs                                   38       0  100.00%
crates/trogon-source-telegram/src/config.rs                                      17       0  100.00%
crates/acp-nats-ws/src/connection.rs                                            166      35  78.92%   75-82, 87-98, 114, 116-117, 122, 133-135, 142, 146, 150, 153-161, 172, 176, 179, 182-186, 220
crates/acp-nats-ws/src/config.rs                                                 83       0  100.00%
crates/acp-nats-ws/src/upgrade.rs                                                57       2  96.49%   59, 90
crates/acp-nats-ws/src/main.rs                                                  189      18  90.48%   89, 209-230, 308
crates/acp-nats/src/nats/subjects/commands/fork.rs                               18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/resume.rs                             18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/cancel.rs                             18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_mode.rs                           18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_model.rs                          18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/load.rs                               18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/prompt.rs                             18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_config_option.rs                  18       0  100.00%
crates/acp-nats/src/nats/subjects/commands/close.rs                              18       0  100.00%
crates/acp-nats/src/nats/subjects/stream.rs                                      58       0  100.00%
crates/acp-nats/src/nats/subjects/mod.rs                                        380       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_wait_for_exit.rs           15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_write_text_file.rs               15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_create.rs                  15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_output.rs                  15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_read_text_file.rs                15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_update.rs                   15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_request_permission.rs       15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_release.rs                 15       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_kill.rs                    15       0  100.00%
crates/acp-telemetry/src/service_name.rs                                         49       0  100.00%
crates/acp-telemetry/src/signal.rs                                                3       3  0.00%    4-43
crates/acp-telemetry/src/trace.rs                                                32       3  90.62%   23-24, 32
crates/acp-telemetry/src/metric.rs                                               35       3  91.43%   30-31, 39
crates/acp-telemetry/src/log.rs                                                  71       1  98.59%   43
crates/acp-telemetry/src/lib.rs                                                 169      32  81.07%   28-34, 56-63, 98, 103, 108, 122-137, 174, 177, 180, 186
crates/trogon-std/src/secret_string.rs                                           35       0  100.00%
crates/trogon-std/src/duration.rs                                                45       0  100.00%
crates/trogon-std/src/http.rs                                                    19       0  100.00%
crates/trogon-std/src/args.rs                                                    10       0  100.00%
crates/trogon-std/src/json.rs                                                    30       0  100.00%
crates/trogon-std/src/env/system.rs                                              17       0  100.00%
crates/trogon-std/src/env/in_memory.rs                                           81       0  100.00%
crates/trogon-source-gitlab/src/signature.rs                                     30       0  100.00%
crates/trogon-source-gitlab/src/config.rs                                        17       0  100.00%
crates/trogon-source-gitlab/src/server.rs                                       431       0  100.00%
crates/acp-nats/src/agent/bridge.rs                                             123       4  96.75%   109-112
crates/acp-nats/src/agent/resume_session.rs                                     102       0  100.00%
crates/acp-nats/src/agent/close_session.rs                                       67       0  100.00%
crates/acp-nats/src/agent/set_session_mode.rs                                    71       0  100.00%
crates/acp-nats/src/agent/initialize.rs                                          83       0  100.00%
crates/acp-nats/src/agent/test_support.rs                                       299       0  100.00%
crates/acp-nats/src/agent/mod.rs                                                 65       0  100.00%
crates/acp-nats/src/agent/set_session_model.rs                                   71       0  100.00%
crates/acp-nats/src/agent/ext_method.rs                                          92       0  100.00%
crates/acp-nats/src/agent/prompt.rs                                             633       0  100.00%
crates/acp-nats/src/agent/logout.rs                                              49       0  100.00%
crates/acp-nats/src/agent/ext_notification.rs                                    88       0  100.00%
crates/acp-nats/src/agent/js_request.rs                                         304       0  100.00%
crates/acp-nats/src/agent/list_sessions.rs                                       50       0  100.00%
crates/acp-nats/src/agent/fork_session.rs                                       106       0  100.00%
crates/acp-nats/src/agent/set_session_config_option.rs                           71       0  100.00%
crates/acp-nats/src/agent/load_session.rs                                       101       0  100.00%
crates/acp-nats/src/agent/authenticate.rs                                        52       0  100.00%
crates/acp-nats/src/agent/new_session.rs                                         91       0  100.00%
crates/acp-nats/src/agent/cancel.rs                                             105       0  100.00%
crates/trogon-source-github/src/server.rs                                       351       0  100.00%
crates/trogon-source-github/src/signature.rs                                     64       0  100.00%
crates/trogon-source-github/src/config.rs                                        17       0  100.00%
crates/acp-nats/src/in_flight_slot_guard.rs                                      32       0  100.00%
crates/acp-nats/src/req_id.rs                                                    39       0  100.00%
crates/acp-nats/src/config.rs                                                   204       0  100.00%
crates/acp-nats/src/error.rs                                                     84       0  100.00%
crates/acp-nats/src/session_id.rs                                                72       0  100.00%
crates/acp-nats/src/pending_prompt_waiters.rs                                   141       0  100.00%
crates/acp-nats/src/lib.rs                                                       73       0  100.00%
crates/acp-nats/src/acp_prefix.rs                                                51       0  100.00%
crates/acp-nats/src/client_proxy.rs                                             200       0  100.00%
crates/acp-nats/src/ext_method_name.rs                                           70       0  100.00%
crates/acp-nats/src/jsonrpc.rs                                                    6       0  100.00%
crates/acp-nats/src/nats/extensions.rs                                            3       0  100.00%
crates/acp-nats/src/nats/parsing.rs                                             285       1  99.65%   153
crates/acp-nats/src/nats/mod.rs                                                  23       0  100.00%
crates/trogon-gateway/src/streams.rs                                             60       0  100.00%
crates/trogon-gateway/src/http.rs                                                91       0  100.00%
crates/trogon-gateway/src/main.rs                                                 4       0  100.00%
crates/trogon-gateway/src/config.rs                                             974       0  100.00%
crates/acp-nats/src/nats/subjects/global/authenticate.rs                          8       0  100.00%
crates/acp-nats/src/nats/subjects/global/initialize.rs                            8       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_new.rs                           8       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext.rs                                  12       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext_notify.rs                           12       0  100.00%
crates/acp-nats/src/nats/subjects/global/logout.rs                                8       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_list.rs                          8       0  100.00%
crates/acp-nats-agent/src/connection.rs                                        1434       1  99.93%   686
crates/trogon-source-discord/src/gateway.rs                                     449       1  99.78%   133
crates/trogon-source-discord/src/server.rs                                      645       0  100.00%
crates/trogon-source-discord/src/config.rs                                       91       0  100.00%
crates/trogon-source-discord/src/signature.rs                                   103       0  100.00%
crates/trogon-std/src/fs/system.rs                                               29      12  58.62%   17-19, 31-45
crates/trogon-std/src/fs/mem.rs                                                 220      10  95.45%   61-63, 77-79, 133-135, 158
TOTAL                                                                         22633     204  99.10%

Diff against main

Filename                                              Stmts    Miss  Cover
--------------------------------------------------  -------  ------  --------
crates/trogon-source-linear/src/config.rs              -191       0  +100.00%
crates/trogon-source-linear/src/server.rs               +26      -3  +0.82%
crates/trogon-std/src/time/system.rs                     +8      -3  +11.11%
crates/trogon-source-slack/src/config.rs               -124       0  +100.00%
crates/trogon-source-slack/src/server.rs                +22       0  +100.00%
crates/trogon-nats/src/jetstream/claim_check.rs         +20       0  +100.00%
crates/trogon-nats/src/jetstream/stream_max_age.rs      +18       0  +100.00%
crates/trogon-std/src/dirs/system.rs                    -22     -11  +11.22%
crates/trogon-source-telegram/src/server.rs             -16       0  +100.00%
crates/trogon-source-telegram/src/config.rs             -72       0  +100.00%
crates/acp-telemetry/src/service_name.rs                 +3       0  +100.00%
crates/acp-telemetry/src/trace.rs                         0      -1  +3.12%
crates/acp-telemetry/src/metric.rs                        0      -1  +2.86%
crates/acp-telemetry/src/log.rs                          +1      -1  +1.45%
crates/acp-telemetry/src/lib.rs                         +16     +10  -4.56%
crates/trogon-std/src/secret_string.rs                  +35       0  +100.00%
crates/trogon-std/src/duration.rs                       +45       0  +100.00%
crates/trogon-source-gitlab/src/config.rs               -83       0  +100.00%
crates/trogon-source-gitlab/src/server.rs               -17       0  +100.00%
crates/trogon-source-github/src/server.rs               -16       0  +100.00%
crates/trogon-source-github/src/config.rs               -72       0  +100.00%
crates/trogon-gateway/src/streams.rs                    +60       0  +100.00%
crates/trogon-gateway/src/http.rs                       +91       0  +100.00%
crates/trogon-gateway/src/main.rs                        +4       0  +100.00%
crates/trogon-gateway/src/config.rs                    +974       0  +100.00%
crates/trogon-source-discord/src/server.rs              -18       0  +100.00%
crates/trogon-source-discord/src/config.rs             -199      -2  +0.69%
TOTAL                                                  +493     -12  +0.07%

Results for commit: 407878d

Minimum allowed coverage is 95%

♻️ This comment has been updated with latest results

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
devops/docker/compose/compose.yml (1)

20-73: Consider adding a healthcheck for trogon-gateway.

The previous per-source services had healthchecks, but trogon-gateway doesn't have one. Since the gateway exposes GET /health (per serve.rs lines 69-73), adding a healthcheck would allow ngrok to use condition: service_healthy instead of service_started, ensuring the HTTP server is ready before exposing tunnels.

🔧 Proposed healthcheck addition
     depends_on:
       nats:
         condition: service_healthy
     restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
+      interval: 5s
+      timeout: 3s
+      start_period: 5s
+      retries: 3

Then update ngrok's dependency:

     depends_on:
       trogon-gateway:
-        condition: service_started
+        condition: service_healthy
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@devops/docker/compose/compose.yml` around lines 20 - 73, Add a Docker Compose
healthcheck for the trogon-gateway service that probes its HTTP health endpoint
(GET /health as implemented in serve.rs) so compose can detect when the gateway
is ready; implement a healthcheck using a simple HTTP probe (curl/wget) against
http://localhost:${TROGON_GATEWAY_PORT:-8080}/health with sensible interval,
timeout and retries, reference the trogon-gateway service block and the /health
path in serve.rs to locate where to add it, and then change ngrok’s depends_on
entry to use condition: service_healthy instead of service_started so ngrok
waits for the gateway to report healthy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rsworkspace/crates/trogon-gateway/src/config.rs`:
- Around line 263-279: The resolve_github function currently forwards raw
subject_prefix and stream_name strings to trogon_source_github::GithubConfig,
bypassing NatsToken validation; update resolve_github to validate both
section.subject_prefix and section.stream_name via NatsToken::new (like
resolve_discord/resolve_slack do), push any validation error messages into the
provided errors Vec (remove the underscore from _errors so it is used), and only
construct/return the GithubConfig with the validated NatsToken values (or their
underlying safe representation) when both tokens are valid, otherwise return
None.

---

Nitpick comments:
In `@devops/docker/compose/compose.yml`:
- Around line 20-73: Add a Docker Compose healthcheck for the trogon-gateway
service that probes its HTTP health endpoint (GET /health as implemented in
serve.rs) so compose can detect when the gateway is ready; implement a
healthcheck using a simple HTTP probe (curl/wget) against
http://localhost:${TROGON_GATEWAY_PORT:-8080}/health with sensible interval,
timeout and retries, reference the trogon-gateway service block and the /health
path in serve.rs to locate where to add it, and then change ngrok’s depends_on
entry to use condition: service_healthy instead of service_started so ngrok
waits for the gateway to report healthy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aec78af6-7b23-42e6-89c3-2baa2d1c379e

📥 Commits

Reviewing files that changed from the base of the PR and between 01060bb and eb32e5e.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • devops/docker/compose/.env.example
  • devops/docker/compose/compose.yml
  • devops/docker/compose/services/trogon-gateway/Dockerfile
  • devops/docker/compose/services/trogon-gateway/README.md
  • rsworkspace/crates/trogon-gateway/Cargo.toml
  • rsworkspace/crates/trogon-gateway/src/config.rs
  • rsworkspace/crates/trogon-gateway/src/main.rs
  • rsworkspace/crates/trogon-gateway/src/serve.rs
  • rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs
  • rsworkspace/crates/trogon-source-linear/src/lib.rs
✅ Files skipped from review due to trivial changes (1)
  • rsworkspace/crates/trogon-gateway/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (3)
  • devops/docker/compose/services/trogon-gateway/Dockerfile
  • rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs
  • devops/docker/compose/.env.example

@yordis yordis force-pushed the yordis/unified-gateway branch 2 times, most recently from 5d4740c to adac53a Compare April 7, 2026 03:13
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
rsworkspace/crates/trogon-source-slack/src/config.rs (1)

20-20: ⚠️ Potential issue | 🟡 Minor

Stale documentation: SLACK_MAX_BODY_SIZE is no longer a config option.

Line 20 still documents SLACK_MAX_BODY_SIZE as an environment variable, but this field was removed from SlackConfig. The body size limit is now enforced via the HTTP_BODY_SIZE_MAX constant in the Axum DefaultBodyLimit layer. Remove this line from the doc comment to avoid confusion.

📝 Proposed fix
 /// - `SLACK_STREAM_MAX_AGE_SECS`: max age of messages in the JetStream stream in seconds (default: 604800 / 7 days)
 /// - `SLACK_NATS_ACK_TIMEOUT_SECS`: NATS ack timeout in seconds (default: 10)
-/// - `SLACK_MAX_BODY_SIZE`: maximum webhook body size in bytes (default: 1048576 / 1 MB)
 /// - `SLACK_TIMESTAMP_MAX_DRIFT_SECS`: max allowed clock drift for request timestamps in seconds (default: 300 / 5 min)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-slack/src/config.rs` at line 20, Remove the
stale doc line referencing SLACK_MAX_BODY_SIZE from the SlackConfig
documentation: locate the doc comment that mentions `SLACK_MAX_BODY_SIZE` near
the `SlackConfig` struct and delete that line so docs no longer advertise a
removed env var; optionally add a brief note (if helpful) that body size is
enforced via `HTTP_BODY_SIZE_MAX` used with Axum's `DefaultBodyLimit`, but do
not reintroduce `SLACK_MAX_BODY_SIZE` or related fields.
rsworkspace/crates/trogon-source-slack/src/server.rs (1)

76-92: ⚠️ Potential issue | 🟠 Major

Surface unroutable publish failures to the HTTP response.

This helper only logs a failed .unroutable publish. The callers still return 200/400, so a NATS or object-store outage can silently drop the malformed payload you were trying to retain for inspection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-slack/src/server.rs` around lines 76 - 92,
publish_unroutable currently swallows publish failures; change it to return a
Result<(), Error> (or appropriate anyhow/thiserror type) instead of unit,
propagate the error from publisher.publish_event(subject, headers, body,
ack_timeout).await (don’t just call outcome.log_on_error) and map the outcome
into an Err containing the publish error so callers can act; update the call
sites that invoke publish_unroutable in this module to check the Result and
translate a failed unroutable publish into an HTTP error response (e.g., 5xx)
instead of always returning 200/400 so NATS/object-store outages surface to the
client.
rsworkspace/crates/trogon-source-github/src/server.rs (1)

193-213: ⚠️ Potential issue | 🟠 Major

Don’t dedupe missing deliveries as "unknown".

Nats-Msg-Id is the dedupe key. Falling back to "unknown" means every request without X-GitHub-Delivery shares the same id and later events can be discarded. Reject missing deliveries or omit Nats-Msg-Id when the header is absent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-github/src/server.rs` around lines 193 -
213, The code currently substitutes a missing HEADER_DELIVERY with "unknown",
causing all events without X-GitHub-Delivery to share the same NATS_MESSAGE_ID;
change the logic so HEADER_DELIVERY is not defaulted to "unknown": retrieve
HEADER_DELIVERY into an Option (e.g., let delivery_opt =
headers.get(HEADER_DELIVERY).and_then(|v| v.to_str().ok()).map(|s|
s.to_owned())), and then either return an error/HTTP 400 when delivery is None
(reject missing deliveries) or, if you prefer to accept requests without a
delivery id, avoid calling
nats_headers.insert(async_nats::header::NATS_MESSAGE_ID, ...) when delivery_opt
is None; update uses of delivery (span.record and NATS_HEADER_DELIVERY) to
handle the Option (record only when Some) before calling
state.publisher.publish_event so no constant "unknown" dedupe id is published.
rsworkspace/crates/trogon-source-linear/src/server.rs (1)

327-347: ⚠️ Potential issue | 🟠 Major

Don’t reuse "unknown" as Nats-Msg-Id.

Nats-Msg-Id is the JetStream dedupe key. If webhookId is absent, every payload on this path gets the same id and later events can be dropped as duplicates. Either reject missing webhookId as unroutable or omit Nats-Msg-Id when it’s missing.

Possible fix
-    let webhook_id = parsed
-        .get("webhookId")
-        .and_then(|v| v.as_str())
-        .unwrap_or("unknown")
-        .to_owned();
+    let webhook_id = parsed
+        .get("webhookId")
+        .and_then(|v| v.as_str())
+        .map(str::to_owned);
@@
-    span.record("webhook_id", &webhook_id);
+    span.record("webhook_id", webhook_id.as_deref().unwrap_or("missing"));
@@
-    nats_headers.insert("Nats-Msg-Id", webhook_id.as_str());
+    if let Some(ref webhook_id) = webhook_id {
+        nats_headers.insert(async_nats::header::NATS_MESSAGE_ID, webhook_id.as_str());
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-linear/src/server.rs` around lines 327 -
347, The code currently defaults missing parsed.get("webhookId") to "unknown"
and always inserts a Nats-Msg-Id header, which can cause JetStream dedupe
collisions; change the logic so you do NOT use the "unknown" sentinel—either
return an error (e.g. reject the request) or, preferably, keep webhook_id as an
Option and only insert the "Nats-Msg-Id" into nats_headers when webhook_id is
Some(value); update the parsed.get("webhookId") handling and the
nats_headers.insert call that precedes state.publisher.publish_event(subject,
nats_headers, body, state.nats_ack_timeout).
♻️ Duplicate comments (1)
rsworkspace/crates/trogon-gateway/src/serve.rs (1)

183-185: ⚠️ Potential issue | 🟠 Major

Propagate Discord gateway task failures instead of always returning Ok(()).

This closure marks the task successful no matter how gateway_runner::run terminates, so non-panic gateway failures won’t increment failed or participate in the “all tasks failed” exit path. If gateway_runner::run currently returns (), it needs to return a Result here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-gateway/src/serve.rs` around lines 183 - 185, The
spawned closure currently always returns ("discord-gateway", Ok(())) so
gateway_runner failures are ignored; update the closure passed to join_set.spawn
to propagate trogon_source_discord::gateway_runner::run's result instead of
forcing Ok(()): have run return a Result if it does not already (or map its unit
return into Result by catching errors), then return ("discord-gateway",
run_result) from the closure so failures increment the failed count and
participate in the "all tasks failed" logic; specifically modify the
join_set.spawn async block that calls
trogon_source_discord::gateway_runner::run(p, &cfg, &token, intents).await to
yield the actual Result rather than Ok(()).
🧹 Nitpick comments (6)
rsworkspace/crates/trogon-std/src/http.rs (1)

9-14: Potential truncation on 32-bit platforms and code simplification.

The cast size.as_u64() as usize will silently truncate values exceeding usize::MAX on 32-bit targets. While the current use cases (≤25 MiB) fit comfortably, consider documenting this limitation or adding a compile-time assertion.

The match can also be simplified:

♻️ Simplified constructor
     pub const fn new(size: ByteSize) -> Option<Self> {
-        match NonZeroUsize::new(size.as_u64() as usize) {
-            Some(n) => Some(Self(n)),
-            None => None,
-        }
+        // Note: truncates on 32-bit targets for sizes > 4 GiB
+        if let Some(n) = NonZeroUsize::new(size.as_u64() as usize) {
+            Some(Self(n))
+        } else {
+            None
+        }
     }

Note: Option::map isn't available in const fn contexts yet, so if let is the idiomatic alternative.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-std/src/http.rs` around lines 9 - 14, The
constructor pub const fn new(size: ByteSize) -> Option<Self> risks truncating on
32-bit targets due to size.as_u64() as usize and can be simplified; update the
function (new) to first check that size.as_u64() <= usize::MAX (or add a
compile-time cfg assertion documenting 32-bit limits) and then convert safely,
and replace the match on NonZeroUsize::new(...) with an if let pattern to return
Some(Self(n)) or None for brevity and clarity.
rsworkspace/crates/trogon-source-github/src/config.rs (1)

21-29: Consider using a typed WebhookSecret value object for consistency.

The webhook_secret field is a plain String, while the GitLab source uses a typed WebhookSecret value object (with Arc<str>, empty validation, and redacted Debug). For consistency across sources and to benefit from:

  • Redacted debug output (prevents accidental secret leakage in logs)
  • Cheap cloning via Arc<str>
  • Compile-time type distinction

Consider either reusing GitLab's WebhookSecret via a shared crate or creating a similar type for GitHub.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-github/src/config.rs` around lines 21 - 29,
GithubConfig currently exposes webhook_secret as a plain String; change it to
use a typed WebhookSecret value object (matching the GitLab source) to get
Arc<str> cloning, redacted Debug and consistent type-safety: replace the field
GithubConfig::webhook_secret: String with WebhookSecret, import or move the
WebhookSecret type (or create an equivalent with Arc<str>, empty validation and
redacted Debug), update any constructors, builders and places that construct or
clone GithubConfig to accept/convert into WebhookSecret, and ensure
serialization/deserialization and trait imports are adjusted accordingly so
existing code compiles with the new type.
rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs (1)

1-10: Consider returning Result for better error propagation.

The run function returns (), making it impossible for callers to distinguish between graceful shutdown and failures. The calling code in serve.rs spawns this in a JoinSet and always returns Ok(()) regardless of what happened.

Returning a Result would enable proper error handling and potential restart logic.

♻️ Proposed signature change
 #[cfg(not(coverage))]
-pub async fn run<
+pub async fn run<
     P: trogon_nats::jetstream::JetStreamPublisher,
     S: trogon_nats::jetstream::ObjectStorePut,
 >(
     publisher: trogon_nats::jetstream::ClaimCheckPublisher<P, S>,
     config: &crate::config::DiscordConfig,
     bot_token: &str,
     intents: twilight_model::gateway::Intents,
-) {
+) -> Result<(), GatewayRunnerError> {

Where GatewayRunnerError is a typed error enum per coding guidelines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs` around lines
1 - 10, Change the async function run to return a Result so callers can
distinguish shutdown from failures: replace the unit return with Result<(),
GatewayRunnerError> (create a GatewayRunnerError enum per project guidelines),
update gateway_runner::run to return Err(...) on failures and propagate using ?
where appropriate, keep the existing #[cfg(not(coverage))] attribute and generic
bounds intact, and update the caller in serve.rs that spawns run in the JoinSet
to await and handle the Result (propagate or log/restart as needed) instead of
always treating it as Ok(()).
rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs (1)

836-852: Consider adding a test for fail_next_put.

There's a test for fail_next_get but none for fail_next_put. Adding one would improve coverage symmetry.

💚 Proposed test for fail_next_put
#[tokio::test]
async fn mock_object_store_fail_next_put() {
    let store = MockObjectStore::new();
    store.fail_next_put();
    
    let mut data = std::io::Cursor::new(b"test data".to_vec());
    let result = ObjectStorePut::put(&store, "key", &mut data).await;
    assert!(result.is_err());

    let mut data = std::io::Cursor::new(b"test data".to_vec());
    let result = ObjectStorePut::put(&store, "key", &mut data).await;
    assert!(result.is_ok());
    assert_eq!(store.stored_objects().len(), 1);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs` around lines 836 -
852, Add a symmetric test for MockObjectStore::fail_next_put similar to
mock_object_store_fail_next_get: create MockObjectStore::new(), call
store.fail_next_put(), attempt an ObjectStorePut::put(&store, "key", &mut data)
and assert it returns Err, then perform a second put with fresh data and assert
it returns Ok and that store.stored_objects().len() == 1; reference
MockObjectStore, fail_next_put, ObjectStorePut::put and stored_objects() to
locate where to add the new async #[tokio::test].
rsworkspace/crates/trogon-source-telegram/src/config.rs (1)

22-30: Consider using a wrapper type for webhook_secret for consistency.

The GitLab source uses WebhookSecret wrapper type (see rsworkspace/crates/trogon-source-gitlab/src/webhook_secret.rs) which guarantees non-empty at construction. Using a plain String here is inconsistent and loses that guarantee at the type level.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-telegram/src/config.rs` around lines 22 -
30, The TelegramSourceConfig currently uses a raw String for webhook_secret;
replace its type with the existing WebhookSecret wrapper used by the GitLab
source to preserve the non-empty guarantee. Update the struct field signature
(TelegramSourceConfig::webhook_secret) to WebhookSecret, add the appropriate
use/import of WebhookSecret from the gitlab wrapper module, and adjust any
construction/deserialization sites to call WebhookSecret::new/try_from (or the
wrapper's validated constructor) instead of passing a raw String; also update
any tests/usage that access webhook_secret as a String to use the wrapper's
accessor or .as_str() as needed.
rsworkspace/crates/trogon-source-gitlab/src/signature.rs (1)

22-36: Unnecessary SHA-256 hashing for simple token comparison.

GitLab's X-Gitlab-Token is a shared secret that should be directly compared. The current implementation hashes both values before comparing, which adds unnecessary computation. A constant-time string comparison would be more efficient and semantically clearer.

♻️ Proposed simplification using direct constant-time comparison
 pub fn verify(secret: &str, provided_token: &str) -> Result<(), SignatureError> {
     if provided_token.is_empty() {
         return Err(SignatureError::Missing);
     }
 
-    let expected = Sha256::digest(secret.as_bytes());
-    let provided = Sha256::digest(provided_token.as_bytes());
-
-    let ok = subtle::ConstantTimeEq::ct_eq(expected.as_slice(), provided.as_slice()).unwrap_u8();
+    let secret_bytes = secret.as_bytes();
+    let provided_bytes = provided_token.as_bytes();
+
+    // Length-constant comparison: if lengths differ, compare secret against itself
+    // to maintain constant time, then return mismatch.
+    if secret_bytes.len() != provided_bytes.len() {
+        let _ = subtle::ConstantTimeEq::ct_eq(secret_bytes, secret_bytes);
+        return Err(SignatureError::Mismatch);
+    }
+
+    let ok = subtle::ConstantTimeEq::ct_eq(secret_bytes, provided_bytes).unwrap_u8();
     if ok == 1 {
         Ok(())
     } else {
         Err(SignatureError::Mismatch)
     }
 }

Note: The length check introduces a timing side-channel for length, but since GitLab webhook secrets are typically fixed-length UUIDs, this is acceptable. Alternatively, keep the hash approach if variable-length secret timing leakage is a concern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rsworkspace/crates/trogon-source-gitlab/src/signature.rs` around lines 22 -
36, The verify function currently hashes secret and provided_token before
comparing; instead remove the SHA-256 digest steps and perform a direct
constant-time comparison of the raw byte slices: in verify, after the existing
empty-check that returns SignatureError::Missing, compare secret.as_bytes() and
provided_token.as_bytes() using subtle::ConstantTimeEq::ct_eq (ensure you treat
differing lengths as a non-match and return SignatureError::Mismatch), returning
Ok(()) when ct_eq yields equality and Err(SignatureError::Mismatch) otherwise;
update only verify in signature.rs and keep existing SignatureError variants
(Missing, Mismatch).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@devops/docker/compose/services/trogon-gateway/README.md`:
- Around line 31-38: The README commands assume you're in the service's compose
directory; update the instructions around copying `.env.example` to `.env` and
running `docker compose up` to first instruct the user to "cd into the directory
containing this README (the trogon-gateway compose directory)" or to run the
commands with repo-relative paths, e.g. mention running the copy and `docker
compose` from that compose directory or prefixing paths with the correct
relative path to `.env.example`; ensure the same clarification is applied to the
later lines referenced (lines 88-89) so both copy and compose invocations target
the correct directory.

In `@rsworkspace/crates/acp-nats-ws/src/main.rs`:
- Around line 118-126: The current code always sleeps for a fixed 5s after
process_connections returns; change it to treat 5s as an upper bound by wrapping
the LocalSet run_until in a timeout so we exit early if the LocalSet becomes
idle. Replace the plain rt.block_on(local.run_until(...)) call with a
tokio::time::timeout(drain, local.run_until(...)) (await the timeout result on
the runtime), keeping the same drain Duration variable, so rt.block_on returns
immediately when local.run_until completes or after the 5s bound if tasks are
still running.

In `@rsworkspace/crates/trogon-gateway/src/config.rs`:
- Around line 224-240: In resolve_nats, treat Option<String> values that are
Some("") as absent and validate for half-configured auth: interpret creds, nkey,
token only if the Option is Some(s) and s.trim().is_empty() is false; for
user/password require both section.user and section.password to be present and
non-empty (trimmed) or else return a validation error instead of silently
falling through; change the function to return a Result<NatsConfig, ConfigError>
(or similar error type used in your config path) and return an explicit error
when a half-configured auth is detected (e.g., user without password or empty
creds/nkey/token), otherwise construct
NatsConfig::new(vec![section.url.clone()], auth) with the validated NatsAuth
variants.
- Around line 269-273: The "gateway" branch currently only checks for Some(_) so
an empty string passes; update the check on section.bot_token inside the
"gateway" match arm to validate that the Option contains a non-empty,
non-whitespace token (e.g., match let Some(bot_token) and verify
!bot_token.trim().is_empty()), and if empty push the same error ("discord:
bot_token is required when mode=gateway") and return None; reference the
section.bot_token and bot_token variables in your change.
- Around line 9-11: Replace the untyped Validation variant on ConfigError
(currently Validation(Vec<String>)) with a concrete typed error (e.g., a new
ValidationError enum or struct) and make Validation hold that type
(ConfigError::Validation(ValidationError)); implement std::fmt::Display and
std::error::Error for ValidationError, preserve any per-field context (field
name, violation kind, and message) as structured fields, and update all places
that currently construct ConfigError::Validation(Vec<String>) to build and pass
ValidationError instances instead of formatted strings so callers and tests can
inspect field-level errors.

In `@rsworkspace/crates/trogon-gateway/src/serve.rs`:
- Line 23: Replace the untyped String-based error with a small typed error enum
(e.g., TaskError) and update SourceResult to use TaskError instead of String;
specifically, define TaskError variants that wrap the original error types (for
example TaskError::HttpServer(impl Error) for the http server case referenced by
format!("http server: {e}")) and change all usages (including the spots around
lines 94-95) to construct and return the appropriate TaskError variant rather
than calling format!(); also update any downstream handling/printing to use the
TaskError (preserving source/error chaining) and adjust function signatures and
Result propagation in the affected functions to accept/return TaskError.

In `@rsworkspace/crates/trogon-nats/src/jetstream/claim_check.rs`:
- Around line 155-160: The stored claim-check payload can be orphaned if
publishing/acknowledgement fails because the gateway provisions the bucket with
Default::default() (no max_age) — fix this by either setting a bucket TTL when
provisioning in trogon-gateway (set max_age on the bucket options where the
claim-check bucket is created in serve.rs) or add delete-on-failure cleanup in
the claim-check publish flow: after successfully calling self.store.put(&key,
...) and if the subsequent publish/ack path returns a failure (e.g.
PublishOutcome::PublishFailed or similar), invoke self.store.delete(&key).
Ensure you handle and log any delete errors and don't mask the original publish
error (use the same error return path as PublishOutcome).
- Around line 68-88: The enum ClaimResolveError currently discards wrapped
causes in its Error impl; update the std::error::Error impl for
ClaimResolveError to expose sources by adding a source(&self) -> Option<&(dyn
std::error::Error + 'static)> implementation that returns None for MissingKey,
Some(e) for StoreFailed(e) and Some(e) for ReadFailed(e). To do this, tighten
the trait bounds on the impl to require E: std::error::Error (in addition to the
existing Display/Debug/Send/Sync/'static) so you can return &E as a dyn Error,
and implement source to match on self and return the appropriate reference to
the stored error for the StoreFailed and ReadFailed variants.

In `@rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs`:
- Around line 36-44: The handler in gateway_runner.rs currently silently breaks
on Some(Ok(Message::Close(_))) and None (stream end) which stops the gateway;
modify the match arms for Message::Close and None to either (a) attempt a
reconnect loop with exponential backoff (retrying the connection logic used
earlier in this function, delaying between attempts and resetting backoff on
success) or (b) return a Result::Err (propagate a specific error) so the caller
in serve.rs can restart the shard; update the Some(Err(source)) arm to also
signal fatal vs transient errors if needed. Reference Message::Close and the
match handling in gateway_runner.rs and ensure the function signature propagates
an error (or implements a reconnect loop) so serve.rs can reliably restart
shards.
- Around line 32-46: The loop blocks on bridge.dispatch(&text).await (which
calls publisher.publish_event) and can delay shard.polling; instead decouple
publishing from the poll loop by queuing events to a background publisher task:
create an async mpsc channel (tokio::sync::mpsc) and in the loop send the
received text into the channel (without .awaiting the network call), then run a
separate task (tokio::spawn) that reads from that channel and calls
GatewayBridge::dispatch or publisher.publish_event; if necessary wrap
GatewayBridge or the publisher in an Arc (or Arc<Mutex>/Arc<RwLock>) so the
background task can own the publisher/bridge and perform network I/O without
blocking the shard loop.

In `@rsworkspace/crates/trogon-source-discord/src/server.rs`:
- Around line 266-282: The code currently assigns "unknown" to interaction_id
and unconditionally inserts it into nats_headers as
async_nats::header::NATS_MESSAGE_ID, causing JetStream dedupe collisions; change
the logic to treat the ID as optional: keep the parsed.get("id") result (e.g.,
interaction_id_opt from parsed.get("id").and_then(|v| v.as_str()).map(|s|
s.to_owned())), record and use a real ID when Some, and only insert
NATS_MESSAGE_ID into nats_headers when that Option is Some; alternatively return
a 400/unroutable response when id is missing—do not insert the literal "unknown"
into NATS_MESSAGE_ID. Ensure references to interaction_id, nats_headers,
NATS_MESSAGE_ID, and state.subject_prefix are updated accordingly.

In `@rsworkspace/crates/trogon-source-telegram/src/config.rs`:
- Around line 20-30: The doc mentions TELEGRAM_MAX_BODY_SIZE but the
TelegramSourceConfig struct lacks a max_body_size field; add a pub
max_body_size: usize (or appropriate type) to TelegramSourceConfig and populate
it in the from_env constructor by reading
env.var("TELEGRAM_MAX_BODY_SIZE").ok().and_then(|v|
v.parse().ok()).unwrap_or(DEFAULT_MAX_BODY_SIZE) (use the existing
DEFAULT_MAX_BODY_SIZE constant), ensuring the field name matches usages
elsewhere and imports/types remain consistent.

---

Outside diff comments:
In `@rsworkspace/crates/trogon-source-github/src/server.rs`:
- Around line 193-213: The code currently substitutes a missing HEADER_DELIVERY
with "unknown", causing all events without X-GitHub-Delivery to share the same
NATS_MESSAGE_ID; change the logic so HEADER_DELIVERY is not defaulted to
"unknown": retrieve HEADER_DELIVERY into an Option (e.g., let delivery_opt =
headers.get(HEADER_DELIVERY).and_then(|v| v.to_str().ok()).map(|s|
s.to_owned())), and then either return an error/HTTP 400 when delivery is None
(reject missing deliveries) or, if you prefer to accept requests without a
delivery id, avoid calling
nats_headers.insert(async_nats::header::NATS_MESSAGE_ID, ...) when delivery_opt
is None; update uses of delivery (span.record and NATS_HEADER_DELIVERY) to
handle the Option (record only when Some) before calling
state.publisher.publish_event so no constant "unknown" dedupe id is published.

In `@rsworkspace/crates/trogon-source-linear/src/server.rs`:
- Around line 327-347: The code currently defaults missing
parsed.get("webhookId") to "unknown" and always inserts a Nats-Msg-Id header,
which can cause JetStream dedupe collisions; change the logic so you do NOT use
the "unknown" sentinel—either return an error (e.g. reject the request) or,
preferably, keep webhook_id as an Option and only insert the "Nats-Msg-Id" into
nats_headers when webhook_id is Some(value); update the parsed.get("webhookId")
handling and the nats_headers.insert call that precedes
state.publisher.publish_event(subject, nats_headers, body,
state.nats_ack_timeout).

In `@rsworkspace/crates/trogon-source-slack/src/config.rs`:
- Line 20: Remove the stale doc line referencing SLACK_MAX_BODY_SIZE from the
SlackConfig documentation: locate the doc comment that mentions
`SLACK_MAX_BODY_SIZE` near the `SlackConfig` struct and delete that line so docs
no longer advertise a removed env var; optionally add a brief note (if helpful)
that body size is enforced via `HTTP_BODY_SIZE_MAX` used with Axum's
`DefaultBodyLimit`, but do not reintroduce `SLACK_MAX_BODY_SIZE` or related
fields.

In `@rsworkspace/crates/trogon-source-slack/src/server.rs`:
- Around line 76-92: publish_unroutable currently swallows publish failures;
change it to return a Result<(), Error> (or appropriate anyhow/thiserror type)
instead of unit, propagate the error from publisher.publish_event(subject,
headers, body, ack_timeout).await (don’t just call outcome.log_on_error) and map
the outcome into an Err containing the publish error so callers can act; update
the call sites that invoke publish_unroutable in this module to check the Result
and translate a failed unroutable publish into an HTTP error response (e.g.,
5xx) instead of always returning 200/400 so NATS/object-store outages surface to
the client.

---

Duplicate comments:
In `@rsworkspace/crates/trogon-gateway/src/serve.rs`:
- Around line 183-185: The spawned closure currently always returns
("discord-gateway", Ok(())) so gateway_runner failures are ignored; update the
closure passed to join_set.spawn to propagate
trogon_source_discord::gateway_runner::run's result instead of forcing Ok(()):
have run return a Result if it does not already (or map its unit return into
Result by catching errors), then return ("discord-gateway", run_result) from the
closure so failures increment the failed count and participate in the "all tasks
failed" logic; specifically modify the join_set.spawn async block that calls
trogon_source_discord::gateway_runner::run(p, &cfg, &token, intents).await to
yield the actual Result rather than Ok(()).

---

Nitpick comments:
In `@rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs`:
- Around line 836-852: Add a symmetric test for MockObjectStore::fail_next_put
similar to mock_object_store_fail_next_get: create MockObjectStore::new(), call
store.fail_next_put(), attempt an ObjectStorePut::put(&store, "key", &mut data)
and assert it returns Err, then perform a second put with fresh data and assert
it returns Ok and that store.stored_objects().len() == 1; reference
MockObjectStore, fail_next_put, ObjectStorePut::put and stored_objects() to
locate where to add the new async #[tokio::test].

In `@rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs`:
- Around line 1-10: Change the async function run to return a Result so callers
can distinguish shutdown from failures: replace the unit return with Result<(),
GatewayRunnerError> (create a GatewayRunnerError enum per project guidelines),
update gateway_runner::run to return Err(...) on failures and propagate using ?
where appropriate, keep the existing #[cfg(not(coverage))] attribute and generic
bounds intact, and update the caller in serve.rs that spawns run in the JoinSet
to await and handle the Result (propagate or log/restart as needed) instead of
always treating it as Ok(()).

In `@rsworkspace/crates/trogon-source-github/src/config.rs`:
- Around line 21-29: GithubConfig currently exposes webhook_secret as a plain
String; change it to use a typed WebhookSecret value object (matching the GitLab
source) to get Arc<str> cloning, redacted Debug and consistent type-safety:
replace the field GithubConfig::webhook_secret: String with WebhookSecret,
import or move the WebhookSecret type (or create an equivalent with Arc<str>,
empty validation and redacted Debug), update any constructors, builders and
places that construct or clone GithubConfig to accept/convert into
WebhookSecret, and ensure serialization/deserialization and trait imports are
adjusted accordingly so existing code compiles with the new type.

In `@rsworkspace/crates/trogon-source-gitlab/src/signature.rs`:
- Around line 22-36: The verify function currently hashes secret and
provided_token before comparing; instead remove the SHA-256 digest steps and
perform a direct constant-time comparison of the raw byte slices: in verify,
after the existing empty-check that returns SignatureError::Missing, compare
secret.as_bytes() and provided_token.as_bytes() using
subtle::ConstantTimeEq::ct_eq (ensure you treat differing lengths as a non-match
and return SignatureError::Mismatch), returning Ok(()) when ct_eq yields
equality and Err(SignatureError::Mismatch) otherwise; update only verify in
signature.rs and keep existing SignatureError variants (Missing, Mismatch).

In `@rsworkspace/crates/trogon-source-telegram/src/config.rs`:
- Around line 22-30: The TelegramSourceConfig currently uses a raw String for
webhook_secret; replace its type with the existing WebhookSecret wrapper used by
the GitLab source to preserve the non-empty guarantee. Update the struct field
signature (TelegramSourceConfig::webhook_secret) to WebhookSecret, add the
appropriate use/import of WebhookSecret from the gitlab wrapper module, and
adjust any construction/deserialization sites to call
WebhookSecret::new/try_from (or the wrapper's validated constructor) instead of
passing a raw String; also update any tests/usage that access webhook_secret as
a String to use the wrapper's accessor or .as_str() as needed.

In `@rsworkspace/crates/trogon-std/src/http.rs`:
- Around line 9-14: The constructor pub const fn new(size: ByteSize) ->
Option<Self> risks truncating on 32-bit targets due to size.as_u64() as usize
and can be simplified; update the function (new) to first check that
size.as_u64() <= usize::MAX (or add a compile-time cfg assertion documenting
32-bit limits) and then convert safely, and replace the match on
NonZeroUsize::new(...) with an if let pattern to return Some(Self(n)) or None
for brevity and clarity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0c3cdf5f-7c0e-4cf9-aee8-3406ca913a12

📥 Commits

Reviewing files that changed from the base of the PR and between 6fab3da and 5d4740c.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (64)
  • devops/docker/compose/.env.example
  • devops/docker/compose/compose.yml
  • devops/docker/compose/services/trogon-gateway/Dockerfile
  • devops/docker/compose/services/trogon-gateway/README.md
  • devops/docker/compose/services/trogon-source-github/README.md
  • devops/docker/compose/services/trogon-source-linear/Dockerfile
  • devops/docker/compose/services/trogon-source-linear/README.md
  • devops/docker/compose/services/trogon-source-slack/Dockerfile
  • devops/docker/compose/services/trogon-source-slack/README.md
  • rsworkspace/Cargo.toml
  • rsworkspace/crates/AGENTS.md
  • rsworkspace/crates/acp-nats-ws/src/main.rs
  • rsworkspace/crates/acp-telemetry/src/service_name.rs
  • rsworkspace/crates/trogon-gateway/Cargo.toml
  • rsworkspace/crates/trogon-gateway/src/cli.rs
  • rsworkspace/crates/trogon-gateway/src/config.rs
  • rsworkspace/crates/trogon-gateway/src/main.rs
  • rsworkspace/crates/trogon-gateway/src/serve.rs
  • rsworkspace/crates/trogon-nats/Cargo.toml
  • rsworkspace/crates/trogon-nats/src/jetstream/claim_check.rs
  • rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs
  • rsworkspace/crates/trogon-nats/src/jetstream/mod.rs
  • rsworkspace/crates/trogon-nats/src/jetstream/object_store.rs
  • rsworkspace/crates/trogon-nats/src/jetstream/publish.rs
  • rsworkspace/crates/trogon-source-discord/Cargo.toml
  • rsworkspace/crates/trogon-source-discord/src/config.rs
  • rsworkspace/crates/trogon-source-discord/src/constants.rs
  • rsworkspace/crates/trogon-source-discord/src/gateway.rs
  • rsworkspace/crates/trogon-source-discord/src/gateway_runner.rs
  • rsworkspace/crates/trogon-source-discord/src/lib.rs
  • rsworkspace/crates/trogon-source-discord/src/server.rs
  • rsworkspace/crates/trogon-source-discord/src/signature.rs
  • rsworkspace/crates/trogon-source-github/Cargo.toml
  • rsworkspace/crates/trogon-source-github/src/config.rs
  • rsworkspace/crates/trogon-source-github/src/constants.rs
  • rsworkspace/crates/trogon-source-github/src/main.rs
  • rsworkspace/crates/trogon-source-github/src/server.rs
  • rsworkspace/crates/trogon-source-gitlab/Cargo.toml
  • rsworkspace/crates/trogon-source-gitlab/src/config.rs
  • rsworkspace/crates/trogon-source-gitlab/src/constants.rs
  • rsworkspace/crates/trogon-source-gitlab/src/lib.rs
  • rsworkspace/crates/trogon-source-gitlab/src/server.rs
  • rsworkspace/crates/trogon-source-gitlab/src/signature.rs
  • rsworkspace/crates/trogon-source-gitlab/src/webhook_secret.rs
  • rsworkspace/crates/trogon-source-linear/Cargo.toml
  • rsworkspace/crates/trogon-source-linear/src/constants.rs
  • rsworkspace/crates/trogon-source-linear/src/lib.rs
  • rsworkspace/crates/trogon-source-linear/src/main.rs
  • rsworkspace/crates/trogon-source-linear/src/server.rs
  • rsworkspace/crates/trogon-source-slack/Cargo.toml
  • rsworkspace/crates/trogon-source-slack/src/config.rs
  • rsworkspace/crates/trogon-source-slack/src/constants.rs
  • rsworkspace/crates/trogon-source-slack/src/main.rs
  • rsworkspace/crates/trogon-source-slack/src/server.rs
  • rsworkspace/crates/trogon-source-telegram/Cargo.toml
  • rsworkspace/crates/trogon-source-telegram/src/config.rs
  • rsworkspace/crates/trogon-source-telegram/src/constants.rs
  • rsworkspace/crates/trogon-source-telegram/src/lib.rs
  • rsworkspace/crates/trogon-source-telegram/src/server.rs
  • rsworkspace/crates/trogon-source-telegram/src/signature.rs
  • rsworkspace/crates/trogon-std/Cargo.toml
  • rsworkspace/crates/trogon-std/src/http.rs
  • rsworkspace/crates/trogon-std/src/lib.rs
  • rsworkspace/crates/trogon-std/src/time/system.rs
💤 Files with no reviewable changes (10)
  • devops/docker/compose/services/trogon-source-linear/README.md
  • devops/docker/compose/services/trogon-source-github/README.md
  • devops/docker/compose/services/trogon-source-slack/Dockerfile
  • rsworkspace/crates/trogon-source-github/Cargo.toml
  • devops/docker/compose/services/trogon-source-slack/README.md
  • devops/docker/compose/services/trogon-source-linear/Dockerfile
  • rsworkspace/crates/trogon-source-slack/Cargo.toml
  • rsworkspace/crates/trogon-source-linear/src/main.rs
  • rsworkspace/crates/trogon-source-github/src/main.rs
  • rsworkspace/crates/trogon-source-slack/src/main.rs
✅ Files skipped from review due to trivial changes (10)
  • rsworkspace/crates/trogon-std/Cargo.toml
  • rsworkspace/crates/trogon-std/src/time/system.rs
  • rsworkspace/crates/trogon-nats/Cargo.toml
  • rsworkspace/crates/AGENTS.md
  • rsworkspace/crates/trogon-gateway/src/cli.rs
  • rsworkspace/crates/trogon-source-telegram/Cargo.toml
  • rsworkspace/crates/trogon-source-gitlab/Cargo.toml
  • rsworkspace/crates/trogon-source-telegram/src/constants.rs
  • rsworkspace/crates/trogon-source-discord/src/constants.rs
  • rsworkspace/crates/trogon-source-gitlab/src/constants.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • devops/docker/compose/services/trogon-gateway/Dockerfile
  • rsworkspace/crates/trogon-source-linear/Cargo.toml
  • rsworkspace/Cargo.toml
  • devops/docker/compose/compose.yml

@yordis yordis force-pushed the yordis/unified-gateway branch from 2aa8629 to 641bca0 Compare April 7, 2026 21:10
@yordis yordis force-pushed the yordis/unified-gateway branch 4 times, most recently from 0bf5032 to 009c701 Compare April 7, 2026 23:03
@yordis yordis force-pushed the yordis/unified-gateway branch from 04cb1e1 to f2c0815 Compare April 8, 2026 00:17
@yordis yordis force-pushed the yordis/unified-gateway branch 2 times, most recently from 62efa59 to 0f058ca Compare April 8, 2026 01:04
@yordis yordis force-pushed the yordis/unified-gateway branch 4 times, most recently from 3b4f358 to 90807b6 Compare April 8, 2026 01:55
@yordis yordis force-pushed the yordis/unified-gateway branch 2 times, most recently from a619a6e to b3f721c Compare April 8, 2026 02:12
@yordis yordis force-pushed the yordis/unified-gateway branch 4 times, most recently from d4d0d55 to 8d144c3 Compare April 8, 2026 05:29
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8d144c3. Configure here.

@yordis yordis force-pushed the yordis/unified-gateway branch 7 times, most recently from d4c19ed to 15a8114 Compare April 8, 2026 20:45
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis force-pushed the yordis/unified-gateway branch from 15a8114 to 407878d Compare April 8, 2026 20:47
@yordis yordis added the rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline label Apr 8, 2026
@yordis yordis merged commit 67405d9 into main Apr 8, 2026
9 of 10 checks passed
@yordis yordis deleted the yordis/unified-gateway branch April 8, 2026 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant