Skip to content

Refactor policy gateway into hexagonal architecture - #2

Merged
SPRIME01 merged 15 commits into
mainfrom
codex/set-up-ci-with-github-actions
Nov 12, 2025
Merged

SPRIME01 merged 15 commits into
mainfrom
codex/set-up-ci-with-github-actions

Conversation

@SPRIME01

@SPRIME01 SPRIME01 commented Nov 11, 2025 •

Copy link
Copy Markdown
Collaborator

Summary

  • refactor the FastAPI policy gateway into a hexagonal architecture with application services and ports
  • add a file-based configuration adapter and dedicated HTTP schemas to isolate framework code from domain logic
  • cover policy decisions and configuration loading with unit tests to support future changes

Testing

  • PYTHONPATH=services/policy-gateway/src pytest services/policy-gateway/tests

Codex Task

Summary by Sourcery

Restructure the FastAPI policy gateway into a hexagonal architecture by extracting domain models, application services, and ports, introducing a file‐based configuration adapter and HTTP schemas, and replacing inline decision logic with dependency‐injected service calls

Enhancements:

  • Organize code into application services, domain models, ports, adapters, and HTTP interface following hexagonal architecture
  • Decouple framework-specific code from domain logic by adding ConfigFileAdapter and Pydantic interface schemas
  • Refactor FastAPI routes to depend on PolicyDecisionService for health checks, rule retrieval, and decision endpoints

Tests:

  • Add unit tests for prompt/output decision rules, CI checks, and configuration adapter behavior

Summary by CodeRabbit

  • New Features

    • LLM proxy endpoints (completion + streaming SSE) and typed completion request/response models.
  • Refactor

    • All HTTP endpoints now delegate to a centralized policy decision service via dependency injection; responses standardized for health, rules, filtering, and CI checks.
  • Tests

    • Added unit, integration, and SSE tests covering decisions, CI checks, proxy, and streaming.
  • Documentation

    • Expanded README and reference docs with usage, streaming, provider selection, and deployment guidance.
  • Chores

    • Added dev-setup task/script, CI workflow, and dev requirements.

@sourcery-ai

sourcery-ai Bot commented Nov 11, 2025 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR refactors the FastAPI policy gateway into a hexagonal architecture by extracting decision logic into an application service with defined ports and domain models, isolating framework code through HTTP schemas and a file-based configuration adapter, and adds unit tests to cover policy decisions and config loading.

Sequence diagram for a prompt filter request

sequenceDiagram
    actor User
    participant FastAPI
    participant Schemas
    participant PolicyDecisionService
    participant ConfigFileAdapter
    User->>FastAPI: POST /filter/prompt
    FastAPI->>Schemas: Parse PromptCheckRequest
    FastAPI->>PolicyDecisionService: decide_prompt(PromptDecisionInput)
    PolicyDecisionService->>ConfigFileAdapter: load()
    PolicyDecisionService-->>FastAPI: DecisionResult
    FastAPI->>Schemas: DecisionResponse
    FastAPI-->>User: DecisionResponse
Loading

Sequence diagram for CI check request

sequenceDiagram
    actor User
    participant FastAPI
    participant Schemas
    participant PolicyDecisionService
    participant ConfigFileAdapter
    User->>FastAPI: POST /ci/check
    FastAPI->>Schemas: Parse CiCheckRequest
    FastAPI->>PolicyDecisionService: ci_check(CiCheckInput)
    PolicyDecisionService->>ConfigFileAdapter: load()
    PolicyDecisionService-->>FastAPI: CiCheckResult
    FastAPI->>Schemas: CiCheckResponse
    FastAPI-->>User: CiCheckResponse
Loading

Class diagram for new domain models and service

classDiagram
    class PolicyDecisionService {
        - ConfigurationPort _configuration_port
        + health() Dict[str, str]
        + rules() RulesSnapshot
        + decide_prompt(request: PromptDecisionInput) DecisionResult
        + decide_output(request: OutputDecisionInput) DecisionResult
        + ci_check(request: CiCheckInput) CiCheckResult
    }
    class ConfigurationPort {
        <<interface>>
        + load() dict
    }
    class ConfigFileAdapter {
        - path: str | os.PathLike[str]
        + load() dict
    }
    class PromptDecisionInput {
        + prompt: str
        + context: Dict[str, object]
    }
    class OutputDecisionInput {
        + output: str
        + context: Dict[str, object]
    }
    class CiCheckInput {
        + quality: Dict[str, float]
        + fairness: Dict[str, float]
        + safety: Dict[str, float]
        + drift: Dict[str, float]
    }
    class DecisionResult {
        + allowed: bool
        + action: str
        + reasons: List[str]
    }
    class CiCheckResult {
        + status: str
        + violations: List[str]
    }
    class RulesSnapshot {
        + raw: Dict[str, object]
        + to_dict() Dict[str, object]
    }
    PolicyDecisionService --> ConfigurationPort
    ConfigFileAdapter --|> ConfigurationPort
    PolicyDecisionService --> PromptDecisionInput
    PolicyDecisionService --> OutputDecisionInput
    PolicyDecisionService --> CiCheckInput
    PolicyDecisionService --> DecisionResult
    PolicyDecisionService --> CiCheckResult
    PolicyDecisionService --> RulesSnapshot
Loading

Class diagram for HTTP schemas

classDiagram
    class PromptCheckRequest {
        + prompt: str
        + context: Dict[str, Any] | None
    }
    class OutputCheckRequest {
        + output: str
        + context: Dict[str, Any] | None
    }
    class CiCheckRequest {
        + quality: Dict[str, float]
        + fairness: Dict[str, float]
        + safety: Dict[str, float]
        + drift: Dict[str, float]
    }
    class DecisionResponse {
        + allowed: bool
        + action: str
        + reasons: list[str]
    }
    class CiCheckResponse {
        + status: str
        + violations: list[str]
    }
Loading

File-Level Changes

Change Details Files
Introduce hexagonal layers with application service, ports, domain models, and adapters
  • Extract decision logic into PolicyDecisionService
  • Define ConfigurationPort protocol
  • Create domain model classes for inputs, results, and rules snapshot
  • Implement ConfigFileAdapter for file-based config loading
  • Add Pydantic HTTP schemas to isolate framework code
src/policy_gateway/application/services.py
src/policy_gateway/ports/configuration.py
src/policy_gateway/domain/models.py
src/policy_gateway/infrastructure/config_file_adapter.py
src/policy_gateway/interface/http/schemas.py
src/policy_gateway/__init__.py
Refactor FastAPI app to leverage service and dependency injection
  • Remove inline decision and config-loading logic
  • Build singleton PolicyDecisionService via ConfigFileAdapter
  • Use Depends(get_service) in routes and update handlers to use domain inputs and response models
src/app.py
Add comprehensive unit tests for service logic and config adapter
  • Test prompt and output decision branches using InMemoryConfigAdapter
  • Validate CI checks against configured thresholds
  • Verify ConfigFileAdapter handles YAML, JSON, and missing files
tests/test_policy_service.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Nov 11, 2025 •

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Warning

.coderabbit.yml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: String must contain at most 250 character(s) at "tone_instructions"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Adds a PolicyDecisionService and domain models, ports and adapters (ConfigFileAdapter, HTTPLLMAdapter, LiteLLMAdapter), DI-wired FastAPI endpoints including LLM proxy and SSE streaming, Pydantic HTTP schemas, OpenAPI updates, unit/integration tests, dev tooling, CI workflow, and documentation updates.

Changes

Cohort / File(s) Summary
App / Endpoints
services/policy-gateway/src/app.py
Adds _build_service and get_service DI, moves health/rules/filter/ci endpoints to service delegation, adds /proxy/completion and /proxy/completion/stream (SSE) proxy endpoints and LLM adapter selection.
Application service
services/policy-gateway/src/policy_gateway/application/services.py, services/policy-gateway/src/policy_gateway/__init__.py
New PolicyDecisionService with health(), rules(), decide_prompt(), decide_output(), ci_check() and _lookup_threshold(); exported via package __init__.
Domain models
services/policy-gateway/src/policy_gateway/domain/models.py
New frozen dataclasses and LLM models: PromptDecisionInput, OutputDecisionInput, CiCheckInput, DecisionResult, CiCheckResult, RulesSnapshot, CompletionRequest, CompletionResponse with serialization helpers.
Ports
services/policy-gateway/src/policy_gateway/ports/configuration.py, services/policy-gateway/src/policy_gateway/ports/llm_adapter.py
New protocol interfaces: ConfigurationPort (load) and LLMAdapterPort (complete, stream).
Infrastructure adapters
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py, .../litellm_adapter.py, .../llm_http_adapter.py
New ConfigFileAdapter (YAML/JSON loader, returns {} on missing/parse errors), LiteLLMAdapter (litellm integration with streaming and fallbacks), and HTTPLLMAdapter (proxy to upstream HTTP completion with streaming fallback).
HTTP schemas / interface
services/policy-gateway/src/policy_gateway/interface/http/schemas.py, specs/openapi-policy-gateway.yaml
Pydantic request/response models for prompt/output/ci and completion proxy; OpenAPI adds /proxy/completion and CompletionRequest/CompletionResponse schemas; minor schema styling changes.
Tests
services/policy-gateway/tests/*, services/policy-gateway/tests/test_policy_service.py, .../test_http_responses.py, .../test_integration.py, .../test_llm_proxy.py, .../test_streaming.py
New unit and integration tests for PolicyDecisionService logic, config adapter, HTTP mapping, LLM proxy forwarding, and SSE streaming behavior.
Dev tooling & CI
scripts/dev_setup.sh, services/policy-gateway/requirements-dev.txt, .github/workflows/ci.yml, justfile, .gitignore
Adds development setup script, dev requirements, CI workflow, just task dev-setup, and ignores .venv/.
Docs & config
README.md, docs/..., docs/Reference/LLMOps_Specs/*, tmp_cfg*.yaml, .coderabbit.yml
Extensive documentation updates (ENV, ADRs, LLMOps spec updates introducing PAC_LLM_PROVIDER and streaming), example config files, automation config, and OpenAPI docs adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as FastAPI
    participant Service as PolicyDecisionService
    participant Config as ConfigFileAdapter
    participant Adapter as LLMAdapter

    Client->>API: POST /filter/prompt (PromptCheckRequest)
    API->>API: Depends(get_service) -> get_service()
    API->>Service: decide_prompt(PromptDecisionInput)
    Service->>Config: load()
    Config-->>Service: config dict
    Service-->>API: DecisionResult
    API-->>Client: DecisionResponse (JSON)

    opt Proxy completion (sync)
      Client->>API: POST /proxy/completion (CompletionRequest)
      API->>API: _build_llm_adapter(PAC_LLM_PROVIDER)
      API->>Adapter: complete(CompletionRequest)
      Adapter-->>API: CompletionResponse
      API-->>Client: CompletionResponse (JSON)
    end

    opt Proxy completion (stream)
      Client->>API: POST /proxy/completion/stream (CompletionRequest)
      API->>API: _build_llm_adapter(PAC_LLM_PROVIDER)
      API->>Adapter: stream(CompletionRequest)
      loop chunks
        Adapter-->>API: chunk
        API-->>Client: SSE data: chunk
      end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Files/areas to focus review on:
    • PolicyDecisionService: threshold extraction, numeric conversions, edge-case handling in decide_prompt/decide_output/ci_check.
    • Adapter implementations: error normalization, parsing of variable upstream shapes, and streaming fallbacks in LiteLLMAdapter and HTTPLLMAdapter.
    • ConfigFileAdapter.load: silent-failure behavior on missing/parse errors vs. logging/visibility.
    • app.py DI wiring and runtime LLM adapter selection via PAC_LLM_PROVIDER.
    • SSE formatting and streaming correctness in /proxy/completion/stream and corresponding tests.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Refactor policy gateway into hexagonal architecture' directly and clearly summarizes the main change: restructuring the policy gateway codebase using hexagonal (ports and adapters) architecture principles.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87b34ff and 62c6bae.

📒 Files selected for processing (8)
  • .vscode/settings.json (1 hunks)
  • README.md (7 hunks)
  • scripts/dev_setup.sh (1 hunks)
  • services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/infrastructure/litellm_adapter.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/infrastructure/llm_http_adapter.py (1 hunks)
  • services/policy-gateway/tests/test_llm_proxy.py (1 hunks)
  • specs/openapi-policy-gateway.yaml (7 hunks)

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.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @SPRIME01, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant architectural overhaul to the policy gateway service, transitioning it to a hexagonal architecture. The primary goal is to decouple the core business logic from external concerns such as the web framework and data persistence, thereby enhancing the system's testability, maintainability, and adaptability. This refactoring involves structuring the codebase into distinct layers, utilizing dependency inversion through ports and adapters, and establishing clear boundaries between different components.

Highlights

  • Architectural Refactoring: The FastAPI policy gateway has been refactored to adhere to a hexagonal architecture, promoting better separation of concerns and modularity.
  • Application Services and Ports: Introduced dedicated application services and abstract ports to encapsulate core domain logic and define clear interfaces for external dependencies.
  • Configuration Management: A new file-based configuration adapter has been added, isolating the logic for loading configuration from the main application flow.
  • HTTP Schema Isolation: Dedicated HTTP schemas are now used to separate framework-specific data models from the core domain logic, enhancing maintainability.
  • Improved Test Coverage: Comprehensive unit tests have been added to cover policy decisions and configuration loading, ensuring robustness and supporting future changes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • Consider instantiating or refreshing the PolicyDecisionService per-request (rather than as a static global) if you need to pick up config changes at runtime.
  • ConfigFileAdapter.load currently swallows all exceptions silently—adding logging or error reporting would help surface issues in malformed or unreadable config files.
  • The drift field on CiCheckInput isn’t used in the ci_check implementation; either remove it or add logic to incorporate drift checks to prevent confusion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider instantiating or refreshing the PolicyDecisionService per-request (rather than as a static global) if you need to pick up config changes at runtime.
- ConfigFileAdapter.load currently swallows all exceptions silently—adding logging or error reporting would help surface issues in malformed or unreadable config files.
- The `drift` field on CiCheckInput isn’t used in the ci_check implementation; either remove it or add logic to incorporate drift checks to prevent confusion.

## Individual Comments

### Comment 1
<location> `services/policy-gateway/src/policy_gateway/application/services.py:31-35` </location>
<code_context>
+
+    def decide_prompt(self, request: PromptDecisionInput) -> DecisionResult:
+        context = request.context or {}
+        if context.get("contains_pii") and not context.get("lawful_basis"):
+            return DecisionResult(
+                allowed=False,
+                action="block",
+                reasons=["PII without lawful basis"],
+            )
+        if (context.get("jailbreak_score") or 0) > 0.8:
</code_context>

<issue_to_address>
**question:** PII check logic may not handle cases where lawful_basis is present but empty or falsy.

If you require lawful_basis to be a specific non-empty value, consider checking its type or content directly rather than relying on its truthiness.
</issue_to_address>

### Comment 2
<location> `services/policy-gateway/src/policy_gateway/application/services.py:37` </location>
<code_context>
+                action="block",
+                reasons=["PII without lawful basis"],
+            )
+        if (context.get("jailbreak_score") or 0) > 0.8:
+            return DecisionResult(
+                allowed=True,
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Defaulting jailbreak_score to 0 may mask missing or malformed input.

Explicitly handle cases where jailbreak_score is missing or not a float, such as by logging or raising an error, to avoid unintended behavior.

Suggested implementation:

```python
        jailbreak_score = context.get("jailbreak_score")
        if jailbreak_score is None:
            # Log missing jailbreak_score
            logger.warning("Missing jailbreak_score in context for prompt decision")
        else:
            try:
                score = float(jailbreak_score)
                if score > 0.8:
                    return DecisionResult(
                        allowed=True,
                        action="safe_mode",
                        reasons=["high jailbreak score"],
                    )
            except (TypeError, ValueError):
                logger.warning(f"Malformed jailbreak_score in context: {jailbreak_score}")

```

- Ensure that a `logger` is available in this module. If not, you may need to import and configure Python's logging module at the top of the file:
    ```python
    import logging
    logger = logging.getLogger(__name__)
    ```
- If you prefer to raise an error instead of logging, replace the `logger.warning` lines with `raise ValueError(...)`.
- Adjust the logging level and error handling to match your application's conventions.
</issue_to_address>

### Comment 3
<location> `services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py:25` </location>
<code_context>
-        if CFG_PATH.endswith((".yml", ".yaml")):
-            return yaml.safe_load(open(CFG_PATH))
-        return json.load(open(CFG_PATH))
-    except Exception:
-        return {}
+def _build_service() -> PolicyDecisionService:
</code_context>

<issue_to_address>
**suggestion:** Exception handling in config loader is too broad.

Catching all exceptions can hide specific errors like permission issues or config syntax problems. Please narrow the exception type and add logging for better debuggability.
</issue_to_address>

### Comment 4
<location> `services/policy-gateway/tests/test_policy_service.py:25-33` </location>
<code_context>
+    return PolicyDecisionService(InMemoryConfigAdapter(config or {}))
+
+
+def test_prompt_blocks_when_pii_without_basis():
+    service = create_service()
+    request = PromptDecisionInput(prompt="hi", context={"contains_pii": True})
+    result = service.decide_prompt(request)
+    assert result.allowed is False
+    assert result.action == "block"
+    assert "PII without lawful basis" in result.reasons
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Missing test for lawful basis present with PII.

Add a test where both 'contains_pii' is True and 'lawful_basis' is present in the context to confirm the prompt is allowed as expected.

```suggestion
def test_prompt_blocks_when_pii_without_basis():
    service = create_service()
    request = PromptDecisionInput(prompt="hi", context={"contains_pii": True})
    result = service.decide_prompt(request)
    assert result.allowed is False
    assert result.action == "block"
    assert "PII without lawful basis" in result.reasons


def test_prompt_allows_when_pii_with_lawful_basis():
    service = create_service()
    request = PromptDecisionInput(
        prompt="hi",
        context={"contains_pii": True, "lawful_basis": "consent"}
    )
    result = service.decide_prompt(request)
    assert result.allowed is True
    assert result.action != "block"
    assert "PII without lawful basis" not in result.reasons
```
</issue_to_address>

### Comment 5
<location> `services/policy-gateway/tests/test_policy_service.py:33-40` </location>
<code_context>
+    assert "PII without lawful basis" in result.reasons
+
+
+def test_prompt_safe_mode_when_jailbreak_high():
+    service = create_service()
+    request = PromptDecisionInput(prompt="hi", context={"jailbreak_score": 0.9})
+    result = service.decide_prompt(request)
+    assert result.allowed is True
+    assert result.action == "safe_mode"
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Missing test for jailbreak_score at boundary values.

Add tests for 'jailbreak_score' at 0.8 and 0.79 to verify correct threshold behavior for 'safe_mode'.

```suggestion

def test_prompt_safe_mode_when_jailbreak_high():
    service = create_service()
    request = PromptDecisionInput(prompt="hi", context={"jailbreak_score": 0.9})
    result = service.decide_prompt(request)
    assert result.allowed is True
    assert result.action == "safe_mode"

def test_prompt_safe_mode_when_jailbreak_at_threshold():
    service = create_service()
    request = PromptDecisionInput(prompt="hi", context={"jailbreak_score": 0.8})
    result = service.decide_prompt(request)
    assert result.allowed is True
    assert result.action == "safe_mode"

def test_prompt_no_safe_mode_when_jailbreak_below_threshold():
    service = create_service()
    request = PromptDecisionInput(prompt="hi", context={"jailbreak_score": 0.79})
    result = service.decide_prompt(request)
    assert result.allowed is True
    assert result.action != "safe_mode"
```
</issue_to_address>

### Comment 6
<location> `services/policy-gateway/tests/test_policy_service.py:42-47` </location>
<code_context>
+    assert result.action == "safe_mode"
+
+
+def test_output_summarize_when_verbatim_high():
+    service = create_service()
+    request = OutputDecisionInput(output="o", context={"verbatim_ratio": 0.3})
+    result = service.decide_output(request)
+    assert result.allowed is True
+    assert result.action == "summarize"
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Missing test for verbatim_ratio at and below threshold.

Add tests for 'verbatim_ratio' at 0.2 and just below (e.g., 0.19) to confirm the action only triggers when the ratio exceeds 0.2.

```suggestion
def test_output_summarize_when_verbatim_high():
    service = create_service()
    request = OutputDecisionInput(output="o", context={"verbatim_ratio": 0.3})
    result = service.decide_output(request)
    assert result.allowed is True
    assert result.action == "summarize"


def test_output_no_summarize_when_verbatim_at_threshold():
    service = create_service()
    request = OutputDecisionInput(output="o", context={"verbatim_ratio": 0.2})
    result = service.decide_output(request)
    assert result.allowed is True
    assert result.action != "summarize"


def test_output_no_summarize_when_verbatim_below_threshold():
    service = create_service()
    request = OutputDecisionInput(output="o", context={"verbatim_ratio": 0.19})
    result = service.decide_output(request)
    assert result.allowed is True
    assert result.action != "summarize"
```
</issue_to_address>

### Comment 7
<location> `services/policy-gateway/tests/test_policy_service.py:50-59` </location>
<code_context>
+    assert result.action == "summarize"
+
+
+def test_ci_check_detects_violations():
+    cfg = {
+        "thresholds": {
+            "quality": {"pass_at_5": {"target": 0.8}},
+            "fairness": {"subgroup_delta": {"target_max": 0.2}},
+            "safety": {"harmful_rate": {"target_max": 0.1}},
+        }
+    }
+    service = create_service(cfg)
+    request = CiCheckInput(
+        quality={"pass_at_5": 0.7},
+        fairness={"subgroup_delta": 0.3},
+        safety={"harmful_rate": 0.2},
+        drift={},
+    )
+    result = service.ci_check(request)
+    assert result.status == "fail"
+    assert set(result.violations) == {
+        "quality.pass_at_5",
+        "fairness.subgroup_delta",
</code_context>

<issue_to_address>
**suggestion (testing):** Missing test for partial CI check violations.

Please add tests where only a subset of CI check metrics fail, to ensure the service reports violations accurately.
</issue_to_address>

### Comment 8
<location> `services/policy-gateway/tests/test_policy_service.py:94-100` </location>
<code_context>
+    assert result.violations == []
+
+
+def test_config_file_adapter_reads_yaml(tmp_path: Path):
+    config_data = {"foo": "bar"}
+    cfg_path = tmp_path / "config.yaml"
+    cfg_path.write_text(yaml.safe_dump(config_data))
+
+    adapter = ConfigFileAdapter(cfg_path)
+    assert adapter.load() == config_data
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Missing test for invalid YAML/JSON file content.

Add tests to verify that ConfigFileAdapter returns an empty dict and handles errors gracefully when loading invalid YAML or JSON.

```suggestion
def test_config_file_adapter_reads_yaml(tmp_path: Path):
    config_data = {"foo": "bar"}
    cfg_path = tmp_path / "config.yaml"
    cfg_path.write_text(yaml.safe_dump(config_data))

    adapter = ConfigFileAdapter(cfg_path)
    assert adapter.load() == config_data


def test_config_file_adapter_invalid_yaml(tmp_path: Path):
    # Write invalid YAML content
    cfg_path = tmp_path / "invalid.yaml"
    cfg_path.write_text("foo: [unclosed_list\nbar: baz")

    adapter = ConfigFileAdapter(cfg_path)
    assert adapter.load() == {}


def test_config_file_adapter_invalid_json(tmp_path: Path):
    # Write invalid JSON content
    cfg_path = tmp_path / "invalid.json"
    cfg_path.write_text('{"foo": "bar", "baz": [1, 2,}')  # Invalid JSON

    adapter = ConfigFileAdapter(cfg_path)
    assert adapter.load() == {}
```
</issue_to_address>

### Comment 9
<location> `services/policy-gateway/src/policy_gateway/application/services.py:75` </location>
<code_context>
        status = "pass" if not violations else "fail"

</code_context>

<issue_to_address>
**suggestion (code-quality):** Swap if/else branches of if expression to remove negation ([`swap-if-expression`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/swap-if-expression))

```suggestion
        status = "fail" if violations else "pass"
```

<br/><details><summary>Explanation</summary>Negated conditions are more difficult to read than positive ones, so it is best
to avoid them where we can. By swapping the `if` and `else` conditions around we
can invert the condition and make it positive.
</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +31 to +35
if context.get("contains_pii") and not context.get("lawful_basis"):
return DecisionResult(
allowed=False,
action="block",
reasons=["PII without lawful basis"],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question: PII check logic may not handle cases where lawful_basis is present but empty or falsy.

If you require lawful_basis to be a specific non-empty value, consider checking its type or content directly rather than relying on its truthiness.

action="block",
reasons=["PII without lawful basis"],
)
if (context.get("jailbreak_score") or 0) > 0.8:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Defaulting jailbreak_score to 0 may mask missing or malformed input.

Explicitly handle cases where jailbreak_score is missing or not a float, such as by logging or raising an error, to avoid unintended behavior.

Suggested implementation:

        jailbreak_score = context.get("jailbreak_score")
        if jailbreak_score is None:
            # Log missing jailbreak_score
            logger.warning("Missing jailbreak_score in context for prompt decision")
        else:
            try:
                score = float(jailbreak_score)
                if score > 0.8:
                    return DecisionResult(
                        allowed=True,
                        action="safe_mode",
                        reasons=["high jailbreak score"],
                    )
            except (TypeError, ValueError):
                logger.warning(f"Malformed jailbreak_score in context: {jailbreak_score}")
  • Ensure that a logger is available in this module. If not, you may need to import and configure Python's logging module at the top of the file:
    import logging
    logger = logging.getLogger(__name__)
  • If you prefer to raise an error instead of logging, replace the logger.warning lines with raise ValueError(...).
  • Adjust the logging level and error handling to match your application's conventions.

Comment on lines +33 to +40

def test_prompt_safe_mode_when_jailbreak_high():
service = create_service()
request = PromptDecisionInput(prompt="hi", context={"jailbreak_score": 0.9})
result = service.decide_prompt(request)
assert result.allowed is True
assert result.action == "safe_mode"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Missing test for jailbreak_score at boundary values.

Add tests for 'jailbreak_score' at 0.8 and 0.79 to verify correct threshold behavior for 'safe_mode'.

Suggested change
def test_prompt_safe_mode_when_jailbreak_high():
service = create_service()
request = PromptDecisionInput(prompt="hi", context={"jailbreak_score": 0.9})
result = service.decide_prompt(request)
assert result.allowed is True
assert result.action == "safe_mode"
def test_prompt_safe_mode_when_jailbreak_high():
service = create_service()
request = PromptDecisionInput(prompt="hi", context={"jailbreak_score": 0.9})
result = service.decide_prompt(request)
assert result.allowed is True
assert result.action == "safe_mode"
def test_prompt_safe_mode_when_jailbreak_at_threshold():
service = create_service()
request = PromptDecisionInput(prompt="hi", context={"jailbreak_score": 0.8})
result = service.decide_prompt(request)
assert result.allowed is True
assert result.action == "safe_mode"
def test_prompt_no_safe_mode_when_jailbreak_below_threshold():
service = create_service()
request = PromptDecisionInput(prompt="hi", context={"jailbreak_score": 0.79})
result = service.decide_prompt(request)
assert result.allowed is True
assert result.action != "safe_mode"

Comment on lines +50 to +59
def test_ci_check_detects_violations():
cfg = {
"thresholds": {
"quality": {"pass_at_5": {"target": 0.8}},
"fairness": {"subgroup_delta": {"target_max": 0.2}},
"safety": {"harmful_rate": {"target_max": 0.1}},
}
}
service = create_service(cfg)
request = CiCheckInput(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Missing test for partial CI check violations.

Please add tests where only a subset of CI check metrics fail, to ensure the service reports violations accurately.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request does an excellent job of refactoring the policy gateway into a hexagonal architecture. The separation of concerns is clear, and the introduction of application services, ports, and adapters greatly improves the maintainability and testability of the code. The addition of unit tests is also a fantastic step forward. My review includes a few suggestions to further enhance the new architecture, primarily focusing on improving error handling in the configuration adapter to prevent silent failures, and pointing out a potential logic change in the CI checks that could make them less strict.

Comment on lines +68 to +72
if fairness_max is not None and request.fairness.get("subgroup_delta", 0) > fairness_max:
violations.append("fairness.subgroup_delta")

safety_max = self._lookup_threshold(thresholds, "safety", "harmful_rate", "target_max")
if safety_max is not None and request.safety.get("harmful_rate", 0) > safety_max:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The default values for subgroup_delta and harmful_rate have been changed from 1 to 0 when the metric is missing from the request. In the original implementation, a missing value defaulted to 1, which would likely cause the check to fail (e.g., 1 > target_max). The new implementation with a default of 0 will likely cause the check to pass. This is a significant change in behavior that makes the checks less strict. If this was unintentional, the defaults should be changed back to 1 to preserve the original logic.

SPRIME01 and others added 2 commits November 11, 2025 18:53
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
services/policy-gateway/src/policy_gateway/application/services.py (2)

22-23: Consider checking configuration availability in health endpoint.

The health check currently returns a static response. For production readiness, consider verifying that the configuration port can successfully load data.

Example enhancement:

 def health(self) -> Dict[str, str]:
-    return {"status": "ok"}
+    try:
+        self._configuration_port.load()
+        return {"status": "ok"}
+    except Exception:
+        return {"status": "degraded"}

29-43: Consider making policy thresholds configurable.

The jailbreak score threshold (0.8) and verbatim ratio threshold (0.2 in decide_output) are hardcoded, while ci_check properly loads its thresholds from configuration. For consistency and flexibility, consider loading these thresholds from the configuration port as well.

Example refactor:

 def decide_prompt(self, request: PromptDecisionInput) -> DecisionResult:
+    config = self._configuration_port.load() or {}
+    jailbreak_threshold = config.get("prompt_thresholds", {}).get("jailbreak_score", 0.8)
     context = request.context or {}
     if context.get("contains_pii") and not context.get("lawful_basis"):
         return DecisionResult(
             allowed=False,
             action="block",
             reasons=["PII without lawful basis"],
         )
-    if (context.get("jailbreak_score") or 0) > 0.8:
+    if (context.get("jailbreak_score") or 0) > jailbreak_threshold:
         return DecisionResult(
             allowed=True,
             action="safe_mode",
             reasons=["high jailbreak score"],
         )
     return DecisionResult(allowed=True, action="allow")
services/policy-gateway/src/app.py (2)

49-68: Consider explicit response construction instead of dict unpacking.

Using **decision.__dict__ assumes the dataclass fields exactly match the Pydantic model fields. While this works currently, it's fragile and could break silently if the models diverge.

More explicit alternative:

 def filter_prompt(
     body: PromptCheckRequest,
     service: PolicyDecisionService = Depends(get_service),
 ) -> DecisionResponse:
     decision = service.decide_prompt(
         PromptDecisionInput(prompt=body.prompt, context=body.context or {})
     )
-    return DecisionResponse(**decision.__dict__)
+    return DecisionResponse(
+        allowed=decision.allowed,
+        action=decision.action,
+        reasons=decision.reasons
+    )

71-84: CI check implementation follows the same pattern.

Same suggestion as filter endpoints: consider explicit field mapping instead of dict unpacking for robustness.

Regarding the static analysis B008 warnings about Depends() in default arguments: these are false positives. Using Depends() in function parameter defaults is the official FastAPI pattern and is safe because FastAPI's dependency injection system evaluates the callable at request time, not at function definition time.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f477e2 and 18c7a25.

📒 Files selected for processing (8)
  • services/policy-gateway/src/app.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/__init__.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/application/services.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/domain/models.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/interface/http/schemas.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/ports/configuration.py (1 hunks)
  • services/policy-gateway/tests/test_policy_service.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
services/policy-gateway/src/policy_gateway/__init__.py (1)
services/policy-gateway/src/policy_gateway/application/services.py (1)
  • PolicyDecisionService (16-93)
services/policy-gateway/src/policy_gateway/ports/configuration.py (2)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (1)
  • load (18-26)
services/policy-gateway/tests/test_policy_service.py (1)
  • load (17-18)
services/policy-gateway/src/policy_gateway/application/services.py (3)
services/policy-gateway/src/policy_gateway/domain/models.py (6)
  • CiCheckInput (20-24)
  • CiCheckResult (35-37)
  • DecisionResult (28-31)
  • OutputDecisionInput (14-16)
  • PromptDecisionInput (8-10)
  • RulesSnapshot (41-45)
services/policy-gateway/src/policy_gateway/ports/configuration.py (2)
  • ConfigurationPort (6-12)
  • load (9-12)
services/policy-gateway/src/app.py (1)
  • ci_check (72-84)
services/policy-gateway/tests/test_policy_service.py (3)
services/policy-gateway/src/policy_gateway/application/services.py (4)
  • PolicyDecisionService (16-93)
  • decide_prompt (29-43)
  • decide_output (45-53)
  • ci_check (55-76)
services/policy-gateway/src/policy_gateway/domain/models.py (3)
  • CiCheckInput (20-24)
  • OutputDecisionInput (14-16)
  • PromptDecisionInput (8-10)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (2)
  • ConfigFileAdapter (12-26)
  • load (18-26)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (1)
services/policy-gateway/src/policy_gateway/ports/configuration.py (2)
  • ConfigurationPort (6-12)
  • load (9-12)
services/policy-gateway/src/app.py (4)
services/policy-gateway/src/policy_gateway/application/services.py (6)
  • PolicyDecisionService (16-93)
  • health (22-23)
  • rules (25-27)
  • decide_prompt (29-43)
  • decide_output (45-53)
  • ci_check (55-76)
services/policy-gateway/src/policy_gateway/domain/models.py (4)
  • CiCheckInput (20-24)
  • OutputDecisionInput (14-16)
  • PromptDecisionInput (8-10)
  • to_dict (44-45)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (1)
  • ConfigFileAdapter (12-26)
services/policy-gateway/src/policy_gateway/interface/http/schemas.py (5)
  • CiCheckRequest (18-22)
  • CiCheckResponse (31-33)
  • DecisionResponse (25-28)
  • OutputCheckRequest (13-15)
  • PromptCheckRequest (8-10)
🪛 Ruff (0.14.4)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py

25-25: Do not catch blind exception: Exception

(BLE001)

services/policy-gateway/src/app.py

40-40: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


45-45: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


52-52: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


63-63: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


74-74: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (8)
services/policy-gateway/src/policy_gateway/application/services.py (3)

16-20: LGTM! Clean dependency injection pattern.

The constructor properly implements the hexagonal architecture pattern by accepting the configuration port abstraction, enabling testability and flexibility.


25-27: LGTM! Proper defensive coding.

Good use of or {} to handle potential None returns from the configuration port.


78-93: LGTM! Robust threshold extraction.

The helper properly handles missing keys, None values, and type conversion errors. The defensive programming ensures the CI check won't crash on malformed configuration.

services/policy-gateway/src/policy_gateway/domain/models.py (1)

1-45: Excellent domain model design!

The dataclasses are well-structured with:

  • Immutability via frozen=True preventing accidental mutations
  • Proper use of field(default_factory=...) to avoid mutable default pitfalls
  • Clear type hints and separation of concerns
  • Simple, focused models aligned with hexagonal architecture principles
services/policy-gateway/src/app.py (4)

26-32: LGTM! Standard FastAPI service initialization.

The module-level service instance is a common pattern in FastAPI. Note that configuration is loaded once at startup, so configuration changes require a service restart.


35-36: LGTM! Clean dependency injection.

This pattern enables easy testing by allowing the service dependency to be overridden.


39-41: LGTM!

Clean delegation to the service layer.


44-46: LGTM!

Properly converts the RulesSnapshot to a dictionary for the HTTP response.

SPRIME01 and others added 8 commits November 11, 2025 18:54
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
…s.py

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
…ig_file_adapter.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…s.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…ig_file_adapter.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (1)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (1)

23-40: Fail-open: invalid config silently disables policy enforcement.

Returning {} on read/parse errors means any malformed YAML/JSON (or unreadable file) causes the gateway to run with zero policies, letting CI checks and filters fail open. We need to surface these failures so the service can abort or alert operators. Please let IO/parsing exceptions propagate (or re-raise with context) instead of swallowing them.

Suggested fix:

         if not self._path.exists():
             return {}
-        try:
-            raw = self._path.read_text(encoding="utf-8")
-        except OSError:
-            # If the file cannot be read, treat it as missing/empty for safety.
-            return {}
+        try:
+            raw = self._path.read_text(encoding="utf-8")
+        except OSError as exc:
+            raise RuntimeError(f"Unable to read configuration file {self._path}") from exc
 
         if self._path.suffix in {".yml", ".yaml"}:
             try:
-                return yaml.safe_load(raw) or {}
-            except yaml.YAMLError:
-                return {}
+                return yaml.safe_load(raw) or {}
+            except yaml.YAMLError as exc:
+                raise ValueError(f"Invalid YAML in {self._path}") from exc
 
         try:
             return json.loads(raw)
-        except json.JSONDecodeError:
-            return {}
+        except json.JSONDecodeError as exc:
+            raise ValueError(f"Invalid JSON in {self._path}") from exc
🧹 Nitpick comments (4)
README.md (2)

249-249: Capitalize "GitHub" in text references.

GitHub has a capital letter mid-word and should be capitalized when referencing the platform or its services in text. Line 249 currently shows [AI Agent Instructions](.github/copilot-instructions.md) — while the directory name .github is correct, the accompanying text should reference "GitHub" when describing it.

Update to:

-| [AI Agent Instructions](.github/copilot-instructions.md) | AI Assistants | Architecture patterns, workflows |
+| [AI Agent Instructions](.github/copilot-instructions.md) | AI Assistants | GitHub integration—architecture patterns and workflows |

358-358: Capitalize "GitHub" in development setup reference.

GitHub should be capitalized when referencing the platform in documentation text. Line 358 reads: **Development setup:** See [.github/copilot-instructions.md](... — update the accompanying text to capitalize "GitHub."

Update to:

-**Development setup:** See [`.github/copilot-instructions.md`](.github/copilot-instructions.md) for architecture patterns, workflows, and conventions.
+**Development setup:** See the [copilot-instructions.md](.github/copilot-instructions.md) file in the GitHub repository for architecture patterns, workflows, and conventions.
scripts/dev_setup.sh (2)

25-25: Add validation that PYTHONPATH directories exist.

Line 25 exports PYTHONPATH assuming services/policy-gateway/src exists at $PWD. If the script is run from an unexpected location (e.g., a subdirectory) or the directory is missing, the PYTHONPATH will be set incorrectly without notice, potentially causing silent import failures later.

Additionally, the script assumes execution from the repository root, but this is not documented.

Consider adding validation and a clear prerequisite comment:

+# Ensure the script is run from the repository root
+if [ ! -d "services/policy-gateway/src" ]; then
+  echo "Error: services/policy-gateway/src not found. Run this script from the repository root." >&2
+  exit 1
+fi
+
 export PYTHONPATH="$PWD/services/policy-gateway/src:$PYTHONPATH"

36-37: Consider using verbose pytest output for developer experience.

The -q (quiet) flag on pytest (line 37) suppresses most output, which may be surprising to developers running the setup script interactively. A more informative output would help developers understand which tests are running and which passed/failed.

Consider removing -q or making it optional:

-python3 -m pytest services/policy-gateway/tests -q
+python3 -m pytest services/policy-gateway/tests

Alternatively, use -v for verbose output if debugging is needed:

-python3 -m pytest services/policy-gateway/tests -q
+python3 -m pytest services/policy-gateway/tests -v
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18c7a25 and 4e8354f.

⛔ Files ignored due to path filters (7)
  • devbox.lock is excluded by !**/*.lock
  • services/policy-gateway/src/__pycache__/app.cpython-312.pyc is excluded by !**/*.pyc
  • services/policy-gateway/src/policy_gateway/domain/__pycache__/models.cpython-312.pyc is excluded by !**/*.pyc
  • services/policy-gateway/src/policy_gateway/infrastructure/__pycache__/config_file_adapter.cpython-312.pyc is excluded by !**/*.pyc
  • services/policy-gateway/src/policy_gateway/ports/__pycache__/configuration.cpython-312.pyc is excluded by !**/*.pyc
  • services/policy-gateway/tests/__pycache__/test_http_responses.cpython-312.pyc is excluded by !**/*.pyc
  • services/policy-gateway/tests/__pycache__/test_policy_service.cpython-312.pyc is excluded by !**/*.pyc
📒 Files selected for processing (11)
  • .github/copilot-instructions.md (2 hunks)
  • README.md (7 hunks)
  • scripts/dev_setup.sh (1 hunks)
  • services/policy-gateway/requirements-dev.txt (1 hunks)
  • services/policy-gateway/src/app.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/application/services.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/domain/models.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/ports/configuration.py (1 hunks)
  • services/policy-gateway/tests/test_http_responses.py (1 hunks)
  • services/policy-gateway/tests/test_policy_service.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
services/policy-gateway/src/policy_gateway/application/services.py (4)
services/policy-gateway/src/policy_gateway/domain/models.py (6)
  • CiCheckInput (20-24)
  • CiCheckResult (47-56)
  • DecisionResult (28-43)
  • OutputDecisionInput (14-16)
  • PromptDecisionInput (8-10)
  • RulesSnapshot (60-64)
services/policy-gateway/src/policy_gateway/ports/configuration.py (2)
  • ConfigurationPort (6-11)
  • load (9-11)
services/policy-gateway/src/app.py (3)
  • health (39-40)
  • rules (44-45)
  • ci_check (72-84)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (1)
  • load (17-40)
services/policy-gateway/src/policy_gateway/ports/configuration.py (2)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (1)
  • load (17-40)
services/policy-gateway/tests/test_policy_service.py (1)
  • load (21-22)
services/policy-gateway/src/app.py (4)
services/policy-gateway/src/policy_gateway/application/services.py (6)
  • PolicyDecisionService (16-93)
  • health (22-23)
  • rules (25-27)
  • decide_prompt (29-43)
  • decide_output (45-53)
  • ci_check (55-76)
services/policy-gateway/src/policy_gateway/domain/models.py (5)
  • CiCheckInput (20-24)
  • OutputDecisionInput (14-16)
  • PromptDecisionInput (8-10)
  • to_response (33-43)
  • to_response (51-56)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (1)
  • ConfigFileAdapter (11-40)
services/policy-gateway/src/policy_gateway/interface/http/schemas.py (5)
  • CiCheckRequest (18-22)
  • CiCheckResponse (31-33)
  • DecisionResponse (25-28)
  • OutputCheckRequest (13-15)
  • PromptCheckRequest (8-10)
services/policy-gateway/tests/test_policy_service.py (4)
services/policy-gateway/src/policy_gateway/application/services.py (4)
  • PolicyDecisionService (16-93)
  • decide_prompt (29-43)
  • decide_output (45-53)
  • ci_check (55-76)
services/policy-gateway/src/policy_gateway/domain/models.py (3)
  • CiCheckInput (20-24)
  • OutputDecisionInput (14-16)
  • PromptDecisionInput (8-10)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (2)
  • ConfigFileAdapter (11-40)
  • load (17-40)
services/policy-gateway/src/policy_gateway/ports/configuration.py (2)
  • ConfigurationPort (6-11)
  • load (9-11)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (2)
services/policy-gateway/src/policy_gateway/ports/configuration.py (2)
  • ConfigurationPort (6-11)
  • load (9-11)
services/policy-gateway/tests/test_policy_service.py (1)
  • load (21-22)
services/policy-gateway/tests/test_http_responses.py (2)
services/policy-gateway/src/policy_gateway/domain/models.py (4)
  • CiCheckResult (47-56)
  • DecisionResult (28-43)
  • to_response (33-43)
  • to_response (51-56)
services/policy-gateway/src/policy_gateway/interface/http/schemas.py (2)
  • CiCheckResponse (31-33)
  • DecisionResponse (25-28)
🪛 LanguageTool
README.md

[uncategorized] ~249-~249: The official name of this software platform is spelled with a capital “H”.
Context: ...e walkthrough | | AI Agent Instructions | AI Assistant...

(GITHUB)


[uncategorized] ~358-~358: The official name of this software platform is spelled with a capital “H”.
Context: ...RAMP, etc.) Development setup: See [.github/copilot-instructions.md](.github/copil...

(GITHUB)

🪛 markdownlint-cli2 (0.18.1)
.github/copilot-instructions.md

15-15: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.14.4)
services/policy-gateway/src/app.py

39-39: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


44-44: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


51-51: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


63-63: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


74-74: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (20)
.github/copilot-instructions.md (4)

23-25: Documentation clearly articulates hexagonal architecture principles.

The dependency flow and key principle that domain logic remains framework-agnostic align well with the PR objectives. This provides strong guidance for future contributors.


28-48: Domain-to-HTTP conversion pattern guidance is clear and actionable.

Explicit field mapping example vs. anti-pattern clearly demonstrates why decoupling domain models from HTTP schemas matters. This directly supports the refactoring goals in the PR.


177-177: Verify external reference accuracy.

Line 177 references a "Fix needed" for services/policy-gateway/src/policy_gateway/ports/configuration.py line 9-12 regarding Protocol method stubs. Please confirm this guidance is accurate and still applies after the refactoring, or clarify if this was already resolved.


181-181: Verify external reference accuracy.

Line 181 references a "Known issue" about duplicate code in ConfigFileAdapter.load(). Please confirm this duplicate code still exists in the current implementation or remove/update this guidance if it has been resolved during the refactoring.

README.md (8)

59-99: Architecture diagram and components section look good.

The restructured ASCII art diagram clearly communicates the developer workflow, and the Core Components table accurately reflects the hexagonal architecture introduced in this PR. The sidecar pattern description aligns with the Policy Gateway refactor objectives.


116-127: Clarify devbox dependency in Quick Start.

Line 116 introduces devbox shell as a prerequisite for the Local Demo workflow, but devbox is listed as "Recommended" (line 107), not "Required." This could confuse users without devbox installed.

Either:

  1. Make devbox "Required" in prerequisites and ensure installation instructions are clear, or
  2. Add an alternative path for users without devbox (e.g., "devbox shell # or ensure Python 3.11+ is in PATH")

Additionally, please verify that the artifact JSON structure (lines 122–127) matches the schema expected by the just ci-check command and pac_ci.py policy checker.


135-142: Verify curl example request/response accuracy against implemented API.

The new "Test the policy gateway" section introduces a curl example that should be validated against the actual PolicyDecisionService HTTP schemas introduced in this PR.

Please confirm that:

  1. The /filter/prompt endpoint and request schema ({"prompt":"test","context":{"jailbreak_score":0.9}}) match the Pydantic models in interface/http/schemas.py
  2. The response structure ({"allowed":true,"action":"safe_mode","reasons":[...]}) matches the actual HTTP response model
  3. Expected output threshold logic (jailbreak score 0.9) aligns with the decision logic in PolicyDecisionService

If schemas differ, update the curl example to match the actual API contract.


147-166: Verify ADR-006 ConfigMap deployment approach aligns with ConfigFileAdapter implementation.

The Production Deployment section describes creating an ADR-006 ConfigMap from policies/adr-006.embedded-governance.yaml (lines 147–150). Given the PR introduces a ConfigFileAdapter for file-based configuration, please confirm:

  1. The ConfigMap mounting strategy shown here is the intended deployment mechanism for the adapter
  2. The path policies/adr-006.embedded-governance.yaml is the expected ConfigMap source
  3. The reference to docs/PROJECT_STATE.md (line 161) for production hardening is accurate and the file exists

170-216: Governance examples are clear and compelling.

The fairness gate and runtime prompt filtering examples effectively communicate the value proposition. Ensure that the response structure shown in lines 208–212 matches the actual PolicyDecisionService response format.


244-256: Verify documentation file existence and update GitHub references.

Lines 246–256 reference multiple documentation files. Please confirm these files exist in the repository:

  • docs/IAGPM_GenAI_Handbook/Quick_Start_Guide.md
  • .github/copilot-instructions.md
  • docs/IAGPM_GenAI_Handbook/Reference.md
  • docs/IAGPM_GenAI_Handbook/Technical/llmops_reference_runbook.md
  • docs/IAGPM_GenAI_Handbook/Technical/policy_as_code_starter.md
  • docs/PROJECT_STATE.md
  • tests/TESTING_GUIDE.md

Additionally, per static analysis guidance, the capitalization of "GitHub" should be corrected in text references (currently .github — note that the directory name is correct, but references to the GitHub platform in accompanying text should use "GitHub" not ".github").


349-359: Clarify development setup documentation path.

Line 358 references .github/copilot-instructions.md for "architecture patterns, workflows, and conventions." This file should be verified to exist and contain the development guidance described. Additionally, ensure the path .github/copilot-instructions.md is correctly formatted if it's an internal reference.


403-439: Verify justfile commands and Core APIs table accuracy.

The new "Just Commands" section (lines 408–411) references four automation shortcuts. Please verify these commands exist and work as documented:

  • just ci-check
  • just deploy-config
  • just deploy-res
  • just deploy-vllm-with-gateway

Additionally, the "Core APIs" table (lines 430–439) documents six endpoints across two services. Please confirm that all endpoints are implemented with the documented purposes:

Policy Gateway:

  • POST /filter/prompt – Runtime prompt filtering
  • POST /filter/output – Runtime output filtering
  • POST /ci/check – Pre-merge threshold validation

Risk & Evidence Service (RES):

  • POST /evidence – Submit audit artifacts
  • GET /risk/snapshot – Current compliance state
  • GET /metrics – Prometheus metrics

These should align with the PolicyDecisionService implementation and HTTP schemas introduced in this PR.

services/policy-gateway/src/app.py (2)

25-35: Clean hexagonal architecture initialization.

The service builder pattern with a module-level singleton and DI accessor is well-structured for FastAPI applications.


38-84: FastAPI dependency injection is correctly implemented.

The static analysis warnings (B008) about Depends() in default arguments are false positives. FastAPI is specifically designed to use Depends() in function parameter defaults as its idiomatic dependency injection pattern, and this is the recommended approach per FastAPI documentation.

The refactored endpoints properly:

  • Delegate business logic to the service layer
  • Use typed domain models for transformation
  • Declare explicit response models for OpenAPI documentation
  • Maintain clean separation between HTTP and domain layers
services/policy-gateway/src/policy_gateway/application/services.py (4)

16-27: LGTM: Clean service initialization and configuration loading.

The constructor properly accepts the configuration port, and the rules() method defensively handles missing configuration with the or {} fallback.


29-43: Approve the prompt decision logic with existing caveats.

The decision flow is clear and correctly structured. Previous reviews have noted concerns about the truthiness check for lawful_basis and the default value for jailbreak_score, which are valid points to keep in mind.


45-53: Output decision logic is consistent and clear.

The implementation follows the same pattern as decide_prompt() with defensive defaults.


78-93: Excellent defensive threshold extraction.

The _lookup_threshold() method properly handles missing keys, invalid types, and conversion errors with appropriate None returns, enabling safe threshold evaluation in ci_check().

services/policy-gateway/requirements-dev.txt (2)

1-7: Dependencies align with PR objectives.

The included packages appropriately support the hexagonal architecture refactor: FastAPI/Pydantic/Uvicorn for the service layer, pytest/pytest-cov for testing, ruff for code quality, and HTTP clients for integration tests.


5-6: Remove unused HTTP client dependencies from requirements-dev.txt.

Neither requests nor httpx is used in the codebase. The test suite uses FastAPI's TestClient (from fastapi.testclient) for HTTP endpoint testing. Remove both lines 5-6 to eliminate unnecessary dependencies.

Comment on lines +15 to +21
```
ports/ → Interfaces (ConfigurationPort Protocol)
application/ → Service layer (PolicyDecisionService)
domain/ → Models (DecisionResult, CiCheckInput, etc.)
infrastructure/ → Adapters (ConfigFileAdapter)
interface/ → HTTP schemas (Pydantic models)
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language identifier to fenced code block.

Line 15 starts a fenced code block without specifying a language. This violates markdown lint rules (MD040) and reduces syntax highlighting in editors and documentation renderers.

-```
+```yaml
 ports/          → Interfaces (ConfigurationPort Protocol)
 application/    → Service layer (PolicyDecisionService)
 domain/         → Models (DecisionResult, CiCheckInput, etc.)
 infrastructure/ → Adapters (ConfigFileAdapter)
 interface/      → HTTP schemas (Pydantic models)

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

15-15: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

.github/copilot-instructions.md around lines 15 to 21: the fenced code block
starting at line 15 lacks a language identifier which triggers MD040 and
prevents syntax highlighting; update the opening fence to include a language
(e.g., "yaml") so it becomes ```yaml and leave the block contents unchanged.


</details>

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread scripts/dev_setup.sh Resolved
@@ -0,0 +1,10 @@
fastapi>=0.95.0
pydantic>=1.10.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify Pydantic version constraint.

The constraint pydantic>=1.10.0 allows both Pydantic v1 and v2, which have breaking API changes. Confirm whether supporting both versions is intentional or if the constraint should be tightened (e.g., pydantic>=1.10.0,<2.0.0 for v1-only, or pydantic>=2.0.0 for v2-only).


🌐 Web query:

What breaking changes were introduced in Pydantic v2 compared to v1?

💡 Result:

Short summary of the main breaking changes in Pydantic v2 vs v1:

  1. Core rewrite: validation moved into the new pydantic-core (Rust) package — semantics, performance, and some behaviors changed. [3][1]
  2. Renamed BaseModel API: many methods/attributes renamed to model_* or pydantic*_ (e.g., parse_obj → model_validate, construct → model_construct, copy → model_copy, dict → model_dump, json → model_dump_json, json_schema → model_json_schema, fields → model_fields). Old names may be deprecated/removed. [1]
  3. Equality and representation: model equality semantics changed (models are no longer equal to dicts) and other repr/eq behaviors were adjusted. [3][1]
  4. Validators and hooks API changed: validator registration, internals (e.g., validators, pydantic_validator) and lifecycle hooks differ; many v1 validator patterns were replaced. [1]
  5. Dataclass behavior: Pydantic/vanilla dataclasses changed (no tuple validation for dataclass fields, post_init runs after validation, no pydantic_model backing, extra='allow' removed for dataclasses); recommend using TypeAdapter for dataclass validation/JSON schema. [1]
  6. from_orm & parsing: from_orm removed; use model_validate with from_attributes=True. Some parse_* helpers deprecated (parse_raw/parse_file). [3][1]
  7. Error format and validation details: ValidationError structure/messages/fields changed (msg/ctx/loc), and some error fields or formats differ — code that parsed v1 error messages may break. [2][1]
  8. Typing/annotation and schema differences: how schemas are generated and how certain typing constructs are interpreted changed (TypeAdapter introduced; JSON Schema ref formatting may differ). [1][2]
  9. Migration tooling & compatibility: v1 docs and a v1 compatibility module exist (pydantic.v1), and a migration guide + codemod tool are provided because the upgrade is non-trivial. [1][2]

For details and the full migration notes (including exact renamed symbols, dataclass examples, and recommended fixes) see the official migration guide and version-compatibility notes. [1][2][3]

Sources: Pydantic migration guide and docs. [1] Migration Guide; [2] Version Compatibility / Version Policy; [3] Pydantic v2 announcement.


I need to verify how Pydantic is actually used in the codebase to assess the urgency of this constraint issue.


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for Pydantic usage in policy-gateway service
echo "=== Pydantic imports in policy-gateway ==="
find services/policy-gateway -type f -name "*.py" 2>/dev/null | head -20 | xargs grep -l "pydantic" 2>/dev/null || echo "No .py files found"

echo -e "\n=== Main requirements.txt or setup.py for policy-gateway ==="
find services/policy-gateway -type f \( -name "requirements*.txt" -o -name "setup.py" -o -name "pyproject.toml" \) 2>/dev/null | head -10

echo -e "\n=== Content of requirements-dev.txt ==="
cat services/policy-gateway/requirements-dev.txt 2>/dev/null || echo "File not found"

Length of output: 458



---

🏁 Script executed:

```shell
#!/bin/bash
# Check main requirements.txt
echo "=== Content of main requirements.txt ==="
cat services/policy-gateway/requirements.txt 2>/dev/null

echo -e "\n=== Pydantic usage in schemas.py (full content or key patterns) ==="
cat services/policy-gateway/src/policy_gateway/interface/http/schemas.py 2>/dev/null | head -100

Length of output: 839


I need to verify what Pydantic version FastAPI 0.115.0 actually requires to ensure requirements-dev.txt is compatible.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check FastAPI 0.115.0 dependencies - search for what Pydantic version it requires
# Using pip index to query package metadata
python3 -c "
import json
import urllib.request

# Fetch FastAPI 0.115.0 metadata from PyPI
url = 'https://pypi.org/pypi/fastapi/0.115.0/json'
try:
    with urllib.request.urlopen(url) as response:
        data = json.loads(response.read().decode())
        requires = data['info'].get('requires_dist', [])
        print('FastAPI 0.115.0 dependencies:')
        for req in requires:
            if 'pydantic' in req.lower():
                print(f'  {req}')
except Exception as e:
    print(f'Error: {e}')
" 2>&1

Length of output: 134



---

🌐 Web query:

```web
FastAPI 0.115.0 pydantic version requirement dependencies

💡 Result:

FastAPI 0.115.0 requires Pydantic v2 (i.e. pydantic >= 2.x). See the FastAPI 0.115.0 release notes and ecosystem examples noting Pydantic 2 as the required major version. [1][2]

Sources:

  • FastAPI release notes (0.115.0). [1]
  • Example dependency guidance showing "pydantic >= 2.x required for fastapi >= 0.113.0". [2]

Tighten Pydantic constraint to Pydantic v2 to match production requirement.

FastAPI 0.115.0 requires Pydantic v2, which is pinned in the main requirements.txt via fastapi==0.115.0. The current dev constraint pydantic>=1.10.0 is unconstrained and allows Pydantic v1, creating version skew between development and production environments.

Change to:

pydantic>=2.0.0,<3.0.0

This ensures dev dependencies align with the production constraint imposed by FastAPI.

🤖 Prompt for AI Agents
In services/policy-gateway/requirements-dev.txt around line 2, the pydantic
constraint is too loose (pydantic>=1.10.0); update that line to pin Pydantic v2
by replacing it with pydantic>=2.0.0,<3.0.0 so dev dependencies match production
FastAPI's Pydantic v2 requirement.

Comment on lines +55 to +76
def ci_check(self, request: CiCheckInput) -> CiCheckResult:
config = self._configuration_port.load() or {}
thresholds: Dict[str, Any] = config.get("thresholds", {})

violations: List[str] = []

quality_target = self._lookup_threshold(thresholds, "quality", "pass_at_5", "target")
if quality_target is not None and request.quality.get("pass_at_5", 0) < quality_target:
violations.append("quality.pass_at_5")

fairness_max = self._lookup_threshold(
thresholds, "fairness", "subgroup_delta", "target_max"
)
if fairness_max is not None and request.fairness.get("subgroup_delta", 0) > fairness_max:
violations.append("fairness.subgroup_delta")

safety_max = self._lookup_threshold(thresholds, "safety", "harmful_rate", "target_max")
if safety_max is not None and request.safety.get("harmful_rate", 0) > safety_max:
violations.append("safety.harmful_rate")

status = "fail" if violations else "pass"
return CiCheckResult(status=status, violations=violations)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that drift evaluation is implemented.

The ci_check() method evaluates quality, fairness, and safety thresholds but does not evaluate the drift field from CiCheckInput. A previous review comment indicated this was addressed in commits 75e93ff-e6a1d44, but the evaluation is not visible in the current code.

Run this script to confirm whether drift evaluation logic is present:


🏁 Script executed:

#!/bin/bash
# Description: Search for drift evaluation in the ci_check method

# Look for drift threshold lookups or drift violation checks
rg -n -B2 -A5 'drift' services/policy-gateway/src/policy_gateway/application/services.py

# Also check if there are any drift-related threshold lookups
ast-grep --pattern 'self._lookup_threshold($$$, "drift", $$$)'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Description: Comprehensive search for drift in services.py and related context

# Check file exists and search more broadly
echo "=== Checking file exists ==="
ls -la services/policy-gateway/src/policy_gateway/application/services.py

echo -e "\n=== Search entire services.py for 'drift' (case-insensitive) ==="
rg -in 'drift' services/policy-gateway/src/policy_gateway/application/services.py

echo -e "\n=== Find CiCheckInput class definition ==="
ast-grep --pattern 'class CiCheckInput { $$$ }'

echo -e "\n=== Check for DataClass with CiCheckInput ==="
rg -n 'CiCheckInput' services/policy-gateway/src/policy_gateway/application/services.py -B5 -A15

echo -e "\n=== Search for configuration/config files with drift thresholds ==="
find services/policy-gateway -name '*.py' -o -name '*.json' -o -name '*.yaml' -o -name '*.yml' | head -20

Length of output: 2476


🏁 Script executed:

#!/bin/bash
# Description: Examine CiCheckInput definition and drift configuration

echo "=== CiCheckInput definition in models.py ==="
rg -n 'class CiCheckInput' services/policy-gateway/src/policy_gateway/domain/models.py -A20

echo -e "\n=== Search for 'drift' in models.py ==="
rg -in 'drift' services/policy-gateway/src/policy_gateway/domain/models.py

echo -e "\n=== Search for 'drift' in all Python files under services/policy-gateway ==="
rg -in 'drift' services/policy-gateway --type=py

echo -e "\n=== Check test files for drift expectations ==="
rg -in 'drift' services/policy-gateway/tests/ --type=py -B2 -A2

Length of output: 2830


🏁 Script executed:

#!/bin/bash
# Description: Check for drift thresholds in configuration

echo "=== Check test_policy_service for drift threshold expectations ==="
rg -in 'drift' services/policy-gateway/tests/test_policy_service.py -B5 -A5

echo -e "\n=== Check config_file_adapter for drift handling ==="
cat services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py

echo -e "\n=== Check for any configuration files (json/yaml) ==="
find services/policy-gateway -type f \( -name '*.json' -o -name '*.yaml' -o -name '*.yml' \)

echo -e "\n=== Check if there are drift thresholds in test configuration ==="
rg -n 'thresholds' services/policy-gateway/tests/test_policy_service.py -B2 -A10

Length of output: 3135


Add drift evaluation to the ci_check() method.

The ci_check() method currently evaluates quality, fairness, and safety thresholds but ignores the drift field from CiCheckInput. The drift field is defined in the data model and accepted by the HTTP endpoint, but it is never validated. Add drift threshold checks following the same pattern as the other metrics (lines 61-69).

🤖 Prompt for AI Agents
In services/policy-gateway/src/policy_gateway/application/services.py around
lines 55 to 76, add a drift threshold check analogous to the existing fairness
and safety checks: call self._lookup_threshold(thresholds, "drift", "psi",
"target_max") (or the correct drift metric name if different), store it to
drift_max, then if drift_max is not None and request.drift.get("psi", 0) >
drift_max append "drift.psi" to violations; place this check before computing
status so drift violations are included in the CiCheckResult.

Comment on lines +134 to +149
def test_config_file_adapter_invalid_yaml(tmp_path: Path):
# Write invalid YAML content
cfg_path = tmp_path / "invalid.yaml"
cfg_path.write_text("foo: [unclosed_list\nbar: baz")

adapter = ConfigFileAdapter(cfg_path)
assert adapter.load() == {}


def test_config_file_adapter_invalid_json(tmp_path: Path):
# Write invalid JSON content
cfg_path = tmp_path / "invalid.json"
cfg_path.write_text('{"foo": "bar", "baz": [1, 2,}') # Invalid JSON

adapter = ConfigFileAdapter(cfg_path)
assert adapter.load() == {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Surface configuration parse failures instead of asserting fail-open behavior.

These tests require ConfigFileAdapter.load() to swallow invalid YAML/JSON and return {}. That expectation pushes the adapter toward a fail-open posture: if a policy file is malformed, the service proceeds with an empty config and skips all thresholds. Please change the tests to expect an exception (e.g., pytest.raises(ValueError)) so we fail fast and make configuration errors visible. Update both the YAML and JSON cases accordingly.

🤖 Prompt for AI Agents
In services/policy-gateway/tests/test_policy_service.py around lines 134 to 149,
the tests currently assert that ConfigFileAdapter.load() returns {} on invalid
YAML/JSON which enforces a fail-open behavior; update both tests to expect a
parsing exception instead (use pytest.raises(ValueError) or the specific
exception your loader raises) so malformed configs fail fast; ensure pytest is
imported if not already, wrap the adapter.load() call with pytest.raises(...) in
both the YAML and JSON test cases, and adjust any test names/docstrings to
reflect the new expectation.

@SPRIME01

Copy link
Copy Markdown
Collaborator Author

@coderabbitai configuration

@coderabbitai

coderabbitai Bot commented Nov 12, 2025

Copy link
Copy Markdown
Contributor
CodeRabbit configuration
language: en-US
tone_instructions: ''
early_access: false
enable_free_tier: true
reviews:
  profile: chill
  request_changes_workflow: false
  high_level_summary: true
  high_level_summary_placeholder: '@coderabbitai summary'
  high_level_summary_in_walkthrough: false
  auto_title_placeholder: '@coderabbitai'
  auto_title_instructions: ''
  review_status: true
  commit_status: true
  fail_commit_status: false
  collapse_walkthrough: false
  changed_files_summary: true
  sequence_diagrams: true
  estimate_code_review_effort: true
  assess_linked_issues: true
  related_issues: true
  related_prs: true
  suggested_labels: true
  labeling_instructions: []
  auto_apply_labels: false
  suggested_reviewers: true
  auto_assign_reviewers: false
  in_progress_fortune: true
  poem: false
  path_filters: []
  path_instructions: []
  abort_on_close: true
  disable_cache: false
  auto_review:
    enabled: true
    auto_incremental_review: true
    ignore_title_keywords: []
    labels: []
    drafts: false
    base_branches: []
    ignore_usernames: []
  finishing_touches:
    docstrings:
      enabled: true
    unit_tests:
      enabled: true
  pre_merge_checks:
    docstrings:
      mode: warning
      threshold: 80
    title:
      mode: warning
      requirements: ''
    description:
      mode: warning
    issue_assessment:
      mode: warning
    custom_checks: []
  tools:
    ast-grep:
      rule_dirs: []
      util_dirs: []
      essential_rules: true
      packages: []
    shellcheck:
      enabled: true
    ruff:
      enabled: true
    markdownlint:
      enabled: true
    github-checks:
      enabled: true
      timeout_ms: 90000
    languagetool:
      enabled: true
      enabled_rules: []
      disabled_rules: []
      enabled_categories: []
      disabled_categories: []
      enabled_only: false
      level: default
    biome:
      enabled: true
    hadolint:
      enabled: true
    swiftlint:
      enabled: true
    phpstan:
      enabled: true
      level: default
    phpmd:
      enabled: true
    phpcs:
      enabled: true
    golangci-lint:
      enabled: true
    yamllint:
      enabled: true
    gitleaks:
      enabled: true
    checkov:
      enabled: true
    detekt:
      enabled: true
    eslint:
      enabled: true
    flake8:
      enabled: true
    rubocop:
      enabled: true
    buf:
      enabled: true
    regal:
      enabled: true
    actionlint:
      enabled: true
    pmd:
      enabled: true
    clang:
      enabled: true
    cppcheck:
      enabled: true
    semgrep:
      enabled: true
    circleci:
      enabled: true
    clippy:
      enabled: true
    sqlfluff:
      enabled: true
    prismaLint:
      enabled: true
    pylint:
      enabled: true
    oxc:
      enabled: true
    shopifyThemeCheck:
      enabled: true
    luacheck:
      enabled: true
    brakeman:
      enabled: true
    dotenvLint:
      enabled: true
    htmlhint:
      enabled: true
    checkmake:
      enabled: true
    osvScanner:
      enabled: true
chat:
  art: true
  auto_reply: true
  integrations:
    jira:
      usage: auto
    linear:
      usage: auto
knowledge_base:
  opt_out: false
  web_search:
    enabled: true
  code_guidelines:
    enabled: true
    filePatterns: []
  learnings:
    scope: auto
  issues:
    scope: auto
  jira:
    usage: auto
    project_keys: []
  linear:
    usage: auto
    team_keys: []
  pull_requests:
    scope: auto
  mcp:
    usage: auto
    disabled_servers: []
code_generation:
  docstrings:
    language: en-US
    path_instructions: []
  unit_tests:
    path_instructions: []

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (9)
.github/workflows/ci.yml (1)

31-46: Consider using $GITHUB_PATH for cleaner virtual environment persistence.

The review comment correctly identifies that venv activation doesn't persist across steps. However, the proposed solutions (re-activation or absolute paths) are not the most idiomatic GitHub Actions pattern.

Each step runs in its own shell/process, so activation modifications don't carry forward. The recommended approach is to add the venv's bin directory to $GITHUB_PATH in the initial setup step, which automatically makes the venv Python available to all subsequent steps:

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          curl -LsSf https://astral.sh/uv/install.sh | sh || true
+         # Create venv and add to PATH for all subsequent steps
          if command -v uv >/dev/null 2>&1; then
            uv venv .venv
-           source .venv/bin/activate
            pip install -r services/policy-gateway/requirements-dev.txt
          else
            python -m venv .venv
-           source .venv/bin/activate
            pip install -r services/policy-gateway/requirements-dev.txt
          fi
+         echo "${{ github.workspace }}/.venv/bin" >> $GITHUB_PATH

This eliminates the need to modify linter and test steps. Alternatively, the proposed re-activation or absolute-path approaches will also work.

tmp_cfg2.yaml (1)

1-4: Consider renaming or relocating this configuration file.

The tmp_cfg2.yaml filename suggests this is a temporary test fixture. If this file is used for testing purposes, it should be moved to the tests/ directory or renamed to clearly indicate its purpose (e.g., test_threshold_config.yaml). If it's intended for production use, remove the "tmp" prefix and document its purpose.

README.md (1)

59-519: Excellent documentation improvements with comprehensive coverage.

The README updates significantly improve the project's documentation:

  • Clear problem statement and business value proposition
  • Well-structured quick start with concrete examples
  • Detailed streaming endpoint usage with curl and JavaScript examples
  • Production deployment guidance with checklists
  • Comprehensive core APIs table

The streaming documentation (lines 186-214) is particularly helpful with multiple client examples.

Optional: Address minor formatting issues flagged by linters:

  1. Lines 164, 186: Convert setext headings to ATX style:
-Provider selection & streaming
-------------------------------
+### Provider selection & streaming

-Streaming endpoint (SSE)
-------------------------
+### Streaming endpoint (SSE)
  1. Line 192: Remove spaces inside code span:
- - POST /proxy/completion/stream — returns `text/event-stream` chunks where each
+ - POST /proxy/completion/stream — returns `text/event-stream` chunks where each
  1. Lines 325, 434: Use capital 'H' in GitHub references per style guide (.github → .github is correct for paths, but the linter may be flagging text references).
docs/Reference/LLMOps_Specs/ADR.md (1)

159-173: LGTM! Clear documentation of runtime adapter and streaming additions.

The ADR update effectively documents the new architectural capabilities:

  • Runtime provider selection via PAC_LLM_PROVIDER
  • SSE streaming endpoint for low-latency responses
  • Rationale for the changes (provider flexibility without code changes)

Fix the list indentation flagged by markdownlint:

 - New environment variable: `PAC_LLM_PROVIDER` — controls which adapter the
    Policy Gateway uses at runtime. Supported values:
-   - `http` (default) — forward to an upstream vLLM-like HTTP endpoint
-   - `litellm` — use an in-process LiteLLM client adapter when available
+  - `http` (default) — forward to an upstream vLLM-like HTTP endpoint
+  - `litellm` — use an in-process LiteLLM client adapter when available

This ensures consistent 2-space indentation for nested list items.

services/policy-gateway/tests/test_llm_proxy.py (2)

13-16: Narrow the exception type for JSON parsing.

The bare Exception catch is overly broad. Use json.JSONDecodeError to handle only JSON parsing failures.

Apply this diff:

         try:
             j = json.loads(body or "{}")
-        except Exception:
+        except json.JSONDecodeError:
             j = {}

47-53: Avoid runtime sys.path manipulation.

Dynamically manipulating sys.path at runtime is fragile and depends on the test file's location in the directory structure. Consider using pytest's pythonpath configuration or ensuring the src directory is already on the path via PYTHONPATH environment variable (as mentioned in the PR testing instructions).

If the testing instructions already specify PYTHONPATH=services/policy-gateway/src, these lines can be removed entirely:

-        # Ensure the app imports from the correct src path
-        repo_root = Path(__file__).resolve().parents[3]
-        src_dir = str(repo_root / "services" / "policy-gateway" / "src")
-        import sys
-
-        sys.path.insert(0, src_dir)
         import app as gateway_app  # type: ignore
services/policy-gateway/src/policy_gateway/infrastructure/litellm_adapter.py (1)

5-8: Narrow the exception type for import handling.

The bare except Exception is too broad for handling import failures. Use except ImportError to catch only module import errors.

Apply this diff:

 try:
     import litellm
-except Exception:  # pragma: no cover - litellm may not be installed in analysis env
+except ImportError:  # pragma: no cover - litellm may not be installed in analysis env
     litellm = None
services/policy-gateway/src/policy_gateway/infrastructure/llm_http_adapter.py (1)

26-44: Consider adding retry logic for transient failures.

The adapter currently fails immediately on any exception. For production robustness, consider adding retry logic with exponential backoff for transient network errors or 5xx responses.

Example using httpx with retry:

from httpx import Client, HTTPStatusError

def complete(self, request: CompletionRequest) -> CompletionResponse:
    url = self.endpoint.rstrip("/") + "/completion"
    payload: Dict[str, object] = {"prompt": request.prompt}
    if request.model:
        payload["model"] = request.model
    if request.max_tokens is not None:
        payload["max_tokens"] = request.max_tokens

    # Simple retry logic
    for attempt in range(3):
        try:
            resp = httpx.post(url, json=payload, timeout=self.timeout)
            resp.raise_for_status()
            j = resp.json()
            break
        except (httpx.NetworkError, HTTPStatusError) as exc:
            if attempt == 2:  # last attempt
                raise RuntimeError(f"LLM request failed: {exc}") from exc
            # exponential backoff: 0.1s, 0.2s
            time.sleep(0.1 * (2 ** attempt))
    else:
        raise RuntimeError("LLM request failed after retries")

    content = j.get("content") or j.get("text") or json.dumps(j)
    return CompletionResponse(
        content=str(content), model=j.get("model"), usage=j.get("usage", {})
    )
services/policy-gateway/src/app.py (1)

43-55: Cache the LLM adapter to avoid repeated instantiation.

The _build_llm_adapter() function is called for every proxy request (lines 62, 84), creating a new adapter instance each time. For the HTTP adapter, this means recreating the httpx client for each request. Consider caching the adapter or using FastAPI's dependency injection with a singleton scope.

Option 1: Use functools.lru_cache for simple caching:

+from functools import lru_cache
+
+@lru_cache(maxsize=1)
 def _build_llm_adapter():
     """Factory for LLM adapters. Selects implementation using PAC_LLM_PROVIDER.

Option 2: Use FastAPI dependency with singleton pattern:

_llm_adapter_singleton = None

def get_llm_adapter():
    global _llm_adapter_singleton
    if _llm_adapter_singleton is None:
        _llm_adapter_singleton = _build_llm_adapter()
    return _llm_adapter_singleton

@app.post("/proxy/completion", response_model=CompletionResponse)
def proxy_completion(
    body: CompletionRequest,
    adapter = Depends(get_llm_adapter),
):
    # use adapter...
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e8354f and 87b34ff.

📒 Files selected for processing (22)
  • .coderabbit.yml (1 hunks)
  • .github/workflows/ci.yml (1 hunks)
  • .gitignore (1 hunks)
  • README.md (7 hunks)
  • docs/Reference/ENVIRONMENT.md (1 hunks)
  • docs/Reference/LLMOps_Specs/ADR.md (1 hunks)
  • docs/Reference/LLMOps_Specs/UPDATES.md (1 hunks)
  • justfile (1 hunks)
  • scripts/dev_setup.sh (1 hunks)
  • services/policy-gateway/requirements-dev.txt (1 hunks)
  • services/policy-gateway/src/app.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/domain/models.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/infrastructure/litellm_adapter.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/infrastructure/llm_http_adapter.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/interface/http/schemas.py (1 hunks)
  • services/policy-gateway/src/policy_gateway/ports/llm_adapter.py (1 hunks)
  • services/policy-gateway/tests/test_integration.py (1 hunks)
  • services/policy-gateway/tests/test_llm_proxy.py (1 hunks)
  • services/policy-gateway/tests/test_streaming.py (1 hunks)
  • specs/openapi-policy-gateway.yaml (7 hunks)
  • tmp_cfg.yaml (1 hunks)
  • tmp_cfg2.yaml (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • tmp_cfg.yaml
  • docs/Reference/LLMOps_Specs/UPDATES.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • scripts/dev_setup.sh
  • services/policy-gateway/requirements-dev.txt
  • services/policy-gateway/src/policy_gateway/interface/http/schemas.py
  • services/policy-gateway/src/policy_gateway/domain/models.py
🧰 Additional context used
🧬 Code graph analysis (6)
services/policy-gateway/src/policy_gateway/ports/llm_adapter.py (5)
services/policy-gateway/src/policy_gateway/domain/models.py (2)
  • CompletionRequest (69-73)
  • CompletionResponse (77-80)
services/policy-gateway/src/policy_gateway/interface/http/schemas.py (2)
  • CompletionRequest (36-39)
  • CompletionResponse (42-45)
services/policy-gateway/src/policy_gateway/infrastructure/litellm_adapter.py (2)
  • complete (29-74)
  • stream (76-98)
services/policy-gateway/src/policy_gateway/infrastructure/llm_http_adapter.py (2)
  • complete (26-44)
  • stream (46-68)
services/policy-gateway/tests/test_streaming.py (1)
  • stream (20-22)
services/policy-gateway/src/policy_gateway/infrastructure/litellm_adapter.py (3)
services/policy-gateway/src/policy_gateway/domain/models.py (2)
  • CompletionRequest (69-73)
  • CompletionResponse (77-80)
services/policy-gateway/src/policy_gateway/ports/llm_adapter.py (3)
  • LLMAdapterPort (8-21)
  • complete (19-19)
  • stream (21-21)
services/policy-gateway/src/policy_gateway/infrastructure/llm_http_adapter.py (2)
  • complete (26-44)
  • stream (46-68)
services/policy-gateway/src/policy_gateway/infrastructure/llm_http_adapter.py (3)
services/policy-gateway/src/policy_gateway/domain/models.py (2)
  • CompletionRequest (69-73)
  • CompletionResponse (77-80)
services/policy-gateway/src/policy_gateway/ports/llm_adapter.py (3)
  • LLMAdapterPort (8-21)
  • complete (19-19)
  • stream (21-21)
services/policy-gateway/src/policy_gateway/infrastructure/litellm_adapter.py (2)
  • complete (29-74)
  • stream (76-98)
services/policy-gateway/src/app.py (6)
services/policy-gateway/src/policy_gateway/application/services.py (6)
  • PolicyDecisionService (16-93)
  • health (22-23)
  • rules (25-27)
  • decide_prompt (29-43)
  • decide_output (45-53)
  • ci_check (55-76)
services/policy-gateway/src/policy_gateway/domain/models.py (8)
  • CiCheckInput (20-24)
  • OutputDecisionInput (14-16)
  • PromptDecisionInput (8-10)
  • CompletionRequest (69-73)
  • CompletionResponse (77-80)
  • to_dict (63-64)
  • to_response (33-43)
  • to_response (51-56)
services/policy-gateway/src/policy_gateway/infrastructure/config_file_adapter.py (1)
  • ConfigFileAdapter (11-40)
services/policy-gateway/src/policy_gateway/infrastructure/litellm_adapter.py (3)
  • LiteLLMAdapter (14-98)
  • complete (29-74)
  • stream (76-98)
services/policy-gateway/src/policy_gateway/infrastructure/llm_http_adapter.py (3)
  • HTTPLLMAdapter (12-68)
  • complete (26-44)
  • stream (46-68)
services/policy-gateway/src/policy_gateway/interface/http/schemas.py (7)
  • CiCheckRequest (18-22)
  • CiCheckResponse (31-33)
  • CompletionRequest (36-39)
  • CompletionResponse (42-45)
  • DecisionResponse (25-28)
  • OutputCheckRequest (13-15)
  • PromptCheckRequest (8-10)
services/policy-gateway/tests/test_integration.py (1)
services/policy-gateway/tests/test_streaming.py (1)
  • _import_app (7-13)
services/policy-gateway/tests/test_streaming.py (4)
services/policy-gateway/tests/test_integration.py (1)
  • _import_app (7-14)
services/policy-gateway/src/policy_gateway/infrastructure/litellm_adapter.py (1)
  • stream (76-98)
services/policy-gateway/src/policy_gateway/infrastructure/llm_http_adapter.py (1)
  • stream (46-68)
services/policy-gateway/src/policy_gateway/ports/llm_adapter.py (1)
  • stream (21-21)
🪛 actionlint (1.7.8)
.github/workflows/ci.yml

27-27: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 LanguageTool
README.md

[uncategorized] ~325-~325: The official name of this software platform is spelled with a capital “H”.
Context: ...e walkthrough | | AI Agent Instructions | AI Assistant...

(GITHUB)


[uncategorized] ~434-~434: The official name of this software platform is spelled with a capital “H”.
Context: ...RAMP, etc.) Development setup: See [.github/copilot-instructions.md](.github/copil...

(GITHUB)

🪛 markdownlint-cli2 (0.18.1)
README.md

164-164: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)


186-186: Heading style
Expected: atx; Actual: setext

(MD003, heading-style)


192-192: Spaces inside code span elements

(MD038, no-space-in-code)

docs/Reference/LLMOps_Specs/ADR.md

165-165: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


166-166: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (14)
.gitignore (1)

3-3: Correct and consistent with project conventions.

Adding .venv/ aligns with the new development setup introduced in this PR and follows the existing pattern of trailing slashes for directory entries.

services/policy-gateway/src/policy_gateway/ports/llm_adapter.py (1)

8-21: LGTM! Clean port abstraction following hexagonal architecture.

This Protocol definition correctly establishes the contract for LLM adapters:

  • Clear separation between synchronous (complete) and streaming (stream) operations
  • Well-documented, especially the note about stream() potentially yielding a single element for non-streaming providers
  • Proper use of domain models (CompletionRequest, CompletionResponse)
  • No implementation leakage

This enables runtime provider selection (HTTP vs. LiteLLM) without changing application code.

services/policy-gateway/tests/test_streaming.py (1)

16-35: LGTM! Well-structured streaming endpoint test.

This test effectively validates the SSE streaming behavior:

  • Clean mock adapter implementation that yields multiple chunks
  • Proper use of monkeypatch to inject the test adapter
  • Validates the key SSE contract: text/event-stream content type and data: prefixed chunks
  • Follows the same path-manipulation pattern as other tests for consistent app imports

The test coverage is appropriate for verifying that the streaming endpoint correctly wraps the adapter's stream() method and formats chunks as SSE events.

services/policy-gateway/tests/test_integration.py (1)

17-62: LGTM! Excellent integration test coverage.

This test provides comprehensive end-to-end validation:

Strengths:

  1. Proper isolation: Uses tmp_path fixture and monkeypatch for clean test environment
  2. Multiple scenarios: Tests both success and failure paths for CI checks
  3. Context-aware filtering: Validates prompt filtering with jailbreak detection
  4. Threshold validation: Confirms the threshold-based decision logic works correctly

The test effectively validates the hexagonal architecture refactor by exercising:

  • Configuration loading via PAC_CONFIG
  • Domain logic in PolicyDecisionService
  • HTTP interface and schemas
  • Decision rules for both runtime (/filter/prompt) and pre-merge (/ci/check) scenarios
justfile (1)

26-28: LGTM! Clean developer convenience addition.

The dev-setup task provides a straightforward entry point for environment setup. The comment clearly documents what the script does. Verification confirms that scripts/dev_setup.sh exists and is executable.

services/policy-gateway/src/policy_gateway/infrastructure/litellm_adapter.py (2)

76-95: Verify the litellm streaming API signature.

Similar to the complete() method, the stream() method uses a prompt parameter which may not match the standard litellm API. Please verify the streaming API signature as well.


23-74: Defensive response normalization is appropriate here.

The extensive fallback logic for extracting content, model, and usage from various response shapes is good defensive programming given that litellm may wrap different provider APIs with varying response structures. This approach maintains adapter robustness across potential litellm versions and provider backends.

services/policy-gateway/src/policy_gateway/infrastructure/llm_http_adapter.py (2)

12-44: Well-structured HTTP adapter with appropriate error handling.

The implementation correctly:

  • Configures endpoint from environment or parameter
  • Builds payload with optional fields
  • Uses broad exception handling at the adapter boundary (documented as intentional)
  • Provides sensible fallback for content extraction

46-68: Streaming fallback is well-designed.

The fallback from streaming to a single completed chunk on error is a sensible degradation strategy that maintains the streaming interface while handling upstream endpoints that don't support streaming.

specs/openapi-policy-gateway.yaml (2)

84-99: New proxy endpoint and schemas are well-defined.

The new /proxy/completion endpoint and associated CompletionRequest/CompletionResponse schemas correctly:

  • Define the required prompt field
  • Include optional model and max_tokens fields
  • Match the domain models from policy_gateway/domain/models.py
  • Follow the existing API patterns

Also applies to: 149-163


15-15: Consistent quote styling improves spec quality.

The change from single quotes to double quotes for response status codes ("200") is a good stylistic improvement that aligns with common OpenAPI conventions.

Also applies to: 25-25, 43-43, 60-60, 77-77, 94-94

services/policy-gateway/src/app.py (3)

28-31: Dependency injection pattern is well-implemented.

The use of factory functions with FastAPI's Depends correctly enables:

  • Per-request service instantiation (allowing tests to monkeypatch environment variables)
  • Clean separation between construction and usage
  • Testability via dependency injection

Also applies to: 37-40


77-98: SSE streaming implementation is correct.

The streaming endpoint correctly:

  • Builds a domain request from the HTTP body
  • Yields SSE-formatted chunks with data: prefix and \n\n terminator
  • Returns a StreamingResponse with appropriate text/event-stream media type

111-147: Endpoint refactoring follows hexagonal architecture principles.

The refactored endpoints correctly:

  • Use dependency injection for the service layer
  • Map HTTP schemas to domain models
  • Delegate business logic to the service
  • Map domain results back to HTTP responses via to_response() methods

This is a clean implementation of the hexagonal architecture pattern.

Comment thread .coderabbit.yml
@@ -0,0 +1,186 @@
language: en-US
tone_instructions: 'Generate concise, actionable, and unambiguous fix instructions. Specify exact file paths and edits; focus on mechanical details required for automated or manual application. Focus on critical functional issues only. Avoid unnecessary explanations or context.'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix validation error: tone_instructions exceeds 250 character limit.

The tone_instructions field exceeds the schema-defined maximum of 250 characters, causing a validation error. This will prevent the configuration from being accepted.

Shorten the tone_instructions to stay within 250 characters. For example:

- tone_instructions: 'Generate concise, actionable, and unambiguous fix instructions. Specify exact file paths and edits; focus on mechanical details required for automated or manual application. Focus on critical functional issues only. Avoid unnecessary explanations or context.'
+ tone_instructions: 'Provide concise, actionable fix instructions with exact file paths and edits. Focus on critical functional issues only.'

This condensed version conveys the key guidance while complying with the character limit.

📝 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
tone_instructions: 'Generate concise, actionable, and unambiguous fix instructions. Specify exact file paths and edits; focus on mechanical details required for automated or manual application. Focus on critical functional issues only. Avoid unnecessary explanations or context.'
tone_instructions: 'Provide concise, actionable fix instructions with exact file paths and edits. Focus on critical functional issues only.'
🤖 Prompt for AI Agents
.coderabbit.yml around line 2: the value of tone_instructions exceeds the
250-character schema limit and must be shortened; replace the current long
string with a concise instruction under 250 characters (keep it quoted), for
example a single-sentence guidance that preserves intent but is <250 chars, then
validate YAML to ensure no trailing commas or invalid quoting.

Comment thread .github/workflows/ci.yml
Comment on lines +26 to +29
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Update actions/setup-python to a newer version.

The runner of "actions/setup-python@v4" is too old to run on GitHub Actions. Consider upgrading to @v5 or later to ensure compatibility with current GitHub Actions infrastructure.

      - name: Setup Python
-       uses: actions/setup-python@v4
+       uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
🧰 Tools
🪛 actionlint (1.7.8)

27-27: the runner of "actions/setup-python@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
.github/workflows/ci.yml around lines 26 to 29: the workflow uses
actions/setup-python@v4 which is outdated for current GitHub Actions runners;
update the action reference to a supported newer major version (for example
actions/setup-python@v5 or the latest stable release) by changing the uses line
accordingly and verify the existing with: python-version input remains valid;
commit the updated workflow file and run CI to ensure compatibility.

Comment thread .github/workflows/ci.yml
Comment on lines +48 to +52
- name: Run linter (ruff)
env:
PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
run: |
ruff services/policy-gateway || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reconsider suppressing linter errors in CI.

Ruff errors are suppressed with || true (line 52), which means linting failures won't cause the CI workflow to fail. This defeats the purpose of running linting in CI. Consider removing the || true to ensure code quality standards are enforced:

      - name: Run linter (ruff)
        env:
          PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
        run: |
-         ruff services/policy-gateway || true
+         ruff services/policy-gateway
📝 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
- name: Run linter (ruff)
env:
PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
run: |
ruff services/policy-gateway || true
- name: Run linter (ruff)
env:
PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
run: |
ruff services/policy-gateway
🤖 Prompt for AI Agents
.github/workflows/ci.yml around lines 48 to 52: the ruff invocation is suffixed
with `|| true`, which masks lint failures and prevents CI from failing on lint
errors; remove the `|| true` so that ruff's non‑zero exit code will fail the
job, and if desired configure ruff via a config file or adjust the job to run
only on changed files rather than suppressing errors.

Comment thread .github/workflows/ci.yml
Comment on lines +54 to +58
- name: Run tests
env:
PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
run: |
pytest -q

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Ensure test runner uses the configured virtual environment.

The test step does not explicitly activate the venv (related to the issue flagged in lines 31–46). Ensure pytest is invoked from the correct environment by re-activating the venv or using the absolute path:

      - name: Run tests
        env:
          PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
        run: |
+         source .venv/bin/activate
          pytest -q

Or use the absolute path:

-         pytest -q
+         .venv/bin/pytest -q

I'll gather the necessary information to verify this review comment by examining the workflow file and venv setup context.


🏁 Script executed:

#!/bin/bash
# Read the GitHub Actions workflow file to inspect venv setup and test invocation
if [ -f .github/workflows/ci.yml ]; then
  echo "=== Full workflow context (lines 30-60) ==="
  sed -n '30,60p' .github/workflows/ci.yml
else
  echo "Workflow file not found"
fi

Length of output: 1085


🏁 Script executed:

#!/bin/bash
# Check if there are other workflow files or CI configurations
fd -t f "(ci|test)" --path ".github" 

Length of output: 233


Activate the venv or use absolute path to pytest in the test step.

Each GitHub Actions step runs in a new shell session, so the venv activation from the "Install dependencies" step (line 40) does not persist to the "Run tests" step. This means pytest -q currently runs outside the configured virtual environment. Use either approach:

Option 1 (re-activate):

      - name: Run tests
        env:
          PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
        run: |
+         source .venv/bin/activate
          pytest -q

Option 2 (absolute path):

      - name: Run tests
        env:
          PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
        run: |
-         pytest -q
+         .venv/bin/pytest -q

The same issue affects the "Run linter (ruff)" step on line 51.

📝 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
- name: Run tests
env:
PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
run: |
pytest -q
- name: Run tests
env:
PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
run: |
source .venv/bin/activate
pytest -q
Suggested change
- name: Run tests
env:
PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
run: |
pytest -q
- name: Run tests
env:
PYTHONPATH: ${{ github.workspace }}/services/policy-gateway/src
run: |
.venv/bin/pytest -q
🤖 Prompt for AI Agents
.github/workflows/ci.yml around lines 51 to 58: the "Run linter (ruff)" and "Run
tests" steps are invoking tools (ruff, pytest) without ensuring the virtualenv
from the "Install dependencies" step is active in each new shell; either
re-activate the venv at the start of each step (e.g., source .venv/bin/activate
or . .venv/Scripts/activate on Windows) or call the tools via absolute paths
inside the venv (e.g., .venv/bin/ruff or .venv/bin/pytest or python -m pytest
using the venv's python) so the commands run inside the installed environment.

Comment on lines +1 to +377
# Project development environment

Minimal environment document. See README.md for full developer setup instructions.

## Goals / contract

- Inputs: source files in the repository, a working devbox environment (or a local machine with Python 3.11+).
- Outputs: reproducible dev environment with isolated `.venv`, installed dev dependencies, runnable FastAPI policy gateway, and passing unit tests.
- Error modes: missing `uv` (falls back to `pip`), PEP 668-managed Python (create venv first), missing OS packages for binary wheels.

### Key files & locations

- `services/policy-gateway/src/app.py` — policy gateway FastAPI app.
- `services/policy-gateway/src/policy_gateway/*` — hexagonal architecture code (ports, application, domain, infrastructure, interface).
- `services/policy-gateway/requirements-dev.txt` — development dependencies used by the convenience script.
- `scripts/dev_setup.sh` — convenience script that creates/activates a virtualenv, installs deps (prefers `uv`), runs `ruff` and `pytest`.
- `policies/adr-006.embedded-governance.yaml` — single source of truth for embedded governance thresholds and rules.

### Architecture (high level)

```mermaid
flowchart LR
subgraph App
PG[Policy Gateway (FastAPI)]
PG -->|proxies| vLLM[vLLM / LLM service]
end

subgraph K8s[Kubernetes]
PG_DEPLOY[Deployment: policy-gateway]
VLLM_DEPLOY[Deployment: vLLM]
INGRESS[Ingress]
CONFIGMAP[ConfigMap: adr-006.embedded-governance.yaml]
PG_DEPLOY -->|mounts| CONFIGMAP
# Project development environment

This document describes how to set up and work with the Governed Speed project development environment. It covers the recommended local developer workflow (devbox + virtualenv), the `uv` package manager preference, key environment variables, and quick run/test/lint commands. It also includes mermaid diagrams of the high-level architecture and the developer workflow.

## Goals / contract

- Inputs: source files in the repository, a working devbox environment (or a local machine with Python 3.11+).
- Outputs: reproducible dev environment with isolated `.venv`, installed dev dependencies, runnable FastAPI policy gateway, and passing unit tests.
- Error modes: missing `uv` (falls back to `pip`), PEP 668-managed Python (create venv first), missing OS packages for binary wheels.

## Key files & locations

- `services/policy-gateway/src/app.py` — policy gateway FastAPI app.
- `services/policy-gateway/src/policy_gateway/*` — hexagonal architecture code (ports, application, domain, infrastructure, interface).
- `services/policy-gateway/requirements-dev.txt` — development dependencies used by the convenience script.
- `scripts/dev_setup.sh` — convenience script that creates/activates a virtualenv, installs deps (prefers `uv`), runs `ruff` and `pytest`.
- `policies/adr-006.embedded-governance.yaml` — single source of truth for embedded governance thresholds and rules.

## Architecture (high level)

```mermaid
flowchart LR
subgraph App
PG[Policy Gateway (FastAPI)]
PG -->|proxies| vLLM[vLLM / LLM service]
end

subgraph K8s[Kubernetes]
PG_DEPLOY[Deployment: policy-gateway]
VLLM_DEPLOY[Deployment: vLLM]
INGRESS[Ingress]
CONFIGMAP[ConfigMap: adr-006.embedded-governance.yaml]
PG_DEPLOY -->|mounts| CONFIGMAP
INGRESS --> PG_DEPLOY
end

subgraph Infra
RES[Risk & Evidence Service]
PROM[Prometheus]
GRAF[Grafana]
end

PG_DEPLOY --- RES
PG_DEPLOY --- PROM
PROM --- GRAF
VLLM_DEPLOY --- PROM

note right of PG_DEPLOY
Gateway runs as a sidecar alongside the LLM in k8s.
Gateway enforces policies and forwards requests to vLLM.
end
```

## Developer workflow (diagram)

```mermaid
flowchart TD
Devbox[Devbox shell]
Devbox --> VENV[Create .venv]
VENV -->|uv present| UV[uv venv .venv]
VENV -->|uv absent| PYV[python3 -m venv .venv]
VENV --> Activate[Activate venv]
Activate --> Install[Install dev deps]
Install -->|prefer uv| UV_INSTALL[uv install -r requirements-dev.txt]
Install -->|fallback pip| PIP_INSTALL[pip install -r requirements-dev.txt]
Install --> Lint[Run ruff]
Install --> Test[Run pytest]
Test --> Run[Run uvicorn app:app]
Run --> Local[Local development / Integration testing]
CI[GitHub Actions CI] --> Install
CI --> Lint
CI --> Test
```

## Environment variables used by the services

- `PAC_CONFIG` — path to gateway config (default: `/config/adr-006.embedded-governance.yaml`).
- `PAC_UPSTREAM_URL` — upstream LLM endpoint (default: `http://localhost:8000`).
- `STORAGE_PATH` — path for evidence storage (default: `/evidence`).
- `PAC_LLM_PROVIDER` — select LLM adapter at runtime (default: `http`).
- `http` — forward to `${PAC_UPSTREAM_URL}`
- `litellm` — use local `litellm` client adapter (requires package installed)

## Streaming SSE endpoint usage

The Policy Gateway exposes `POST /proxy/completion/stream` which returns a
Server-Sent-Events stream (`text/event-stream`). Each event contains a
`data: <chunk>` line and a blank line terminator. Use this endpoint for
low-latency UI streaming or token-by-token rendering.

Quick curl test:

```bash
curl -N -H "Content-Type: application/json" \
-X POST http://localhost:8081/proxy/completion/stream \
-d '{"prompt":"Summarize the recent meeting notes"}'
```

Browser example using Fetch + ReadableStream (preferred when POST isn't
supported by EventSource):

```javascript
// POST to an SSE-compatible proxy or use a fetch streaming response
fetch('/proxy/completion/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: 'Hello' }) })
.then(resp => {
const reader = resp.body.getReader();
const decoder = new TextDecoder();
return reader.read().then(function process({ done, value }) {
if (done) return;
const chunk = decoder.decode(value, { stream: true });
console.log('chunk:', chunk);
return reader.read().then(process);
});
})
.catch(err => console.error(err));
```

Environment examples:

```bash
# Default (HTTP forwarder)
export PAC_LLM_PROVIDER=http
export PAC_UPSTREAM_URL=http://localhost:8000

# Use litellm adapter (ensure litellm installed in the runtime venv/container)
export PAC_LLM_PROVIDER=litellm
```

## Recommended quickstart (zsh / devbox)

1. Open devbox shell (recommended):

```bash
devbox shell
```

1. Run the convenience setup script (it will create `.venv`, prefer `uv` for installs, and run linter/tests):

```bash
# From the repo root
chmod +x scripts/dev_setup.sh # one-time
./scripts/dev_setup.sh
```

1. If you want to start the API after setup:

```bash
# Activate the venv created by the script (if not already active)
source .venv/bin/activate

# Run the gateway (ensure PYTHONPATH points to the service src dir)
export PYTHONPATH="$PWD/services/policy-gateway/src:${PYTHONPATH:-}"
uvicorn app:app --reload --port 8081
```

1. Run a single test file manually (example):

```bash

# Project development environment

This document describes how to set up and work with the Governed Speed project development environment. It covers the recommended local developer workflow (devbox + virtualenv), the `uv` package manager preference, key environment variables, and quick run/test/lint commands. It also includes mermaid diagrams of the high-level architecture and the developer workflow.

## Goals / contract

- Inputs: source files in the repository, a working devbox environment (or a local machine with Python 3.11+).
- Outputs: reproducible dev environment with isolated `.venv`, installed dev dependencies, runnable FastAPI policy gateway, and passing unit tests.
- Error modes: missing `uv` (falls back to `pip`), PEP 668-managed Python (create venv first), missing OS packages for binary wheels.

## Key files & locations

- `services/policy-gateway/src/app.py` — policy gateway FastAPI app.
- `services/policy-gateway/src/policy_gateway/*` — hexagonal architecture code (ports, application, domain, infrastructure, interface).
- `services/policy-gateway/requirements-dev.txt` — development dependencies used by the convenience script.
- `scripts/dev_setup.sh` — convenience script that creates/activates a virtualenv, installs deps (prefers `uv`), runs `ruff` and `pytest`.
- `policies/adr-006.embedded-governance.yaml` — single source of truth for embedded governance thresholds and rules.

## Architecture (high level)

```mermaid
flowchart LR
subgraph App
PG[Policy Gateway (FastAPI)]
PG -->|proxies| vLLM[vLLM / LLM service]
end

subgraph K8s[Kubernetes]
PG_DEPLOY[Deployment: policy-gateway]
VLLM_DEPLOY[Deployment: vLLM]
INGRESS[Ingress]
CONFIGMAP[ConfigMap: adr-006.embedded-governance.yaml]
PG_DEPLOY -->|mounts| CONFIGMAP
INGRESS --> PG_DEPLOY
end

subgraph Infra
RES[Risk & Evidence Service]
PROM[Prometheus]
GRAF[Grafana]
end

PG_DEPLOY --- RES
PG_DEPLOY --- PROM
PROM --- GRAF
VLLM_DEPLOY --- PROM

note right of PG_DEPLOY
Gateway runs as a sidecar alongside the LLM in k8s.
Gateway enforces policies and forwards requests to vLLM.
end
```

## Developer workflow (diagram)

```mermaid
flowchart TD
Devbox[Devbox shell]
Devbox --> VENV[Create .venv]
VENV -->|uv present| UV[uv venv .venv]
VENV -->|uv absent| PYV[python3 -m venv .venv]
VENV --> Activate[Activate venv]
Activate --> Install[Install dev deps]
Install -->|prefer uv| UV_INSTALL[uv install -r services/policy-gateway/requirements-dev.txt]
Install -->|fallback pip| PIP_INSTALL[pip install -r services/policy-gateway/requirements-dev.txt]
Install --> Lint[Run ruff]
Install --> Test[Run pytest]
Test --> Run[Run uvicorn app:app]
Run --> Local[Local development / Integration testing]
CI[GitHub Actions CI] --> Install
CI --> Lint
CI --> Test
```

## Environment variables used by the services

- `PAC_CONFIG` — path to gateway config (default: `/config/adr-006.embedded-governance.yaml`).
- `PAC_UPSTREAM_URL` — upstream LLM endpoint (default: `http://localhost:8000`).
- `STORAGE_PATH` — path for evidence storage (default: `/evidence`).

## Recommended quickstart (zsh / devbox)

1. Open devbox shell (recommended):

```bash
devbox shell
```

2. Run the convenience setup script (it will create `.venv`, prefer `uv` for installs, and run linter/tests):

```bash
# From the repo root
chmod +x scripts/dev_setup.sh # one-time
./scripts/dev_setup.sh
```

3. If you want to start the API after setup:

```bash
# Activate the venv created by the script (if not already active)
source .venv/bin/activate

# Run the gateway (ensure PYTHONPATH points to the service src dir)
export PYTHONPATH="$PWD/services/policy-gateway/src:${PYTHONPATH:-}"
uvicorn app:app --reload --port 8081
```

4. Run a single test file manually (example):

```bash
pytest services/policy-gateway/tests/test_policy_service.py -q
```

## Using `uv` (package manager)

- This repository prefers `uv` when available for fast installs and venv management. `scripts/dev_setup.sh` tries `uv` first and falls back to pip.
- Typical `uv` commands used by the script:
- `uv venv .venv` — create a venv
- `uv install -r services/policy-gateway/requirements-dev.txt` or `uv add -r ...` — install requirements

If `uv` is present on your machine, the script will use it automatically. If not present, the script uses `python3 -m venv` + `pip`.

## CI recommendations

- Add a GitHub Actions workflow to replicate `scripts/dev_setup.sh` steps:
1. create Python 3.11 environment
2. create/activate venv or rely on Actions runner's python
3. install `uv` if you want to use it (optional) or use `pip` to install `requirements-dev.txt`
4. run `ruff` and `pytest` as part of PR validation

## Troubleshooting

- If the script fails with a PEP 668 / "externally-managed-environment" error, create and activate a venv first:

```bash
python3 -m venv .venv
source .venv/bin/activate
./scripts/dev_setup.sh
```

- If imports fail in tests (e.g. `ModuleNotFoundError: No module named 'yaml'`) ensure `PyYAML` is installed in the active venv (the script installs it from `requirements-dev.txt`).

## Notes and next steps

- The repository follows a hexagonal architecture for the policy gateway; domain models and HTTP schemas are kept separate. We added `to_response()` helpers on domain classes to centralize mapping (see `services/policy-gateway/src/policy_gateway/domain/models.py`).
- You can add a CI job and/or a `Makefile`/`just` entry that wraps `scripts/dev_setup.sh` for a single-command developer experience.

If you want, I can also:

- Add a GitHub Actions workflow that runs `scripts/dev_setup.sh` (or equivalent steps) on PRs.
- Extend the diagrams with more details (Kubernetes manifests, ingress, or observability dashboards).
1. Run a single test file manually (example):

```bash
pytest services/policy-gateway/tests/test_policy_service.py -q
```

### Using `uv` (package manager)

- This repository prefers `uv` when available for fast installs and venv management. `scripts/dev_setup.sh` tries `uv` first and falls back to pip.
- Typical `uv` commands used by the script:
- `uv venv .venv` — create a venv
- `uv install -r services/policy-gateway/requirements-dev.txt` or `uv add -r ...` — install requirements

If `uv` is present on your machine, the script will use it automatically. If not present, the script uses `python3 -m venv` + `pip`.

### CI recommendations

- Add a GitHub Actions workflow to replicate `scripts/dev_setup.sh` steps:
1. create Python 3.11 environment
1. create/activate venv or rely on Actions runner's python
1. install `uv` if you want to use it (optional) or use `pip` to install `requirements-dev.txt`
1. run `ruff` and `pytest` as part of PR validation

### Troubleshooting

- If the script fails with a PEP 668 / "externally-managed-environment" error, create and activate a venv first:

```bash
python3 -m venv .venv
source .venv/bin/activate
./scripts/dev_setup.sh
```

- If imports fail in tests (e.g. `ModuleNotFoundError: No module named 'yaml'`) ensure `PyYAML` is installed in the active venv (the script installs it from `requirements-dev.txt`).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Major documentation duplication - consolidate repeated sections.

This document contains extensive duplication that will create maintenance burden and confusion:

  1. Lines 1-85 and 193-244: The entire "Goals / contract", "Key files & locations", and "Architecture" sections are duplicated nearly verbatim.
  2. Lines 162-191 and 273-348: Quickstart instructions repeated with minor variations.
  3. Lines 89-106 and 249-265: Developer workflow diagrams duplicated.

This appears to be the result of a merge conflict or accidental paste operation.

Recommendation: Consolidate to a single, well-organized structure:

# Project development environment

## Goals / contract
[single instance]

## Key files & locations
[single instance]

## Architecture
[single instance - one mermaid diagram]

## Developer workflow
[single instance - one mermaid diagram]

## Environment variables
[consolidated list including PAC_LLM_PROVIDER, streaming vars]

## Streaming SSE endpoint usage
[single instance with examples]

## Quickstart
[single instance with clear steps]

## Using uv
[single instance]

## CI recommendations
[single instance]

## Troubleshooting
[single instance]

Remove the duplicate blocks and organize the unique content (especially the streaming and PAC_LLM_PROVIDER sections from lines 113-160) into the appropriate sections.

🤖 Prompt for AI Agents
In docs/Reference/ENVIRONMENT.md lines 1-377 there are large duplicated blocks
(Goals/contract, Key files, Architecture, Developer workflow, Quickstart, and
other sections) likely from a bad merge; remove the repeated copies so each
logical section appears only once, keep the most complete version of each
section (merge unique bits like the Streaming SSE examples and PAC_LLM_PROVIDER
details into the canonical sections), collapse duplicated mermaid diagrams into
single instances, ensure headings/order follow the recommended outline in the
review, and run a quick pass to fix numbering/formatting and remove any stray
pasted fragments.

Comment on lines +64 to +66
from policy_gateway.domain.models import (
CompletionRequest as DomainCompletionRequest,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Move duplicate imports to the module top.

The CompletionRequest domain model is imported inside two different functions. Move this import to the top of the file alongside other domain model imports to improve code organization and avoid duplication.

Apply this diff:

 from policy_gateway.domain.models import (
     CiCheckInput,
+    CompletionRequest as DomainCompletionRequest,
     OutputDecisionInput,
     PromptDecisionInput,
 )

Then remove the inline imports:

 def proxy_completion(
     body: CompletionRequest,
 ):
     adapter = _build_llm_adapter()
-    # map HTTP request -> domain request
-    from policy_gateway.domain.models import (
-        CompletionRequest as DomainCompletionRequest,
-    )
-
     domain_req = DomainCompletionRequest(
 def proxy_completion_stream(body: CompletionRequest):
     """Stream completion results as Server-Sent-Events (SSE)."""
     adapter = _build_llm_adapter()
-    from policy_gateway.domain.models import (
-        CompletionRequest as DomainCompletionRequest,
-    )
-
     domain_req = DomainCompletionRequest(

Also applies to: 85-87

🤖 Prompt for AI Agents
In services/policy-gateway/src/app.py around lines 64-66 and 85-87, the
CompletionRequest domain model is being imported inline inside two functions;
move the duplicate import to the module top with the other domain model imports
(add "from policy_gateway.domain.models import CompletionRequest as
DomainCompletionRequest" alongside existing imports near line ~64), then remove
the inline imports at both function locations so the functions use the top-level
DomainCompletionRequest import.

SPrime01 added 2 commits November 12, 2025 11:54
… improve dev setup script, refine LiteLLMAdapter and HTTPLLMAdapter for better error handling, and update OpenAPI spec to require content and model fields.
@SPRIME01
SPRIME01 merged commit 4629772 into main Nov 12, 2025
3 of 4 checks passed
@SPRIME01
SPRIME01 deleted the codex/set-up-ci-with-github-actions branch November 12, 2025 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant