Skip to content

fix: add per-route isolated tenant profile dimension to gateway routing#3190

Open
praisonai-triage-agent[bot] wants to merge 2 commits into
mainfrom
claude/issue-3189-20260719-0944
Open

fix: add per-route isolated tenant profile dimension to gateway routing#3190
praisonai-triage-agent[bot] wants to merge 2 commits into
mainfrom
claude/issue-3189-20260719-0944

Conversation

@praisonai-triage-agent

@praisonai-triage-agent praisonai-triage-agent Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Fixes #3189

Summary

Gateway routing could bind a chat to an agent but not to an isolated tenant profile, so memory/credentials/model were shared across every route. This adds the minimal core-protocol contract for per-route isolation β€” the 'secondary touch (core protocol)' the issue itself scopes as core's role β€” without building out the heavy wrapper subsystem (which would be scope creep with no live consumer).

Changes (core only, praisonaiagents/gateway/protocols.py)

  • RouteBinding gains an optional profile: Optional[str] field (the routing dimension it lacked), parsed by from_dict.
  • RouteMatch gains profile, and resolve_route populates it from the winning binding, so the wrapper can enter the profile's memory namespace / secret scope for the turn without re-resolving.
  • Fails closed: an unmatched route falls back with profile=None and never inherits another tenant's profile; most-specific-wins ordering is unchanged.

Why minimal

Per AGENTS.md, this package stays lightweight and powerful. Rather than adding a ProfileScope/ProfileResolverProtocol and wrapper isolation wiring with no live consumer, this adds the smallest backward-compatible contract that unblocks tenant isolation. Heavy memory/secret/home wiring belongs in the wrapper and can build on this contract.

Tests

Added TestProfileIsolation in tests/unit/test_route_bindings.py (default None, from_dict parse + string-coercion, matched-profile surfacing, fail-closed on unmatched, unscoped binding). All 55 route tests pass.

Backward-compatible: all new fields are optional; no new dependencies or exports.

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional profile isolation for individual routes.
    • Route matching now reports the profile associated with a matched route.
    • Profile values can be configured per route and are safely handled when omitted or unmatched.
  • Bug Fixes

    • Prevented unmatched or unscoped routes from inheriting another route’s profile.
  • Tests

    • Added coverage for profile parsing, matching, defaults, and fail-closed behavior.

…ng (fixes #3189)

Adds an optional `profile` field to the core RouteBinding routing contract
and surfaces the matched profile via RouteMatch/resolve_route. This gives
each route a named isolation scope the wrapper can key its memory namespace
/ secret scope off, so one gateway can multiplex tenants. Resolution is
most-specific-wins and fails closed: an unmatched route carries profile=None
and never inherits another tenant's profile.

Core-only, backward-compatible (all fields optional), no new deps/exports.

Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
@MervinPraison

Copy link
Copy Markdown
Owner

@coderabbitai review

@MervinPraison

Copy link
Copy Markdown
Owner

/review

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more β†’

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account β†’

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us β†’

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor
βœ… Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@MervinPraison MervinPraison added pipeline/blocked:ci Blocked: CI not green on HEAD pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet pipeline/final-claude-pending Reviews done; waiting for FINAL @claude labels Jul 19, 2026
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae766c2c-606d-4f6f-80cb-8f322de157ec

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review
πŸ“ Walkthrough

Walkthrough

Adds optional per-route profile support to gateway routing contracts. Route configuration parses profiles, route resolution returns the matched profile, and unit tests verify defaults, coercion, propagation, unscoped bindings, and unmatched-route behavior.

Changes

Route profile isolation

Layer / File(s) Summary
Profile contract and route resolution
src/praisonai-agents/praisonaiagents/gateway/protocols.py
RouteBinding and RouteMatch now expose optional profiles; configuration parsing reads them, and resolve_route propagates the matched profile.
Profile isolation validation
src/praisonai-agents/tests/unit/test_route_bindings.py
Unit tests validate profile defaults, string coercion, matched-route propagation, unscoped bindings, and fail-closed unmatched routes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: mervinpraison

πŸš₯ Pre-merge checks | βœ… 5
βœ… Passed checks (5 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title clearly describes the main change: adding per-route tenant profile routing.
Linked Issues check βœ… Passed The PR adds RouteBinding.profile, propagates it through route resolution, and tests fail-closed behavior and most-specific matching, matching #3189's core requirement.
Out of Scope Changes check βœ… Passed The changes stay within the routing contract and tests; no unrelated features or wrapper isolation wiring were introduced.
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-3189-20260719-0944

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.

@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a minimal, backward-compatible profile field to RouteBinding and RouteMatch so a gateway can tag each route with an isolated tenant-profile name (memory namespace / secret scope). resolve_route propagates the winning binding's profile to RouteMatch; unmatched routes and unscoped bindings always yield profile=None, satisfying the fail-closed contract.

  • RouteBinding.__post_init__ normalises blank and whitespace-only profile strings to None, matching the existing trust normalisation pattern and preventing a wrapper from accidentally entering an anonymous namespace.
  • from_dict parses and string-coerces the profile key; RouteMatch surfaces it so the wrapper can enter the correct isolation scope without re-resolving.
  • TestProfileIsolation covers all documented edge cases: default, parsing, integer coercion, matched-profile propagation, fail-closed fallback, unscoped binding, and profile="" normalisation via from_dict.

Confidence Score: 5/5

Safe to merge β€” all new fields are optional with None defaults, the blank-profile normalisation guard is correct and tested, and the fail-closed fallback path is unchanged.

The change is narrowly scoped to two dataclass fields and one line in resolve_route. The previously raised empty-profile normalisation concern is resolved by the post_init guard added in this PR, and the accompanying test explicitly covers from_dict with an empty profile string. No existing behaviour is altered.

No files require special attention.

Important Files Changed

Filename Overview
src/praisonai-agents/praisonaiagents/gateway/protocols.py Adds optional profile field to RouteBinding and RouteMatch; normalises blank profiles to None in __post_init__; propagates the winning binding's profile through resolve_route; fallback path keeps profile=None. All changes are backward-compatible and correctly fail-closed.
src/praisonai-agents/tests/unit/test_route_bindings.py Adds TestProfileIsolation class covering default-None, from_dict parsing, integer string-coercion, matched-profile surfacing, fail-closed on unmatched, unscoped binding, and blank/whitespace normalisation (including the previously flagged profile= empty string edge case via from_dict).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Inbound RouteFacts] --> B[resolve_route]
    B --> C{Any binding\nmatches?}
    C -- Yes --> D[Select highest-priority\nmost-specific binding]
    D --> E[RouteMatch\nagent = binding.agent\nbinding = best\nprofile = best.profile]
    E --> F{profile\nis None?}
    F -- No --> G[Wrapper enters\ntenant profile scope\nmemory / secrets / home]
    F -- Yes --> H[Wrapper skips profile scope\nroute is unscoped]
    C -- No --> I[RouteMatch\nagent = default_agent\nbinding = None\nprofile = None  fail-closed]
    I --> H

    subgraph RouteBinding.__post_init__
        J[profile blank or whitespace?] -->|Yes| K[profile = None unscoped]
        J -->|No| L[profile kept as-is]
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Inbound RouteFacts] --> B[resolve_route]
    B --> C{Any binding\nmatches?}
    C -- Yes --> D[Select highest-priority\nmost-specific binding]
    D --> E[RouteMatch\nagent = binding.agent\nbinding = best\nprofile = best.profile]
    E --> F{profile\nis None?}
    F -- No --> G[Wrapper enters\ntenant profile scope\nmemory / secrets / home]
    F -- Yes --> H[Wrapper skips profile scope\nroute is unscoped]
    C -- No --> I[RouteMatch\nagent = default_agent\nbinding = None\nprofile = None  fail-closed]
    I --> H

    subgraph RouteBinding.__post_init__
        J[profile blank or whitespace?] -->|Yes| K[profile = None unscoped]
        J -->|No| L[profile kept as-is]
    end
Loading

Reviews (2): Last reviewed commit: "fix: normalise blank route profile to No..." | Re-trigger Greptile

Comment thread src/praisonai-agents/tests/unit/test_route_bindings.py
@MervinPraison

Copy link
Copy Markdown
Owner

@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.

Phase 1: Review per AGENTS.md

  1. Protocol-driven: check heavy implementations vs core SDK
  2. Backward compatible: ensure zero feature regressions
  3. Performance: no hot-path regressions
  4. SDK value: review in depth whether the change genuinely adds value to the SDK β€” never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep
  5. Do not bloat the Agent class with additional params β€” only if absolutely required; we already support many params.
  6. Repo routing: agent-callable tools β†’ PraisonAI-Tools; lifecycle plugins β†’ PraisonAI-Plugins; optional sandbox backends β†’ PraisonAI-Plugins (praisonai.sandbox entry point) β€” request changes if wrongly added to praisonaiagents/

Phase 2: FIX Valid Issues
7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix
8. Also independently identify and fix any gaps or issues you find in the changed code β€” do not rely only on prior reviewer feedback
9. Push all code fixes directly to THIS branch (do NOT create a new PR)
10. Comment a summary of exact files modified and what you skipped

Phase 3: Final Verdict
11. If all issues are resolved, approve the PR / close the Issue
12. If blocking issues remain, request changes / leave clear action items

@MervinPraison MervinPraison added pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:cooldown Blocked: post-push or @claude cooldown and removed pipeline/final-claude-pending Reviews done; waiting for FINAL @claude pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet labels Jul 19, 2026
@praisonai-triage-agent

praisonai-triage-agent Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Claude finished @MervinPraison's task β€”β€” View job


Final Architecture Review β€” PR #3190 βœ…

  • Read AGENTS.md + changed files
  • Phase 1: Architecture review
  • Phase 2: Fix valid issue (blank-profile β†’ None normalization)
  • Run tests (31 passed)
  • Push + final verdict

Phase 1 β€” Architecture (per AGENTS.md)

Check Verdict
Protocol-driven βœ… Pure contract change in gateway/protocols.py. No heavy wrapper subsystem added β€” the PR correctly declined to build ProfileScope/resolver wiring with no live consumer.
Backward compatible βœ… All new fields (RouteBinding.profile, RouteMatch.profile) are Optional[str] = None. No exports, no deps, no signature changes.
Performance βœ… Only a field read on the resolve hot path; specificity/matching ordering unchanged.
SDK value βœ… Genuine value: closes a real tenant-isolation gap (memory/creds/model leaking across routes) with the minimal fail-closed contract. Not scope creep.
Agent class bloat βœ… No changes to Agent.
Repo routing βœ… Correctly a core routing contract β€” belongs in praisonaiagents/. Heavy isolation wiring rightly deferred to the wrapper.

Phase 2 β€” Valid issue fixed

Greptile (p1, valid): profile: "" from a YAML config passed through _as_opt_str("") β†’ "", an empty-named scope that a wrapper checking if profile is not None would treat as scoped β€” violating the docstring's "None means unscoped" fail-closed contract. The sibling trust field already guards this in __post_init__; profile did not.

Fix pushed (commit 3a4b94b):

  • protocols.py RouteBinding.__post_init__: coerce blank/whitespace-only profile β†’ None (mirrors the existing trust normalization); docstring updated.
  • test_route_bindings.py: added test_blank_profile_is_normalised_to_none covering "" / " " via both direct construction and from_dict.

Skipped: No other changes. CodeRabbit/Qodo raised no actionable items; Greptile's suggested test is subsumed by the added test. No wrapper isolation wiring added (correctly out of scope, no live consumer).

Files modified

  • src/praisonai-agents/praisonaiagents/gateway/protocols.py
  • src/praisonai-agents/tests/unit/test_route_bindings.py

Tests

31 passed (route binding suite), including the new fail-closed case.

Phase 3 β€” Verdict

βœ… Approve. The change is minimal, protocol-driven, backward-compatible, and now consistently fail-closed for blank profiles. Ready to merge.
β€’ Branch

Empty/whitespace-only profile strings were passing through _as_opt_str
as non-None values, making them indistinguishable from a named scope
and violating the documented 'None means unscoped' fail-closed contract.
Mirror the existing trust normalisation in __post_init__ and add tests.

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
@MervinPraison MervinPraison added pipeline/blocked:stale-final Blocked: FINAL stale after new commits and removed pipeline/blocked:stale-final Blocked: FINAL stale after new commits labels Jul 19, 2026
@praisonai-triage-agent praisonai-triage-agent Bot removed the pipeline/blocked:cooldown Blocked: post-push or @claude cooldown label Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:ci Blocked: CI not green on HEAD

Projects

None yet

1 participant