Skip to content

docs+mcp-gateway: MCP plan and Phase 1 NATS gateway - #178

Closed
yordis wants to merge 8 commits into
mainfrom
yordis/docs-mcp-gateway-plan
Closed

docs+mcp-gateway: MCP plan and Phase 1 NATS gateway#178
yordis wants to merge 8 commits into
mainfrom
yordis/docs-mcp-gateway-plan

Conversation

@yordis

@yordis yordis commented May 22, 2026

Copy link
Copy Markdown
Member

Summary

  • The MCP gateway RFC lived only on paper until we had runnable queue ingress and JetStream-shaped audit stubs, slowing feedback from teams that negotiate on live NATS topologies rather than prose.
  • Default CI excludes brokerful checks; an opt-in NATS forward test avoids silent divergence between integration environments and desks that only run pure unit crates.

yordis added 4 commits May 22, 2026 04:12
Capture the design space before implementation so the irreversible
decisions (host ABI, DSL, tenancy, correlation) are debated on paper.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
…oint

The gateway plan had no runnable edge path against mcp-nats subjects; deploying
Phase 1 first proves queue-group forwarding and JetStream audit before betting
SpiceDB and transport adapters on speculative wiring.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Operators need an explicit broker-backed check before layering SpiceDB, but CI
clusters rarely expose NATS uniformly; deferring ignored forward tests hides
whole-path drift without silently failing default automation.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@cursor

cursor Bot commented May 22, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Adds a new network-facing gateway binary that enforces authentication/authorization decisions and rewrites headers/subjects, so misconfiguration or logic bugs could block traffic or weaken identity guarantees. The implementation is self-contained and Phase-1 scoped, but it introduces new security-sensitive JWT validation and SpiceDB integration paths.

Overview
Adds a new trogon-mcp-gateway crate plus an extensive design/RFC doc (MCP_GATEWAY_PLAN.md). The gateway runs as a NATS queue-group consumer on {prefix}.gateway.request.> and forwards to {prefix}.server.*, preserving request/reply behavior while emitting per-request audit envelopes to JetStream ({prefix}.audit.>).

Phase 1 enforcement is introduced: a hardcoded CEL gate decides when to invoke SpiceDB authorization (currently tools/call and resources/read), with configurable SpiceDB tuple shapes, cached ZedToken consistency, and JSON-RPC error codes for deny/auth/authz failures. The gateway also adds verified JWT ingress (off/validate/require) with JWKS/RSA/HS256 support, strips the legacy trogon-mcp-tenant header when JWT is active, and injects trusted identity headers plus trace context on egress; an ignored end-to-end NATS forwarding test is included.

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

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

@yordis has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 3 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 14e83bfb-6389-4931-ae74-e91f02624864

📥 Commits

Reviewing files that changed from the base of the PR and between 7b20dba and b23337d.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • MCP_GATEWAY_PLAN.md
  • rsworkspace/crates/trogon-mcp-gateway/Cargo.toml
  • rsworkspace/crates/trogon-mcp-gateway/README.md
  • rsworkspace/crates/trogon-mcp-gateway/src/audit.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/authz.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/config.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/gateway.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/jwt.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/lib.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/main.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/policy.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/rpc_codes.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/spicedb.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/trace.rs
  • rsworkspace/crates/trogon-mcp-gateway/tests/e2e_nats_forward.rs

Walkthrough

Introduces a complete NATS-based MCP gateway service with request interception, CEL-based authorization policy, JetStream audit publishing, decision tracing, and end-to-end request forwarding from gateway ingress topic to backend server topic.

Changes

MCP Gateway Core Implementation

Layer / File(s) Summary
Architecture & Design Document
MCP_GATEWAY_PLAN.md
Comprehensive design document specifying the NATS-based MCP gateway architecture, including subject topology, request flow, policy engine tiers, audit model, redaction strategy, Phase 1 wire-format contracts, and phased delivery roadmap.
Project Scaffolding & Dependencies
rsworkspace/crates/trogon-mcp-gateway/Cargo.toml, rsworkspace/crates/trogon-mcp-gateway/src/lib.rs, rsworkspace/crates/trogon-mcp-gateway/README.md
Cargo manifest with workspace configuration and pinned cel-interpreter=0.10.0, public module surface declarations, and README documentation covering routing semantics, client topic changes, tenant headers, environment variables, and smoke-test steps.
Configuration & Startup Bootstrap
rsworkspace/crates/trogon-mcp-gateway/src/config.rs, rsworkspace/crates/trogon-mcp-gateway/src/main.rs
CLI argument parsing with environment variable overrides for queue group and audit stream initialization; binary entrypoint that loads configuration, applies timeout overrides, initializes telemetry, connects to NATS, constructs allow-all permission checker, and runs the gateway with graceful shutdown and OpenTelemetry cleanup.
Authorization & Policy Engine
rsworkspace/crates/trogon-mcp-gateway/src/authz.rs, rsworkspace/crates/trogon-mcp-gateway/src/policy.rs
Authorization context (AuthzContext) and PermissionChecker trait for tool-call authorization decisions; phase-1 CEL-based policy (SpicedbGatePolicy) that gates SpiceDB enforcement based on JSON-RPC method—requires SpiceDB for tools/call but not for tools/list.
Subject Routing & Request Processing Pipeline
rsworkspace/crates/trogon-mcp-gateway/src/subject.rs, rsworkspace/crates/trogon-mcp-gateway/src/gateway.rs
Subject rewriting translates gateway ingress topics ({prefix}.gateway.request.*) to server topics ({prefix}.server.*); core request handling extracts JSON-RPC method/id, applies authorization checks, injects trace context, forwards requests with fire-and-forget or request/reply semantics, publishes audit events, and returns JSON-RPC errors on upstream timeouts or failures.
Audit, Decision Tracing & Telemetry
rsworkspace/crates/trogon-mcp-gateway/src/audit.rs, rsworkspace/crates/trogon-mcp-gateway/src/trace.rs, rsworkspace/crates/trogon-telemetry/src/service_name.rs
JetStream-backed audit publishing with AuditEnvelope serialization and ack-wait; in-memory decision trace store keyed by request ID with automatic cleanup at 10,000 entries; telemetry service name enum extended with TrogonMcpGateway variant.
Integration Testing & Verification
rsworkspace/crates/trogon-mcp-gateway/tests/e2e_nats_forward.rs
End-to-end integration test that connects to live NATS, subscribes to backend topic, responds with JSON-RPC reply, runs gateway with allow-all permission checker, sends tools/list request through gateway topic, and verifies response structure.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GatewayIngress as Gateway<br/>(gateway.request)
  participant AuthPolicy as Auth & Policy
  participant Backend as Backend<br/>(server.*)
  participant Audit as Audit Stream
  Client->>GatewayIngress: MCP JSON-RPC on gateway topic
  GatewayIngress->>AuthPolicy: Extract method, check if requires SpiceDB
  alt Authorization Required
    AuthPolicy-->>GatewayIngress: Policy decision (allow/deny)
    alt Denied
      GatewayIngress->>Audit: Publish "deny" event
      GatewayIngress-->>Client: JSON-RPC -32003 error
    else Allowed
      GatewayIngress->>Backend: Forward to rewritten backend topic
      alt Has Reply Subject
        Backend-->>GatewayIngress: Response
        GatewayIngress->>Audit: Publish "allow" event
        GatewayIngress-->>Client: Forward response
      else Fire-and-Forget
        GatewayIngress->>Audit: Publish "allow" event
      end
    end
  else No Auth Required
    GatewayIngress->>Backend: Forward directly
    alt Has Reply Subject
      Backend-->>GatewayIngress: Response
      GatewayIngress->>Audit: Publish "allow" event
      GatewayIngress-->>Client: Forward response
    else Fire-and-Forget
      GatewayIngress->>Audit: Publish "allow" event
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • TrogonStack/trogonai#152: Extends ServiceName enum with additional MCP service variants; both PRs modify the telemetry service name declarations.

Suggested labels

rust:coverage-baseline-reset

Poem

🐰 A gateway stands on NATS so swift,
Filtering requests with policy drift,
CEL decisions and audit trails bright,
From gateway.request to server.* flight!
Authorization gates the way,
Traces and audits save the day. 🎫

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: adding MCP gateway design documentation and Phase 1 NATS gateway implementation.
Description check ✅ Passed The description is related to the changeset, explaining why the RFC and gateway implementation are being introduced and their purpose.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented May 22, 2026

Copy link
Copy Markdown

badge

Code Coverage Summary

Details
Filename                                                                              Stmts    Miss  Cover    Missing
----------------------------------------------------------------------------------  -------  ------  -------  ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
crates/trogon-telemetry/src/trace.rs                                                     23       1  95.65%   22
crates/trogon-telemetry/src/log.rs                                                       68       1  98.53%   33
crates/trogon-telemetry/src/lib.rs                                                      197      23  88.32%   94, 99, 104, 114-115, 121-139, 175, 178, 181, 187
crates/trogon-telemetry/src/metric.rs                                                    26       1  96.15%   29
crates/trogon-telemetry/src/resource_attribute.rs                                        23       0  100.00%
crates/trogon-telemetry/src/service_name.rs                                              47       0  100.00%
crates/acp-nats-agent/src/connection.rs                                                1270       1  99.92%   607
crates/trogon-nats/src/lease/nats_kv_lease_config.rs                                     26       0  100.00%
crates/trogon-nats/src/lease/release.rs                                                   5       5  0.00%    8-12
crates/trogon-nats/src/lease/renew.rs                                                   246      19  92.28%   23-29, 48-59
crates/trogon-nats/src/lease/acquire.rs                                                   5       5  0.00%    9-14
crates/trogon-nats/src/lease/renew_interval.rs                                           61       0  100.00%
crates/trogon-nats/src/lease/lease_bucket.rs                                             19       0  100.00%
crates/trogon-nats/src/lease/mod.rs                                                     561      13  97.68%   180-193
crates/trogon-nats/src/lease/lease_key.rs                                                19       0  100.00%
crates/trogon-nats/src/lease/provision.rs                                               187      10  94.65%   82-92
crates/trogon-nats/src/lease/lease_timing.rs                                             15       0  100.00%
crates/trogon-nats/src/lease/ttl.rs                                                      73       0  100.00%
crates/trogon-nats/src/lease/lease_config_error.rs                                       11       0  100.00%
crates/trogon-decider-runtime/src/event/codec/event_payload_error.rs                     35       0  100.00%
crates/trogon-decider-runtime/src/event/codec/event_decode.rs                            29       0  100.00%
crates/trogon-service-config/src/lib.rs                                                  92       0  100.00%
crates/trogon-gateway/src/source/microsoft_graph/server.rs                              325       0  100.00%
crates/trogon-gateway/src/source/microsoft_graph/client_state.rs                         30       0  100.00%
crates/trogon-mcp-gateway/src/authz.rs                                                   11      11  0.00%    18-70
crates/trogon-mcp-gateway/src/config.rs                                                  65      65  0.00%    41-140
crates/trogon-mcp-gateway/src/jwt.rs                                                    399     231  42.11%   41-143, 168-173, 192-221, 230, 253-263, 313-370, 377, 407, 412-436, 440-498
crates/trogon-mcp-gateway/src/spicedb.rs                                                114      92  19.30%   29-30, 41-201
crates/trogon-mcp-gateway/src/subject.rs                                                 22       6  72.73%   7-9, 22-24
crates/trogon-mcp-gateway/src/audit.rs                                                   46      46  0.00%    28-80
crates/trogon-mcp-gateway/src/main.rs                                                    75      75  0.00%    20-123
crates/trogon-mcp-gateway/src/trace.rs                                                   10      10  0.00%    29-39
crates/trogon-mcp-gateway/src/policy.rs                                                  33       8  75.76%   11-13, 37-41
crates/trogon-mcp-gateway/src/gateway.rs                                                550     482  12.36%   34-544, 555-571, 579, 584, 588-602, 628, 633-690
crates/trogon-std/src/time/mock.rs                                                      125       0  100.00%
crates/trogon-std/src/time/system.rs                                                     31       0  100.00%
crates/acp-nats/src/agent/load_session.rs                                                89       0  100.00%
crates/acp-nats/src/agent/prompt.rs                                                     471       0  100.00%
crates/acp-nats/src/agent/close_session.rs                                               63       0  100.00%
crates/acp-nats/src/agent/cancel.rs                                                     101       0  100.00%
crates/acp-nats/src/agent/mod.rs                                                         65       0  100.00%
crates/acp-nats/src/agent/list_sessions.rs                                               47       0  100.00%
crates/acp-nats/src/agent/ext_notification.rs                                            82       0  100.00%
crates/acp-nats/src/agent/fork_session.rs                                                94       0  100.00%
crates/acp-nats/src/agent/set_session_mode.rs                                            67       0  100.00%
crates/acp-nats/src/agent/initialize.rs                                                  79       0  100.00%
crates/acp-nats/src/agent/js_request.rs                                                 283       0  100.00%
crates/acp-nats/src/agent/set_session_model.rs                                           67       0  100.00%
crates/acp-nats/src/agent/test_support.rs                                               267       0  100.00%
crates/acp-nats/src/agent/bridge.rs                                                     123       4  96.75%   108-111
crates/acp-nats/src/agent/new_session.rs                                                 82       0  100.00%
crates/acp-nats/src/agent/logout.rs                                                      49       0  100.00%
crates/acp-nats/src/agent/authenticate.rs                                                49       0  100.00%
crates/acp-nats/src/agent/resume_session.rs                                              90       0  100.00%
crates/acp-nats/src/agent/ext_method.rs                                                  82       0  100.00%
crates/acp-nats/src/agent/set_session_config_option.rs                                   67       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_session.rs                           12       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_session.rs                            9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_agent.rs                             15       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_client.rs                             9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/prompt_wildcard.rs                        9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent.rs                              9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_client.rs                            15       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/global_all.rs                             9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent_ext.rs                          9       0  100.00%
crates/trogon-gateway/src/source/gitlab/signature.rs                                    165       0  100.00%
crates/trogon-gateway/src/source/gitlab/gitlab_signing_token.rs                          74       0  100.00%
crates/trogon-gateway/src/source/gitlab/server.rs                                       460       0  100.00%
crates/trogon-gateway/src/source/discord/config.rs                                      108       0  100.00%
crates/trogon-gateway/src/source/discord/gateway.rs                                     426       1  99.77%   137
crates/trogon-decider-runtime/src/event/event_id.rs                                      32       0  100.00%
crates/trogon-decider-runtime/src/event/mod.rs                                          170       0  100.00%
crates/trogon-decider-runtime/src/event/event_identity.rs                                 3       0  100.00%
crates/trogon-decider-runtime/src/event/stream_event.rs                                   8       0  100.00%
crates/mcp-nats-server/src/allowed_host.rs                                               90       0  100.00%
crates/mcp-nats-server/src/config.rs                                                    276       0  100.00%
crates/mcp-nats-server/src/main.rs                                                      357     127  64.43%   149-166, 202-204, 214, 220-221, 228-231, 255-257, 261-270, 292-305, 310-358, 489, 492, 500-542
crates/trogon-nats/src/jetstream/mocks.rs                                              1637       7  99.57%   450, 824-826, 1003-1005
crates/trogon-nats/src/jetstream/traits.rs                                               40      40  0.00%    181-251
crates/trogon-nats/src/jetstream/publish.rs                                              64       0  100.00%
crates/trogon-nats/src/jetstream/stream_max_age.rs                                       18       0  100.00%
crates/trogon-nats/src/jetstream/claim_check.rs                                         346       0  100.00%
crates/trogon-nats/src/jetstream/create_conflicts.rs                                     24       0  100.00%
crates/trogon-decider-runtime/src/snapshot/mod.rs                                         3       0  100.00%
crates/trogon-decider-runtime/src/snapshot/read_snapshot.rs                              11       0  100.00%
crates/trogon-nats/src/telemetry/messaging.rs                                            82       0  100.00%
crates/acp-nats/src/nats/subjects/responses/cancelled.rs                                 15       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                                 12       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/mcp-nats/src/nats/subjects/client/initialized.rs                                  12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/create_elicitation.rs                           12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/ping.rs                                          9       0  100.00%
crates/mcp-nats/src/nats/subjects/client/list_roots.rs                                   12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/cancelled.rs                                    12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/progress.rs                                     12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/roots_list_changed.rs                           12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/create_message.rs                               12       0  100.00%
crates/mcp-nats-stdio/src/config.rs                                                     160       0  100.00%
crates/mcp-nats-stdio/src/main.rs                                                       212       0  100.00%
crates/mcp-nats/src/nats/subjects/server/resource_list_changed.rs                        12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/set_logging_level.rs                            12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/initialize.rs                                   12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/subscribe_resource.rs                           12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/unsubscribe_resource.rs                         12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/complete.rs                                     12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/elicitation_completed.rs                        12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/cancel_task.rs                                  12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/call_tool.rs                                    12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_prompts.rs                                 12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/cancelled.rs                                    12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_resources.rs                               12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/logging_message.rs                              12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/prompt_list_changed.rs                          12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/get_task.rs                                     12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/get_prompt.rs                                   12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/get_task_result.rs                              12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_resource_templates.rs                      12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/resource_updated.rs                             12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/progress.rs                                     12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_tasks.rs                                   12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/tool_list_changed.rs                            12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/ping.rs                                          9       0  100.00%
crates/mcp-nats/src/nats/subjects/server/read_resource.rs                                12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_tools.rs                                   12       0  100.00%
crates/trogon-gateway/src/source/github/signature.rs                                     61       0  100.00%
crates/trogon-gateway/src/source/github/config.rs                                        17       0  100.00%
crates/trogon-gateway/src/source/github/server.rs                                       328       0  100.00%
crates/acp-nats/src/client/terminal_release.rs                                          347       0  100.00%
crates/acp-nats/src/client/terminal_wait_for_exit.rs                                    378       0  100.00%
crates/acp-nats/src/client/mod.rs                                                      2851       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                                         356       0  100.00%
crates/acp-nats/src/client/terminal_output.rs                                           206       0  100.00%
crates/acp-nats/src/client/ext_session_prompt_response.rs                               135       0  100.00%
crates/acp-nats/src/client/rpc_reply.rs                                                  64       0  100.00%
crates/acp-nats/src/client/ext.rs                                                       308       8  97.40%   163-172, 189-198
crates/acp-nats/src/client/fs_write_text_file.rs                                        418       0  100.00%
crates/acp-nats/src/client/request_permission.rs                                        308       0  100.00%
crates/acp-nats/src/client/terminal_create.rs                                           274       0  100.00%
crates/acp-nats/src/client/terminal_kill.rs                                             290       0  100.00%
crates/trogon-gateway/src/source/telegram/server.rs                                     339       0  100.00%
crates/trogon-gateway/src/source/telegram/config.rs                                     109       0  100.00%
crates/trogon-gateway/src/source/telegram/registration.rs                               327       0  100.00%
crates/trogon-gateway/src/source/telegram/signature.rs                                   32       0  100.00%
crates/acp-nats/src/jetstream/ext_policy.rs                                              26       0  100.00%
crates/acp-nats/src/jetstream/provision.rs                                               53       0  100.00%
crates/acp-nats/src/jetstream/consumers.rs                                               91       0  100.00%
crates/acp-nats/src/jetstream/streams.rs                                                163       4  97.55%   206-208, 218
crates/trogon-gateway/src/source/slack/signature.rs                                      77       0  100.00%
crates/trogon-gateway/src/source/slack/socket_mode.rs                                   731       0  100.00%
crates/trogon-gateway/src/source/slack/server.rs                                        854       0  100.00%
crates/trogon-gateway/src/source/slack/config.rs                                         68       0  100.00%
crates/mcp-nats/src/nats/parsing.rs                                                     191       0  100.00%
crates/mcp-nats/src/nats/mod.rs                                                          99       0  100.00%
crates/trogon-gateway/src/source/notion/signature.rs                                     56       1  98.21%   32
crates/trogon-gateway/src/source/notion/notion_verification_token.rs                     17       0  100.00%
crates/trogon-gateway/src/source/notion/verification_token.rs                           240       0  100.00%
crates/trogon-gateway/src/source/notion/notion_event_type.rs                             46       3  93.48%   47-49
crates/trogon-gateway/src/source/notion/server.rs                                       318       8  97.48%   93-97, 130-131, 150-151
crates/trogon-decider-runtime/src/headers/mod.rs                                         74       0  100.00%
crates/trogon-decider-runtime/src/headers/header_map.rs                                  54       3  94.44%   20-22
crates/trogon-decider-runtime/src/headers/from_entries_error.rs                          11       0  100.00%
crates/trogon-decider-runtime/src/headers/header_name.rs                                 33       0  100.00%
crates/trogon-decider-runtime/src/headers/header_value.rs                                37       0  100.00%
crates/mcp-nats/src/nats/subjects/mod.rs                                                 89       0  100.00%
crates/acp-nats-server/src/acp_connection_id.rs                                          45       0  100.00%
crates/acp-nats-server/src/connection.rs                                                171      32  81.29%   76-83, 88-99, 115, 117-118, 123, 132-133, 138, 142, 146, 149, 157, 161, 164, 167-171, 207
crates/acp-nats-server/src/main.rs                                                      896      10  98.88%   100, 231-238, 437
crates/acp-nats-server/src/transport.rs                                                1852     110  94.06%   277, 452, 536, 554, 581, 635, 640, 659, 671, 790, 813-815, 867, 884-887, 982-985, 1059, 1062, 1065, 1074, 1078, 1081, 1084-1087, 1106, 1138-1141, 1149-1154, 1166-1170, 1174-1183, 1195-1196, 1214-1215, 1225, 1241-1245, 1273-1279, 1290, 1293-1300, 1305-1309, 1312-1317, 1334, 1336-1337, 1419-1420, 1432-1433, 1453-1454, 1506-1522, 2218, 2261, 2313, 2368, 2380
crates/acp-nats-server/src/config.rs                                                    137       9  93.43%   41, 50-61
crates/acp-nats-stdio/src/config.rs                                                      66       0  100.00%
crates/acp-nats-stdio/src/main.rs                                                       135      25  81.48%   65, 113-120, 126-128, 145, 174-193
crates/trogon-decider-runtime/src/execution.rs                                         1239       0  100.00%
crates/acp-nats/src/telemetry/metrics.rs                                                 53       0  100.00%
crates/mcp-nats/src/jsonrpc.rs                                                           22       0  100.00%
crates/mcp-nats/src/config.rs                                                           110       0  100.00%
crates/mcp-nats/src/client.rs                                                            31       0  100.00%
crates/mcp-nats/src/mcp_peer_id.rs                                                       33       0  100.00%
crates/mcp-nats/src/server.rs                                                            31       0  100.00%
crates/mcp-nats/src/mcp_prefix.rs                                                        36       0  100.00%
crates/mcp-nats/src/transport.rs                                                        722       0  100.00%
crates/trogon-std/src/fs/mem.rs                                                         216      10  95.37%   61-63, 77-79, 132-134, 157
crates/trogon-std/src/fs/system.rs                                                       92       0  100.00%
crates/trogon-std/src/telemetry/http.rs                                                 217       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_envelope_encode_error.rs       20       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_envelope_decode_error.rs       38       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/encoded_snapshot.rs                     57       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_payload_decode.rs               3       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_decode_error.rs                22       3  86.36%   11-13
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_encode_error.rs                22       3  86.36%   11-13
crates/trogon-decider-nats/src/store.rs                                                 145      65  55.17%   33-187
crates/trogon-decider-nats/src/stream_store.rs                                          725      84  88.41%   70-213, 240-258, 301, 329-330, 333, 349-353, 519-520, 561, 574-578
crates/trogon-decider-nats/src/snapshot_store.rs                                        740     140  81.08%   64-217, 265-267, 305-313, 322-323, 338-344, 458-473, 502-511, 563, 568, 646-648, 654-656, 688-689, 699-700, 718, 742-743
crates/trogon-gateway/src/source_status.rs                                               28       0  100.00%
crates/trogon-gateway/src/http.rs                                                       217       1  99.54%   128
crates/trogon-gateway/src/main.rs                                                       116       0  100.00%
crates/trogon-gateway/src/source_integration_id.rs                                       61       3  95.08%   55, 57, 65
crates/trogon-gateway/src/config.rs                                                    2655      46  98.27%   105, 124, 342-343, 346, 744, 747, 907, 964, 1047, 1050, 1053, 1057, 1141-1148, 1225, 1228, 1231, 1236, 1294, 1297, 1300, 1379, 1382, 1385, 1389, 1453, 1456, 1459, 1522, 1525, 1528, 1533, 1608, 1611, 1614, 1619, 1677, 1680, 1683, 1896-1898
crates/trogon-gateway/src/streams.rs                                                    169      10  94.08%   11, 23, 31, 39, 47, 55, 63, 71, 79, 87
crates/trogon-gateway/src/source/standard_webhooks.rs                                   172       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_output.rs                          12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_create.rs                          12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_release.rs                         12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_request_permission.rs               12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_write_text_file.rs                       12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_read_text_file.rs                        12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_update.rs                           12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_kill.rs                            12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_wait_for_exit.rs                   12       0  100.00%
crates/trogon-gateway/src/source/sentry/sentry_client_secret.rs                          17       0  100.00%
crates/trogon-gateway/src/source/sentry/server.rs                                       311       0  100.00%
crates/trogon-gateway/src/source/sentry/signature.rs                                     54       0  100.00%
crates/trogon-nats/src/subject_token_violation.rs                                        17       0  100.00%
crates/trogon-nats/src/auth.rs                                                          114       0  100.00%
crates/trogon-nats/src/connect.rs                                                        94       9  90.43%   22-23, 33, 60-65
crates/trogon-nats/src/client.rs                                                         22      22  0.00%    50-86
crates/trogon-nats/src/token.rs                                                           6       0  100.00%
crates/trogon-nats/src/mocks.rs                                                         317       0  100.00%
crates/trogon-nats/src/messaging.rs                                                     561       2  99.64%   144, 154
crates/trogon-nats/src/nats_token.rs                                                    157       0  100.00%
crates/trogon-decider/src/act.rs                                                         62       0  100.00%
crates/trogon-decider/src/events.rs                                                      49       0  100.00%
crates/trogon-decider/src/testing.rs                                                    660       0  100.00%
crates/trogon-decider/src/lib.rs                                                        143       0  100.00%
crates/trogon-decider/src/decision.rs                                                    37       0  100.00%
crates/trogon-std/src/secret_string.rs                                                   35       0  100.00%
crates/trogon-std/src/json.rs                                                            30       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                                                            19       9  52.63%   11-28
crates/trogon-std/src/signal.rs                                                          26      12  53.85%   6-11, 18-25, 34
crates/trogon-std/src/uuid.rs                                                             7       0  100.00%
crates/trogon-decider-runtime/src/stream/mod.rs                                          38       0  100.00%
crates/trogon-decider-runtime/src/stream/read_stream.rs                                  10       0  100.00%
crates/trogon-decider-runtime/src/stream/append_stream.rs                                 5       0  100.00%
crates/trogon-decider-runtime/src/stream/stream_position.rs                              29       0  100.00%
crates/acp-nats/src/nats/subjects/global/logout.rs                                        6       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_list.rs                                  6       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_new.rs                                   6       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext.rs                                           9       0  100.00%
crates/acp-nats/src/nats/subjects/global/authenticate.rs                                  6       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext_notify.rs                                    9       0  100.00%
crates/acp-nats/src/nats/subjects/global/initialize.rs                                    6       0  100.00%
crates/acp-nats/src/nats/parsing.rs                                                     278       1  99.64%   151
crates/acp-nats/src/nats/extensions.rs                                                    3       0  100.00%
crates/acp-nats/src/nats/mod.rs                                                          23       0  100.00%
crates/trogon-std/src/dirs/system.rs                                                     71       0  100.00%
crates/trogon-std/src/dirs/fixed.rs                                                      80       0  100.00%
crates/acp-nats/src/nats/subjects/mod.rs                                                362       0  100.00%
crates/acp-nats/src/nats/subjects/stream.rs                                              56       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_model.rs                                  15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_config_option.rs                          15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_mode.rs                                   15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/cancel.rs                                     15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/fork.rs                                       15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/load.rs                                       15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/prompt.rs                                     15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/resume.rs                                     15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/close.rs                                      15       0  100.00%
crates/trogon-gateway/src/source/linear/signature.rs                                     54       1  98.15%   16
crates/trogon-gateway/src/source/linear/config.rs                                        17       0  100.00%
crates/trogon-gateway/src/source/linear/server.rs                                       386       0  100.00%
crates/mcp-nats/src/telemetry/transport.rs                                                6       0  100.00%
crates/trogon-gateway/src/source/twitter/server.rs                                      525       0  100.00%
crates/trogon-gateway/src/source/twitter/signature.rs                                    69       0  100.00%
crates/trogon-gateway/src/source/twitter/config.rs                                       17       0  100.00%
crates/trogon-gateway/src/source/incidentio/server.rs                                   343       0  100.00%
crates/trogon-gateway/src/source/incidentio/signature.rs                                206       0  100.00%
crates/trogon-gateway/src/source/incidentio/incidentio_event_type.rs                     62       0  100.00%
crates/trogon-gateway/src/source/incidentio/config.rs                                    16       0  100.00%
crates/trogon-gateway/src/source/incidentio/incidentio_signing_secret.rs                 67       0  100.00%
crates/trogon-std/src/env/in_memory.rs                                                   73       0  100.00%
crates/trogon-std/src/env/system.rs                                                      17       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/one_client.rs                             9       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/all_server.rs                             6       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/all_client.rs                             6       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/one_server.rs                             9       0  100.00%
crates/acp-nats/src/config.rs                                                           203       0  100.00%
crates/acp-nats/src/pending_prompt_waiters.rs                                           134       0  100.00%
crates/acp-nats/src/req_id.rs                                                            39       0  100.00%
crates/acp-nats/src/ext_method_name.rs                                                   68       0  100.00%
crates/acp-nats/src/acp_prefix.rs                                                        50       0  100.00%
crates/acp-nats/src/in_flight_slot_guard.rs                                              32       0  100.00%
crates/acp-nats/src/client_proxy.rs                                                     181       0  100.00%
crates/acp-nats/src/error.rs                                                             82       0  100.00%
crates/acp-nats/src/jsonrpc.rs                                                            6       0  100.00%
crates/acp-nats/src/lib.rs                                                               69       0  100.00%
crates/acp-nats/src/session_id.rs                                                        71       0  100.00%
TOTAL                                                                                 41240    1918  95.35%

Diff against main

Filename                                       Stmts    Miss  Cover
-------------------------------------------  -------  ------  --------
crates/trogon-telemetry/src/service_name.rs       +3       0  +100.00%
crates/trogon-mcp-gateway/src/authz.rs           +11     +11  +100.00%
crates/trogon-mcp-gateway/src/config.rs          +65     +65  +100.00%
crates/trogon-mcp-gateway/src/jwt.rs            +399    +231  +42.11%
crates/trogon-mcp-gateway/src/spicedb.rs        +114     +92  +19.30%
crates/trogon-mcp-gateway/src/subject.rs         +22      +6  +72.73%
crates/trogon-mcp-gateway/src/audit.rs           +46     +46  +100.00%
crates/trogon-mcp-gateway/src/main.rs            +75     +75  +100.00%
crates/trogon-mcp-gateway/src/trace.rs           +10     +10  +100.00%
crates/trogon-mcp-gateway/src/policy.rs          +33      +8  +75.76%
crates/trogon-mcp-gateway/src/gateway.rs        +550    +482  +12.36%
crates/acp-nats-server/src/transport.rs            0      +4  -0.22%
TOTAL                                          +1328   +1030  -2.43%

Results for commit: b23337d

Minimum allowed coverage is 95%

♻️ This comment has been updated with latest results

Comment thread rsworkspace/crates/trogon-mcp-gateway/src/gateway.rs Outdated
Comment thread rsworkspace/crates/trogon-mcp-gateway/src/gateway.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (1)
rsworkspace/crates/trogon-mcp-gateway/Cargo.toml (1)

26-26: ⚡ Quick win

Remove redundant bytes from dev-dependencies.

bytes is already declared in [dependencies] at line 12, making the dev-dependency declaration redundant.

♻️ Proposed fix
 [dev-dependencies]
-bytes = { workspace = true }
 uuid = { workspace = true }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-mcp-gateway/Cargo.toml` at line 26, The
dev-dependency declaration for the crate named "bytes" is redundant because
"bytes" is already declared in the [dependencies] section; remove the "bytes = {
workspace = true }" entry from the [dev-dependencies] section so the project
uses the single workspace dependency declaration instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rsworkspace/crates/trogon-mcp-gateway/src/authz.rs`:
- Around line 7-13: AuthzContext currently exposes primitive &str fields
(tenant, server_id, jsonrpc_method, tool_name); replace these with domain value
objects (e.g., TenantId/Option<TenantId>, ServerId, JsonRpcMethod, ToolName)
that validate at construction so invalid values are unrepresentable; implement
small constructors or TryFrom<&str>/new functions on ServerId, JsonRpcMethod,
ToolName, TenantId to enforce validation and convert existing call sites to
construct/propagate those types into the AuthzContext struct and any consumers
of AuthzContext (update references to AuthzContext::server_id, ::jsonrpc_method,
::tool_name, ::tenant accordingly).
- Around line 15-24: Replace the string-wrapper error type AuthzError(pub
String) with a typed enum (e.g., enum AuthzError { Unauthorized, InvalidToken,
IoError(std::io::Error), External(Box<dyn std::error::Error + Send + Sync>), ...
}) so callers can pattern-match and preserve context; update the impl
fmt::Display for AuthzError to match on the enum and render human-friendly
messages for each variant, implement std::error::Error for the enum (propagate
source errors via source() or #[from] conversions), and add From conversions
where appropriate so functions that previously returned AuthzError(String) now
return specific variants (refer to AuthzError, its Display impl, and the
std::error::Error impl to locate and change the code).

In `@rsworkspace/crates/trogon-mcp-gateway/src/gateway.rs`:
- Around line 23-31: GatewayError is currently a string wrapper (pub struct
GatewayError(pub String)) which erases source context; replace it with a typed
enum (e.g., enum GatewayError { PolicyInit{ source: Box<dyn std::error::Error +
Send + Sync> }, Subscription{ source: ... }, SubjectParse{ source: ... }, Authz{
source: ... }, Publish{ source: ... }, Io{ source: ... }, Other(String) }) or a
struct with explicit source fields, implement std::error::Error by returning
source via source(), and implement Display to include a clear message while
preserving the underlying error; update all places that construct GatewayError
(the constructors/uses referenced in gateway.rs lines around GatewayError, and
the other noted sites at 51-56, 112-113, 124, 133, 145, 351-359) to wrap the
original error into the appropriate enum variant (using Box::new(...) or the
concrete error type) rather than to_string()/format!(), so callers can match on
variants and retain original error context for observability and typed handling.
- Around line 114-117: The code currently drops malformed messages when
jsonrpc_method(&msg.payload) returns None (the jsonrpc_method check around
msg.payload and msg.subject) and simply returns Ok(()); instead, construct and
send an immediate JSON-RPC error response indicating "Missing method" (or
JSON-RPC Invalid Request) back to the caller using the same reply/publish path
the gateway uses for request/reply messages, then return after sending; replace
the warn+return path with logic that builds the standard JSON-RPC error object
(including the original id if present from msg.payload), sends it to the
requester's reply address (or via the same publishing function used elsewhere in
this module), and only then return Ok(()) so callers waiting for a reply receive
the protocol error.

In `@rsworkspace/crates/trogon-mcp-gateway/src/policy.rs`:
- Around line 7-16: PolicyError currently stores a raw String; change it to a
typed error (enum or struct) that preserves source errors from the
cel-interpreter instead of stringifying them—for example replace PolicyError(pub
String) with an enum PolicyError { Interpreter { source: Box<dyn
std::error::Error + Send + Sync> }, Other { msg: String } } (or a struct with a
source field) and implement Display and std::error::Error::source to return the
boxed source; then update any construction sites that created
PolicyError(String::from(...)) to wrap the original cel-interpreter error (e.g.,
PolicyError::Interpreter { source: Box::new(err) }) so the original error
context is preserved.
- Around line 24-28: The code in phase1_hardcoded currently discards the typed
error from Program::compile(SPICEDB_GATE_EXPR) by converting it to a String;
update PolicyError to include a variant (e.g.,
PolicyError::Compile(cel_interpreter::Error) or a boxed source:
PolicyError::Compile(Box<dyn std::error::Error + Send + Sync>)) and then change
the map_err to preserve the original error (e.g., .map_err(|e|
PolicyError::Compile(e)) or .map_err(PolicyError::from)), ensuring the concrete
error type from Program::compile is wrapped as the source instead of calling
to_string().
- Around line 30-43: The function requires_spicedb_for_method is converting
cel_interpreter errors to plain strings (via .to_string()) which loses typed
information; update PolicyError to hold the original error (e.g., add variants
or a boxed source like Box<dyn Error + Send + Sync> and implement From for
cel_interpreter error types), then replace the map_err calls around
cel_interpreter::to_value and self.program.execute to wrap/convert the original
error into PolicyError (e.g., map_err(Into::into) or map_err(|e|
PolicyError::ValueError(e))) so callers can inspect the underlying
cel_interpreter error instead of only a string; keep references to to_value(...)
failure and self.program.execute(...) failure when creating the new PolicyError
variants.

In `@rsworkspace/crates/trogon-mcp-gateway/src/trace.rs`:
- Line 24: The current use of self.inner.lock().expect("trace mutex poisoned")
will hard-panic on a poisoned mutex; replace both calls (the one creating let
mut guard = self.inner.lock()... at lines shown and the other at line 32) with
poison-aware handling that recovers the inner data instead of panicking: call
self.inner.lock() and on Err(poison) use poison.into_inner() (or
match/unwrap_or_else to obtain the inner guard) so the code continues serving
requests; preserve the existing variable name (guard) and semantics after you
recover the inner data.

In `@rsworkspace/crates/trogon-mcp-gateway/tests/e2e_nats_forward.rs`:
- Around line 50-53: The backend responder is currently swallowing publish and
flush errors by calling .await.ok() which hides failures; change the responder
to fail fast by asserting or unwrapping the results of publish_with_headers (the
call that sends Bytes::from(backend_reply) with async_nats::HeaderMap::new() and
subject reply) and backend_nats.flush().await so any send/flush error surfaces
in the test (e.g., replace the .ok() calls with expect/unwrap/assert on the
Result returned by publish_with_headers and flush).

---

Nitpick comments:
In `@rsworkspace/crates/trogon-mcp-gateway/Cargo.toml`:
- Line 26: The dev-dependency declaration for the crate named "bytes" is
redundant because "bytes" is already declared in the [dependencies] section;
remove the "bytes = { workspace = true }" entry from the [dev-dependencies]
section so the project uses the single workspace dependency declaration instead.
🪄 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: 7d51ec70-4432-41e5-b7d5-7e8949a1d32a

📥 Commits

Reviewing files that changed from the base of the PR and between 5f8d396 and 7b20dba.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • MCP_GATEWAY_PLAN.md
  • rsworkspace/crates/trogon-mcp-gateway/Cargo.toml
  • rsworkspace/crates/trogon-mcp-gateway/README.md
  • rsworkspace/crates/trogon-mcp-gateway/src/audit.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/authz.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/config.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/gateway.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/lib.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/main.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/policy.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/subject.rs
  • rsworkspace/crates/trogon-mcp-gateway/src/trace.rs
  • rsworkspace/crates/trogon-mcp-gateway/tests/e2e_nats_forward.rs
  • rsworkspace/crates/trogon-telemetry/src/service_name.rs

Comment on lines +7 to +13
#[derive(Clone, Debug)]
pub struct AuthzContext<'a> {
pub tenant: Option<&'a str>,
pub server_id: &'a str,
pub jsonrpc_method: &'a str,
pub tool_name: Option<&'a str>,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Prefer domain-specific value objects over primitive strings.

AuthzContext uses &str for tenant, server_id, jsonrpc_method, and tool_name. These represent domain concepts that should be value objects with validation guarantees (e.g., ServerId, JsonRpcMethod, ToolName).

Introducing domain types ensures correctness at construction and prevents invalid values from propagating through the authorization pipeline. As per coding guidelines: "Prefer domain-specific value objects over primitives (e.g., AcpPrefix instead of String). Each type's factory must guarantee correctness at construction—invalid instances should be unrepresentable."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-mcp-gateway/src/authz.rs` around lines 7 - 13,
AuthzContext currently exposes primitive &str fields (tenant, server_id,
jsonrpc_method, tool_name); replace these with domain value objects (e.g.,
TenantId/Option<TenantId>, ServerId, JsonRpcMethod, ToolName) that validate at
construction so invalid values are unrepresentable; implement small constructors
or TryFrom<&str>/new functions on ServerId, JsonRpcMethod, ToolName, TenantId to
enforce validation and convert existing call sites to construct/propagate those
types into the AuthzContext struct and any consumers of AuthzContext (update
references to AuthzContext::server_id, ::jsonrpc_method, ::tool_name, ::tenant
accordingly).

Comment on lines +15 to +24
#[derive(Debug)]
pub struct AuthzError(pub String);

impl fmt::Display for AuthzError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}

impl std::error::Error for AuthzError {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replace String-based error with typed variants.

AuthzError(pub String) violates coding guidelines: "Errors must be typed—use structs or enums, never String or format!()." String-based errors cannot be pattern-matched by callers and lose the ability to preserve underlying error context.

🔧 Recommended refactor to enum-based error
-#[derive(Debug)]
-pub struct AuthzError(pub String);
-
-impl fmt::Display for AuthzError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.write_str(&self.0)
-    }
-}
-
-impl std::error::Error for AuthzError {}
+#[derive(Debug)]
+pub enum AuthzError {
+    CheckFailed { reason: String },
+    Unreachable { source: Box<dyn std::error::Error + Send + Sync> },
+}
+
+impl fmt::Display for AuthzError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::CheckFailed { reason } => write!(f, "authorization check failed: {}", reason),
+            Self::Unreachable { source } => write!(f, "authorization service unreachable: {}", source),
+        }
+    }
+}
+
+impl std::error::Error for AuthzError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match self {
+            Self::Unreachable { source } => Some(&**source),
+            _ => None,
+        }
+    }
+}

As per coding guidelines: "Errors must be typed—use structs or enums, never String or format!()."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[derive(Debug)]
pub struct AuthzError(pub String);
impl fmt::Display for AuthzError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for AuthzError {}
#[derive(Debug)]
pub enum AuthzError {
CheckFailed { reason: String },
Unreachable { source: Box<dyn std::error::Error + Send + Sync> },
}
impl fmt::Display for AuthzError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CheckFailed { reason } => write!(f, "authorization check failed: {}", reason),
Self::Unreachable { source } => write!(f, "authorization service unreachable: {}", source),
}
}
}
impl std::error::Error for AuthzError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Unreachable { source } => Some(&**source),
_ => None,
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-mcp-gateway/src/authz.rs` around lines 15 - 24,
Replace the string-wrapper error type AuthzError(pub String) with a typed enum
(e.g., enum AuthzError { Unauthorized, InvalidToken, IoError(std::io::Error),
External(Box<dyn std::error::Error + Send + Sync>), ... }) so callers can
pattern-match and preserve context; update the impl fmt::Display for AuthzError
to match on the enum and render human-friendly messages for each variant,
implement std::error::Error for the enum (propagate source errors via source()
or #[from] conversions), and add From conversions where appropriate so functions
that previously returned AuthzError(String) now return specific variants (refer
to AuthzError, its Display impl, and the std::error::Error impl to locate and
change the code).

Comment on lines +23 to +31
pub struct GatewayError(pub String);

impl std::fmt::Display for GatewayError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}

impl std::error::Error for GatewayError {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replace stringly-typed gateway errors with structured variants and preserved sources.

GatewayError(pub String) plus to_string()/&'static str conversions erases source context across the pipeline (policy init, subscription, subject parsing, authz, publish/request). This makes typed handling and observability brittle.

💡 Proposed refactor sketch
-#[derive(Debug)]
-pub struct GatewayError(pub String);
+#[derive(Debug, thiserror::Error)]
+pub enum GatewayError {
+    #[error("policy initialization failed")]
+    PolicyInit {
+        #[source]
+        source: crate::policy::PolicyError,
+    },
+    #[error("subscription failed")]
+    Subscribe {
+        #[source]
+        source: async_nats::SubscribeError,
+    },
+    #[error("subject rewrite failed: {source}")]
+    SubjectRewrite {
+        #[from]
+        source: crate::subject::GatewaySubjectRewriteError,
+    },
+    #[error("invalid ingress subject: {source}")]
+    InvalidIngressSubject {
+        #[source]
+        source: ParseServerIdError,
+    },
+    // ...other typed variants for request/publish/authz...
+}

As per coding guidelines, "Errors must be typed—use structs or enums, never String or format!()" and "Never discard error context by converting a typed error into a string; wrap the source error as a field or variant instead."

Also applies to: 51-56, 112-113, 124-124, 133-133, 145-145, 351-359

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-mcp-gateway/src/gateway.rs` around lines 23 - 31,
GatewayError is currently a string wrapper (pub struct GatewayError(pub String))
which erases source context; replace it with a typed enum (e.g., enum
GatewayError { PolicyInit{ source: Box<dyn std::error::Error + Send + Sync> },
Subscription{ source: ... }, SubjectParse{ source: ... }, Authz{ source: ... },
Publish{ source: ... }, Io{ source: ... }, Other(String) }) or a struct with
explicit source fields, implement std::error::Error by returning source via
source(), and implement Display to include a clear message while preserving the
underlying error; update all places that construct GatewayError (the
constructors/uses referenced in gateway.rs lines around GatewayError, and the
other noted sites at 51-56, 112-113, 124, 133, 145, 351-359) to wrap the
original error into the appropriate enum variant (using Box::new(...) or the
concrete error type) rather than to_string()/format!(), so callers can match on
variants and retain original error context for observability and typed handling.

Comment on lines +114 to +117
let Some(jsonrpc_method) = jsonrpc_method(&msg.payload) else {
warn!(subject = %msg.subject, "ingress message has no JSON-RPC method");
return Ok(());
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return a JSON-RPC error when method is missing on request/reply ingress.

At Line 114-117, malformed request payloads are dropped after a warning. If the caller used request/reply, it waits until timeout with no protocol error response. Emit an immediate JSON-RPC error response instead.

💡 Proposed fix
     let Some(jsonrpc_method) = jsonrpc_method(&msg.payload) else {
         warn!(subject = %msg.subject, "ingress message has no JSON-RPC method");
+        if msg.reply.is_some() {
+            respond_with_jsonrpc_error(
+                client,
+                &msg,
+                jsonrpc_request_id(&msg.payload),
+                -32600,
+                "invalid JSON-RPC request: missing method".to_string(),
+            )
+            .await?;
+        }
         return Ok(());
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-mcp-gateway/src/gateway.rs` around lines 114 - 117,
The code currently drops malformed messages when jsonrpc_method(&msg.payload)
returns None (the jsonrpc_method check around msg.payload and msg.subject) and
simply returns Ok(()); instead, construct and send an immediate JSON-RPC error
response indicating "Missing method" (or JSON-RPC Invalid Request) back to the
caller using the same reply/publish path the gateway uses for request/reply
messages, then return after sending; replace the warn+return path with logic
that builds the standard JSON-RPC error object (including the original id if
present from msg.payload), sends it to the requester's reply address (or via the
same publishing function used elsewhere in this module), and only then return
Ok(()) so callers waiting for a reply receive the protocol error.

Comment on lines +7 to +16
#[derive(Debug)]
pub struct PolicyError(pub String);

impl std::fmt::Display for PolicyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}

impl std::error::Error for PolicyError {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replace String-based error with typed variants that preserve source errors.

PolicyError(pub String) violates coding guidelines. The error should preserve underlying error context from cel-interpreter instead of converting to String.

🔧 Recommended refactor to preserve error context
-#[derive(Debug)]
-pub struct PolicyError(pub String);
-
-impl std::fmt::Display for PolicyError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(&self.0)
-    }
-}
-
-impl std::error::Error for PolicyError {}
+#[derive(Debug)]
+pub enum PolicyError {
+    CompileFailed { source: cel_interpreter::ParseError },
+    ExecutionFailed { source: cel_interpreter::ExecutionError },
+    SerializationFailed { source: serde_json::Error },
+    UnexpectedType { expected: &'static str, actual: String },
+}
+
+impl std::fmt::Display for PolicyError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::CompileFailed { .. } => write!(f, "policy compilation failed"),
+            Self::ExecutionFailed { .. } => write!(f, "policy execution failed"),
+            Self::SerializationFailed { .. } => write!(f, "CEL value serialization failed"),
+            Self::UnexpectedType { expected, actual } => {
+                write!(f, "expected {} but got {}", expected, actual)
+            }
+        }
+    }
+}
+
+impl std::error::Error for PolicyError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match self {
+            Self::CompileFailed { source } => Some(source),
+            Self::ExecutionFailed { source } => Some(source),
+            Self::SerializationFailed { source } => Some(source),
+            _ => None,
+        }
+    }
+}

As per coding guidelines: "Errors must be typed—use structs or enums, never String or format!()" and "Never discard error context by converting a typed error into a string; wrap the source error as a field or variant instead."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[derive(Debug)]
pub struct PolicyError(pub String);
impl std::fmt::Display for PolicyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for PolicyError {}
#[derive(Debug)]
pub enum PolicyError {
CompileFailed { source: cel_interpreter::ParseError },
ExecutionFailed { source: cel_interpreter::ExecutionError },
SerializationFailed { source: serde_json::Error },
UnexpectedType { expected: &'static str, actual: String },
}
impl std::fmt::Display for PolicyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CompileFailed { .. } => write!(f, "policy compilation failed"),
Self::ExecutionFailed { .. } => write!(f, "policy execution failed"),
Self::SerializationFailed { .. } => write!(f, "CEL value serialization failed"),
Self::UnexpectedType { expected, actual } => {
write!(f, "expected {} but got {}", expected, actual)
}
}
}
}
impl std::error::Error for PolicyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::CompileFailed { source } => Some(source),
Self::ExecutionFailed { source } => Some(source),
Self::SerializationFailed { source } => Some(source),
_ => None,
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-mcp-gateway/src/policy.rs` around lines 7 - 16,
PolicyError currently stores a raw String; change it to a typed error (enum or
struct) that preserves source errors from the cel-interpreter instead of
stringifying them—for example replace PolicyError(pub String) with an enum
PolicyError { Interpreter { source: Box<dyn std::error::Error + Send + Sync> },
Other { msg: String } } (or a struct with a source field) and implement Display
and std::error::Error::source to return the boxed source; then update any
construction sites that created PolicyError(String::from(...)) to wrap the
original cel-interpreter error (e.g., PolicyError::Interpreter { source:
Box::new(err) }) so the original error context is preserved.

Comment on lines +24 to +28
pub fn phase1_hardcoded() -> Result<Self, PolicyError> {
Program::compile(SPICEDB_GATE_EXPR)
.map(|program| Self { program })
.map_err(|e| PolicyError(e.to_string()))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve typed error instead of converting to String.

Line 27 discards cel_interpreter's typed error by calling .to_string(). This loses the error source chain and prevents programmatic error handling.

🔧 Proposed fix (requires PolicyError refactor above)
     pub fn phase1_hardcoded() -> Result<Self, PolicyError> {
         Program::compile(SPICEDB_GATE_EXPR)
             .map(|program| Self { program })
-            .map_err(|e| PolicyError(e.to_string()))
+            .map_err(|source| PolicyError::CompileFailed { source })
     }

As per coding guidelines: "Never discard error context by converting a typed error into a string; wrap the source error as a field or variant instead."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-mcp-gateway/src/policy.rs` around lines 24 - 28,
The code in phase1_hardcoded currently discards the typed error from
Program::compile(SPICEDB_GATE_EXPR) by converting it to a String; update
PolicyError to include a variant (e.g.,
PolicyError::Compile(cel_interpreter::Error) or a boxed source:
PolicyError::Compile(Box<dyn std::error::Error + Send + Sync>)) and then change
the map_err to preserve the original error (e.g., .map_err(|e|
PolicyError::Compile(e)) or .map_err(PolicyError::from)), ensuring the concrete
error type from Program::compile is wrapped as the source instead of calling
to_string().

Comment on lines +30 to +43
pub fn requires_spicedb_for_method(&self, jsonrpc_method: &str) -> Result<bool, PolicyError> {
let mut ctx = Context::default();
let mcp = serde_json::json!({ "method": jsonrpc_method });
let value = cel_interpreter::to_value(&mcp).map_err(|e| PolicyError(e.to_string()))?;
ctx.add_variable_from_value("mcp", value);
match self.program.execute(&ctx) {
Ok(Value::Bool(b)) => Ok(b),
Ok(other) => Err(PolicyError(format!(
"policy expression must yield bool, got {:?}",
other.type_of()
))),
Err(e) => Err(PolicyError(e.to_string())),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve typed errors instead of converting to String.

Lines 33 and 41 discard typed errors from cel_interpreter by calling .to_string(). This loses error context and prevents callers from inspecting the underlying cause.

🔧 Proposed fix (requires PolicyError refactor above)
     pub fn requires_spicedb_for_method(&self, jsonrpc_method: &str) -> Result<bool, PolicyError> {
         let mut ctx = Context::default();
         let mcp = serde_json::json!({ "method": jsonrpc_method });
-        let value = cel_interpreter::to_value(&mcp).map_err(|e| PolicyError(e.to_string()))?;
+        let value = cel_interpreter::to_value(&mcp)
+            .map_err(|source| PolicyError::SerializationFailed { source })?;
         ctx.add_variable_from_value("mcp", value);
         match self.program.execute(&ctx) {
             Ok(Value::Bool(b)) => Ok(b),
-            Ok(other) => Err(PolicyError(format!(
-                "policy expression must yield bool, got {:?}",
-                other.type_of()
-            ))),
-            Err(e) => Err(PolicyError(e.to_string())),
+            Ok(other) => Err(PolicyError::UnexpectedType {
+                expected: "bool",
+                actual: format!("{:?}", other.type_of()),
+            }),
+            Err(source) => Err(PolicyError::ExecutionFailed { source }),
         }
     }

As per coding guidelines: "Never discard error context by converting a typed error into a string; wrap the source error as a field or variant instead."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-mcp-gateway/src/policy.rs` around lines 30 - 43,
The function requires_spicedb_for_method is converting cel_interpreter errors to
plain strings (via .to_string()) which loses typed information; update
PolicyError to hold the original error (e.g., add variants or a boxed source
like Box<dyn Error + Send + Sync> and implement From for cel_interpreter error
types), then replace the map_err calls around cel_interpreter::to_value and
self.program.execute to wrap/convert the original error into PolicyError (e.g.,
map_err(Into::into) or map_err(|e| PolicyError::ValueError(e))) so callers can
inspect the underlying cel_interpreter error instead of only a string; keep
references to to_value(...) failure and self.program.execute(...) failure when
creating the new PolicyError variants.


impl TraceStore {
pub fn insert(&self, request_id: impl Into<String>, trace: DecisionTrace) {
let mut guard = self.inner.lock().expect("trace mutex poisoned");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid panicking on trace lock poisoning in the request path.

At Line 24 and Line 32, expect(...) turns a prior panic while holding the mutex into repeated hard panics on future requests. Recover from poison and keep serving.

💡 Proposed fix
-        let mut guard = self.inner.lock().expect("trace mutex poisoned");
+        let mut guard = match self.inner.lock() {
+            Ok(g) => g,
+            Err(poisoned) => poisoned.into_inner(),
+        };
@@
-        self.inner.lock().expect("trace mutex poisoned").get(request_id).cloned()
+        let guard = match self.inner.lock() {
+            Ok(g) => g,
+            Err(poisoned) => poisoned.into_inner(),
+        };
+        guard.get(request_id).cloned()

Also applies to: 32-32

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-mcp-gateway/src/trace.rs` at line 24, The current
use of self.inner.lock().expect("trace mutex poisoned") will hard-panic on a
poisoned mutex; replace both calls (the one creating let mut guard =
self.inner.lock()... at lines shown and the other at line 32) with poison-aware
handling that recovers the inner data instead of panicking: call
self.inner.lock() and on Err(poison) use poison.into_inner() (or
match/unwrap_or_else to obtain the inner guard) so the code continues serving
requests; preserve the existing variable name (guard) and semantics after you
recover the inner data.

Comment on lines +50 to +53
.publish_with_headers(reply.to_string(), async_nats::HeaderMap::new(), Bytes::from(backend_reply))
.await
.ok();
backend_nats.flush().await.ok();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fail fast in the backend responder instead of swallowing send failures.

Line 50-53 currently hides publish/flush failures, which turns actionable failures into opaque request timeouts. Prefer explicit assertions in this test path.

💡 Proposed fix
                 backend_nats
                     .publish_with_headers(reply.to_string(), async_nats::HeaderMap::new(), Bytes::from(backend_reply))
                     .await
-                    .ok();
-                backend_nats.flush().await.ok();
+                    .expect("backend publish reply");
+                backend_nats.flush().await.expect("backend flush reply");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.publish_with_headers(reply.to_string(), async_nats::HeaderMap::new(), Bytes::from(backend_reply))
.await
.ok();
backend_nats.flush().await.ok();
.publish_with_headers(reply.to_string(), async_nats::HeaderMap::new(), Bytes::from(backend_reply))
.await
.expect("backend publish reply");
backend_nats.flush().await.expect("backend flush reply");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-mcp-gateway/tests/e2e_nats_forward.rs` around lines
50 - 53, The backend responder is currently swallowing publish and flush errors
by calling .await.ok() which hides failures; change the responder to fail fast
by asserting or unwrapping the results of publish_with_headers (the call that
sends Bytes::from(backend_reply) with async_nats::HeaderMap::new() and subject
reply) and backend_nats.flush().await so any send/flush error surfaces in the
test (e.g., replace the .ok() calls with expect/unwrap/assert on the Result
returned by publish_with_headers and flush).

Operators need reads and invokes checked by the same PDP without custom clients. Request-reply must not wedge when SpiceDB faults; aligning errors with Trogon RPC codes preserves client semantics.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
}
}
out
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SpiceDB object ID normalization has collision vulnerability

Medium Severity

normalize_spicedb_object_token uses _ as the escape prefix for non-allowed characters (e.g., :_3a), but _ itself is in the allowed-passthrough set. This makes the encoding non-injective — inputs "a_3a" and "a:" both normalize to "a_3a". Similarly, whitespace maps to _, colliding with literal underscores. In the authorization path, this could cause a SpiceDB permission check for one tool or resource to incorrectly match another.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 783b1ac. Configure here.

yordis added 3 commits May 22, 2026 06:28
Forgeable tenancy headers alone are inadequate when SpiceDB gates tools/call
and resources/read; callers need phased enforceable identity aligned with a
broker chokepoint rather than layering SPIRE or STS inside this binary.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Phase 1 already shipped chokepoint PDP and audit; the plan should reflect
ingress JWT so operators and reviewers do not assume header-only tenancy.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
…y egress

Server lanes must not derive principal facts from forgeable tenancy alone after ingress hardening, and authorization decisions deserve monotonic datastore bounds whenever SpiceDB returns snapshot tokens alongside results.

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

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 b23337d. Configure here.

}
}
out
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Client-injected gateway identity headers not stripped on egress

High Severity

When JWT mode is validate or require, egress_header_map only strips the trogon-mcp-tenant header but does not strip gateway-controlled identity headers (trogon-mcp-verified-sub, trogon-mcp-verified-tenant, trogon-mcp-identity-source, trogon-mcp-jwt-issuer). When identity resolves as non-JWT (e.g., no Bearer token in validate mode), append_verified_gateway_identity_headers early-returns without overwriting those headers. A malicious client can inject forged trogon-mcp-verified-sub values that reach backends as trusted identity.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b23337d. Configure here.

jsonwebtoken = { version = "=10.4.0", features = ["rust_crypto"] }
mcp-nats = { workspace = true }
pem = "=3.0.6"
reqwest = { version = "=0.12.22", default-features = false, features = ["json", "rustls-tls"] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Exact reqwest pin downgrades dependency for entire workspace

Medium Severity

The exact version pin reqwest = "=0.12.22" forces the entire workspace to downgrade from 0.12.28 to 0.12.22 (visible in the Cargo.lock diff). This affects all other workspace members that depend on reqwest, potentially losing bug fixes and security patches from the six intermediate versions.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b23337d. Configure here.

@yordis yordis closed this May 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant