Skip to content

Commit c797a27

Browse files
tbitcsoz-agent
andcommitted
feat: P1 — role-based attribution headers in governance proxy
- Add X-Specsmith-Role, X-Specsmith-Model, X-Specsmith-Provider response headers to POST /v1/chat/completions proxy endpoint - Add _infer_role_from_messages() — keyword-based role detection from system prompts across 10 roles (coder, architect, reviewer, editor, researcher, tester, classifier, strategist, drafter, ip-analyst) - Add _resolve_provider_name() — provider identification from env config - Runner already has execution profile filtering via _filter_by_execution_profile Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent c6b42ed commit c797a27

1 file changed

Lines changed: 60 additions & 0 deletions

File tree

src/specsmith/governance_logic.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,13 @@ def do_POST(self) -> None: # noqa: N802
659659
elif self.path == "/v1/chat/completions":
660660
# Kairos BYOE gateway — intercept, gate, forward.
661661
try:
662+
# Detect role from request header or infer from system prompt.
663+
req_role = self.headers.get("X-Specsmith-Role", "")
664+
if not req_role:
665+
req_role = _infer_role_from_messages(
666+
body.get("messages") or []
667+
)
668+
662669
result = run_chat_proxy(
663670
messages=body.get("messages") or [],
664671
model=body.get("model", "kairos"),
@@ -667,10 +674,15 @@ def do_POST(self) -> None: # noqa: N802
667674
import json as _j
668675

669676
raw = _j.dumps(result, ensure_ascii=False).encode()
677+
effective_model = result.get("model", body.get("model", ""))
678+
effective_provider = _resolve_provider_name()
670679
self.send_response(200)
671680
self.send_header("Content-Type", "application/json")
672681
self.send_header("Content-Length", str(len(raw)))
673682
self.send_header("x-kairos-governance", "gated")
683+
self.send_header("X-Specsmith-Role", req_role or "coder")
684+
self.send_header("X-Specsmith-Model", effective_model)
685+
self.send_header("X-Specsmith-Provider", effective_provider)
674686
self.send_header("Access-Control-Allow-Origin", "*")
675687
self.end_headers()
676688
self.wfile.write(raw)
@@ -714,6 +726,54 @@ def do_OPTIONS(self) -> None: # noqa: N802
714726
# ---------------------------------------------------------------------------
715727

716728

729+
# Role keywords used by _infer_role_from_messages to detect intent from system prompts.
730+
_ROLE_KEYWORDS: dict[str, list[str]] = {
731+
"coder": ["write code", "implement", "code", "function", "diff"],
732+
"architect": ["design", "architecture", "system", "trade-off"],
733+
"reviewer": ["review", "feedback", "quality", "pr"],
734+
"editor": ["edit", "format", "refactor", "fix"],
735+
"researcher": ["research", "documentation", "lookup", "search"],
736+
"tester": ["test", "coverage", "assertion", "spec"],
737+
"classifier": ["classify", "categorize", "intent"],
738+
"strategist": ["strategy", "business", "competitive", "market"],
739+
"drafter": ["draft", "specification", "proposal", "report"],
740+
"ip-analyst": ["patent", "claims", "prior art", "ip", "freedom"],
741+
}
742+
743+
744+
def _infer_role_from_messages(messages: list[dict[str, Any]]) -> str:
745+
"""Best-effort role inference from system prompt keywords."""
746+
system_text = ""
747+
for msg in messages:
748+
if isinstance(msg, dict) and msg.get("role") == "system":
749+
content = msg.get("content", "")
750+
system_text += (content if isinstance(content, str) else str(content)).lower()
751+
if not system_text:
752+
return "coder"
753+
best_role = "coder"
754+
best_count = 0
755+
for role, keywords in _ROLE_KEYWORDS.items():
756+
count = sum(1 for kw in keywords if kw in system_text)
757+
if count > best_count:
758+
best_count = count
759+
best_role = role
760+
return best_role
761+
762+
763+
def _resolve_provider_name() -> str:
764+
"""Return the configured AI provider name for attribution headers."""
765+
provider = os.environ.get("KAIROS_AI_BASE_URL", "")
766+
if not provider:
767+
return "specsmith-local"
768+
if "openai" in provider:
769+
return "openai"
770+
if "anthropic" in provider:
771+
return "anthropic"
772+
if "localhost" in provider or "127.0.0.1" in provider:
773+
return "local"
774+
return "byoe"
775+
776+
717777
def _read_confidence_threshold(root: Path) -> float | None:
718778
cfg = root / ".specsmith" / "config.yml"
719779
if not cfg.is_file():

0 commit comments

Comments
 (0)