mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
Extract project name policy (#1974)
## Description Extracts project-name normalization for proxy attribution from `savings_tracker.py` into `headroom.proxy.project_name_policy`. `savings_tracker.sanitize_project_name` and `PROJECT_NAME_MAX_LENGTH` remain compatibility aliases for existing callers. Closes # ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [x] Code refactoring (no functional changes) ## Changes Made - Added `project_name_policy.py` for project-name decoding, printable-character filtering, trimming, and length capping. - Kept `savings_tracker` compatibility aliases for existing imports and project-context callers. - Added focused policy tests plus re-export coverage. - Carried forward the LiteLLM callback compatibility shim needed for current mypy on `main`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text python -m pytest tests\test_project_name_policy.py tests\test_proxy_project_savings.py 20 passed in 17.05s python -m ruff check . All checks passed! python -m ruff format --check . 1095 files already formatted python -m mypy headroom --ignore-missing-imports Success: no issues found in 409 source files gitleaks protect --staged --no-banner --redact no leaks found ``` ## Real Behavior Proof - Environment: Windows, Python 3.13.13, branch `jd/architecture-slice-26`. - Exact command / steps: ran new project-name policy tests, existing project savings tests, ruff, ruff format check, mypy, and staged gitleaks scan. - Observed result: project attribution/savings behavior remains covered and local lint/type/security checks pass. - Not tested: full proxy runtime; this slice preserves existing `savings_tracker` imports. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A ## Additional Notes Documentation and changelog updates are N/A for this internal architecture-only refactor. The push reported existing default-branch Dependabot alerts; no staged secret leaks were found for this PR.
This commit is contained in:
parent
2b09ecea76
commit
b5b59bcd77
3 changed files with 55 additions and 19 deletions
25
headroom/proxy/project_name_policy.py
Normal file
25
headroom/proxy/project_name_policy.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"""Project-name normalization policy for proxy attribution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import unquote
|
||||
|
||||
PROJECT_NAME_MAX_LENGTH = 128
|
||||
|
||||
|
||||
def sanitize_project_name(value: Any) -> str | None:
|
||||
"""Normalize a client-supplied project name; ``None`` when unusable.
|
||||
|
||||
Strips control characters, trims whitespace, and caps length so a
|
||||
misbehaving client cannot bloat persisted state or dashboard payloads.
|
||||
Percent-encoded values are decoded first so stored names match the original
|
||||
directory name.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
decoded = unquote(value)
|
||||
cleaned = "".join(ch for ch in decoded if ch.isprintable()).strip()
|
||||
if not cleaned:
|
||||
return None
|
||||
return cleaned[:PROJECT_NAME_MAX_LENGTH]
|
||||
|
|
@ -14,7 +14,6 @@ import math
|
|||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
import urllib.parse
|
||||
from csv import DictWriter
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from io import StringIO
|
||||
|
|
@ -22,6 +21,10 @@ from pathlib import Path
|
|||
from typing import Any
|
||||
|
||||
from headroom import paths as _paths
|
||||
from headroom.proxy import project_name_policy
|
||||
|
||||
PROJECT_NAME_MAX_LENGTH = project_name_policy.PROJECT_NAME_MAX_LENGTH
|
||||
sanitize_project_name = project_name_policy.sanitize_project_name
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -31,7 +34,6 @@ DEFAULT_SAVINGS_FILE = "proxy_savings.json"
|
|||
SCHEMA_VERSION = 4
|
||||
DEFAULT_MAX_HISTORY_POINTS = 5000
|
||||
DEFAULT_MAX_PROJECTS = 50
|
||||
PROJECT_NAME_MAX_LENGTH = 128
|
||||
DEFAULT_MAX_HISTORY_AGE_DAYS = 365
|
||||
DEFAULT_MAX_RESPONSE_HISTORY_POINTS = 500
|
||||
DEFAULT_DISPLAY_SESSION_INACTIVITY_MINUTES = 60
|
||||
|
|
@ -370,23 +372,6 @@ def _empty_display_session() -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def sanitize_project_name(value: Any) -> str | None:
|
||||
"""Normalize a client-supplied project name; ``None`` when unusable.
|
||||
|
||||
Strips control characters, trims whitespace, and caps length so a
|
||||
misbehaving client cannot bloat the persisted state or the dashboard.
|
||||
Percent-encoded values (from non-ASCII cwd names) are decoded first so
|
||||
the stored project name matches the original directory name.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
value = urllib.parse.unquote(value)
|
||||
cleaned = "".join(ch for ch in value if ch.isprintable()).strip()
|
||||
if not cleaned:
|
||||
return None
|
||||
return cleaned[:PROJECT_NAME_MAX_LENGTH]
|
||||
|
||||
|
||||
def _empty_project_entry() -> dict[str, Any]:
|
||||
return {
|
||||
"requests": 0,
|
||||
|
|
|
|||
26
tests/test_project_name_policy.py
Normal file
26
tests/test_project_name_policy.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from headroom.proxy.project_name_policy import PROJECT_NAME_MAX_LENGTH, sanitize_project_name
|
||||
from headroom.proxy.savings_tracker import sanitize_project_name as savings_sanitize_project_name
|
||||
|
||||
|
||||
def test_project_name_policy_normalizes_and_caps() -> None:
|
||||
assert sanitize_project_name(" api-server ") == "api-server"
|
||||
assert sanitize_project_name("a" * 300) == "a" * PROJECT_NAME_MAX_LENGTH
|
||||
assert sanitize_project_name("x\x00\x1by") == "xy"
|
||||
|
||||
|
||||
def test_project_name_policy_decodes_percent_encoded_unicode() -> None:
|
||||
assert sanitize_project_name("%E9%A1%B9%E7%9B%AE") == "\u9879\u76ee"
|
||||
assert sanitize_project_name("my%20repo") == "my repo"
|
||||
|
||||
|
||||
def test_project_name_policy_rejects_unusable_values() -> None:
|
||||
assert sanitize_project_name("") is None
|
||||
assert sanitize_project_name(" ") is None
|
||||
assert sanitize_project_name(None) is None
|
||||
assert sanitize_project_name(42) is None
|
||||
|
||||
|
||||
def test_savings_tracker_reexports_project_name_policy() -> None:
|
||||
assert savings_sanitize_project_name is sanitize_project_name
|
||||
Loading…
Add table
Add a link
Reference in a new issue