Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/openharness/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import json
import os
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any
Expand All @@ -22,6 +23,21 @@
from openharness.permissions.modes import PermissionMode


# ANSI escape sequence pattern
_ANSI_ESCAPE_PATTERN = re.compile(r"\x1b\[[0-9;]*m")


def strip_ansi_escape_sequences(text: str) -> str:
"""Remove ANSI escape sequences from text.

This is used to clean environment variables that may contain terminal
formatting codes (e.g., '[1m' for bold) which can corrupt API requests.
"""
if not text:
return text
return _ANSI_ESCAPE_PATTERN.sub("", text)


class PathRuleConfig(BaseModel):
"""A glob-pattern path permission rule."""

Expand Down Expand Up @@ -651,6 +667,9 @@ def resolve_auth(self) -> ResolvedAuth:
def merge_cli_overrides(self, **overrides: Any) -> Settings:
"""Return a new Settings with CLI overrides applied (non-None values only)."""
updates = {k: v for k, v in overrides.items() if v is not None}
# Strip ANSI escape sequences from model name if present
if "model" in updates and isinstance(updates["model"], str):
updates["model"] = strip_ansi_escape_sequences(updates["model"])
merged = self.model_copy(update=updates)
if not updates:
return merged
Expand All @@ -668,7 +687,9 @@ def _apply_env_overrides(settings: Settings) -> Settings:
updates: dict[str, Any] = {}
model = os.environ.get("ANTHROPIC_MODEL") or os.environ.get("OPENHARNESS_MODEL")
if model:
updates["model"] = model
# Strip ANSI escape sequences that may be present if the environment
# variable was set with terminal formatting (e.g., bold text)
updates["model"] = strip_ansi_escape_sequences(model)

base_url = (
os.environ.get("ANTHROPIC_BASE_URL")
Expand Down
43 changes: 43 additions & 0 deletions tests/test_config/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
load_settings,
normalize_anthropic_model_name,
save_settings,
strip_ansi_escape_sequences,
_apply_env_overrides,
)


Expand Down Expand Up @@ -416,3 +418,44 @@ def test_load_with_sandbox_settings(self, tmp_path: Path):
assert s.sandbox.network.allowed_domains == ["github.com"]
assert s.sandbox.filesystem.allow_write == [".", "/tmp"]
assert s.sandbox.filesystem.deny_write == [".env"]


class TestAnsiEscapeSequences:
"""Tests for ANSI escape sequence handling in settings."""

def test_strip_ansi_escape_sequences(self):
"""Test that ANSI escape sequences are properly stripped."""
# Normal model name should pass through unchanged
assert strip_ansi_escape_sequences("claude-opus-4-6") == "claude-opus-4-6"
# Bold formatting should be stripped
assert strip_ansi_escape_sequences("\x1b[1mclaude-opus-4-6\x1b[0m") == "claude-opus-4-6"
# Green + bold formatting should be stripped
assert strip_ansi_escape_sequences("\x1b[32m\x1b[1mclaude-opus-4-6\x1b[0m") == "claude-opus-4-6"
# Only bold prefix
assert strip_ansi_escape_sequences("\x1b[1mclaude-opus-4-6") == "claude-opus-4-6"
# Only reset suffix
assert strip_ansi_escape_sequences("claude-opus-4-6\x1b[0m") == "claude-opus-4-6"
# Empty string should return empty string
assert strip_ansi_escape_sequences("") == ""
# None should return None
assert strip_ansi_escape_sequences(None) is None

def test_env_override_strips_ansi_from_model(self, monkeypatch):
"""Test that ANSI escape sequences are stripped from ANTHROPIC_MODEL env var."""
monkeypatch.setenv("ANTHROPIC_MODEL", "\x1b[1mclaude-opus-4-6\x1b[0m")
s = Settings()
updated = _apply_env_overrides(s)
assert updated.model == "claude-opus-4-6"

def test_env_override_strips_ansi_from_openharness_model(self, monkeypatch):
"""Test that ANSI escape sequences are stripped from OPENHARNESS_MODEL env var."""
monkeypatch.setenv("OPENHARNESS_MODEL", "\x1b[32mclaude-sonnet-4-6\x1b[0m")
s = Settings()
updated = _apply_env_overrides(s)
assert updated.model == "claude-sonnet-4-6"

def test_merge_cli_overrides_strips_ansi_from_model(self):
"""Test that ANSI escape sequences are stripped from CLI model override."""
s = Settings()
updated = s.merge_cli_overrides(model="\x1b[1mclaude-opus-4-6\x1b[0m")
assert updated.model == "claude-opus-4-6"