rein-aharness/agent_harness/profiles.py
tegwick 4144eba160 feat: instance manifest, tool profiles, metrics, and budget enforcement
Land HARNESS-WP-0001 T01/T02/T04/T05: extend ADR-005 schedule.yml with
harness fields, named tool-profile registry, ADR-004 metrics writes, and
BudgetTracker wiring. CLI gains validate/profiles; task-file path kept.
2026-07-17 23:49:03 +02:00

78 lines
2.4 KiB
Python

"""Named tool-profile registry.
Instances declare a profile by name in the instance manifest; the harness
resolves and enforces the allow-list. Instances never enumerate tools.
Unknown profile names refuse to run.
"""
from __future__ import annotations
from dataclasses import dataclass
class UnknownToolProfileError(ValueError):
"""Raised when a manifest references a profile that is not registered."""
# Claude Code --allowedTools strings. No push, no network, no arbitrary shell.
_GREEN_COMMIT_TOOLS = (
"Read,Write,Edit,Glob,Grep,"
"Bash(git add:*),Bash(git commit:*),Bash(git status),"
"Bash(git log:*),Bash(git diff:*),Bash(date:*),Bash(ls:*)"
)
# Blue-lane mail triage session: same session tools as green-commit-only.
# Credentialed IMAP scan is a deterministic pre-step outside the session
# (see mailscan.py); the session only reads reports and updates queues.
_BLUE_MAIL_TRIAGE_TOOLS = _GREEN_COMMIT_TOOLS
@dataclass(frozen=True)
class ToolProfile:
"""A named hard allow-list for agentic sessions."""
name: str
description: str
allowed_tools: str
lane: str # green | blue — advisory; enforcement is the allow-list
PROFILES: dict[str, ToolProfile] = {
"green-commit-only": ToolProfile(
name="green-commit-only",
description=(
"Green-lane local commit: read/edit tools plus git add/commit/"
"status/log/diff. No push, no network, no arbitrary shell."
),
allowed_tools=_GREEN_COMMIT_TOOLS,
lane="green",
),
"blue-mail-triage": ToolProfile(
name="blue-mail-triage",
description=(
"Blue-lane mail triage session after a credentialed deterministic "
"scan: same local commit tools as green-commit-only. Credentials "
"and network stay outside the agentic session."
),
allowed_tools=_BLUE_MAIL_TRIAGE_TOOLS,
lane="blue",
),
}
def get_profile(name: str) -> ToolProfile:
"""Resolve a profile by name. Raises UnknownToolProfileError if missing."""
key = (name or "").strip()
if not key:
raise UnknownToolProfileError("tool_profile name is empty")
profile = PROFILES.get(key)
if profile is None:
known = ", ".join(sorted(PROFILES))
raise UnknownToolProfileError(
f"unknown tool_profile '{key}' (known: {known})"
)
return profile
def list_profiles() -> list[ToolProfile]:
return [PROFILES[k] for k in sorted(PROFILES)]