from __future__ import annotations import hashlib import ipaddress import json import re from datetime import UTC, datetime from enum import Enum from pathlib import PurePosixPath from typing import Any from urllib.parse import urlparse import a2a_pack as a2a from a2a_pack import ( A2AAgent, ConsumerSetup, ConsumerSetupField, EgressPolicy, NoAuth, Pricing, Resources, RunContext, WorkspaceAccess, WorkspaceMode, ) from a2a_pack.context import AgentEvent from pydantic import BaseModel, ConfigDict, Field, field_validator APPROVAL_ACK = "I_APPROVE_THE_LISTED_STEPS" OUTPUT_ROOT = "outputs/support-cases" MAX_TEXT = 24000 MAX_ITEMS = 80 class StrictModel(BaseModel): model_config = ConfigDict(extra="forbid", use_enum_values=True) class SupportToFixEngineerConfig(StrictModel): max_log_chars: int = Field(default=12000, ge=1000, le=50000) default_dry_run: bool = True class Severity(str, Enum): sev1 = "sev1" sev2 = "sev2" sev3 = "sev3" sev4 = "sev4" class EvidenceKind(str, Enum): ticket = "ticket" log = "log" trace = "trace" release = "release" change = "change" customer = "customer" health_check = "health_check" repository = "repository" validation = "validation" class DeliveryAction(str, Enum): draft_branch = "draft_branch" draft_commit = "draft_commit" draft_pr = "draft_pr" customer_summary = "customer_summary" class KeyValue(StrictModel): key: str = Field(min_length=1, max_length=120) value: str = Field(default="", max_length=1200) @field_validator("key", "value") @classmethod def clean(cls, value: str) -> str: return redact(value.strip())[:1200] class EvidenceItem(StrictModel): source_id: str = Field(min_length=1, max_length=160) kind: EvidenceKind summary: str = Field(min_length=1, max_length=1200) timestamp: str | None = Field(default=None, max_length=80) url: str | None = Field(default=None, max_length=2048) excerpt: str | None = Field(default=None, max_length=4000) @field_validator("source_id", "summary", "excerpt") @classmethod def clean_text(cls, value: str | None) -> str | None: return None if value is None else redact(value)[:4000] @field_validator("url") @classmethod def safe_url(cls, value: str | None) -> str | None: if not value: return None return validate_safe_url(value, allowed_hosts=set()) class HttpCheck(StrictModel): name: str = Field(min_length=1, max_length=80) url: str = Field(min_length=8, max_length=2048) expected_status: int = Field(default=200, ge=100, le=599) class RepoFileChange(StrictModel): path: str = Field(min_length=1, max_length=240) reason: str = Field(min_length=1, max_length=800) proposed_content: str | None = Field(default=None, max_length=20000) @field_validator("path") @classmethod def path_ok(cls, value: str) -> str: return safe_path(value) @field_validator("reason", "proposed_content") @classmethod def redact_text(cls, value: str | None) -> str | None: return None if value is None else redact(value) class TriageCaseInput(StrictModel): case_id: str = Field(min_length=3, max_length=80, pattern=r"^[A-Za-z0-9][A-Za-z0-9._-]{2,79}$") customer_id: str = Field(min_length=1, max_length=120) tenant_id: str = Field(min_length=1, max_length=120) release: str = Field(min_length=1, max_length=120) ticket_text: str = Field(min_length=1, max_length=MAX_TEXT) evidence: list[EvidenceItem] = Field(default_factory=list, max_length=MAX_ITEMS) affected_services: list[str] = Field(default_factory=list, max_length=20) recent_changes: list[str] = Field(default_factory=list, max_length=40) dry_run: bool = True @field_validator("ticket_text", "customer_id", "tenant_id", "release") @classmethod def redact_fields(cls, value: str) -> str: return redact(value) @field_validator("affected_services", "recent_changes") @classmethod def clean_list(cls, values: list[str]) -> list[str]: return [redact(v.strip())[:500] for v in values if v.strip()] class ReproduceIssueInput(StrictModel): case_id: str = Field(min_length=3, max_length=80, pattern=r"^[A-Za-z0-9][A-Za-z0-9._-]{2,79}$") redacted_symptom: str = Field(min_length=1, max_length=8000) environment: list[KeyValue] = Field(default_factory=list, max_length=30) observed_errors: list[str] = Field(default_factory=list, max_length=40) candidate_files: list[str] = Field(default_factory=list, max_length=40) health_checks: list[HttpCheck] = Field(default_factory=list, max_length=20) allowed_health_hosts: list[str] = Field(default_factory=list, max_length=30) dry_run: bool = True @field_validator("redacted_symptom") @classmethod def redact_symptom(cls, value: str) -> str: return redact(value) @field_validator("candidate_files") @classmethod def safe_paths(cls, values: list[str]) -> list[str]: return [safe_path(v) for v in values] class ProposeFixInput(StrictModel): case_id: str = Field(min_length=3, max_length=80, pattern=r"^[A-Za-z0-9][A-Za-z0-9._-]{2,79}$") reproduction_summary: str = Field(min_length=1, max_length=12000) hypotheses: list[str] = Field(default_factory=list, max_length=20) supporting_evidence: list[EvidenceItem] = Field(default_factory=list, max_length=MAX_ITEMS) contradicting_evidence: list[EvidenceItem] = Field(default_factory=list, max_length=MAX_ITEMS) proposed_changes: list[RepoFileChange] = Field(default_factory=list, max_length=40) repository: str | None = Field(default=None, max_length=200) dry_run: bool = True @field_validator("reproduction_summary") @classmethod def redact_summary(cls, value: str) -> str: return redact(value) class ValidateFixInput(StrictModel): case_id: str = Field(min_length=3, max_length=80, pattern=r"^[A-Za-z0-9][A-Za-z0-9._-]{2,79}$") patch_digest: str = Field(min_length=16, max_length=128) patch_diff: str = Field(min_length=1, max_length=60000) test_selectors: list[str] = Field(default_factory=list, max_length=40) lint_selectors: list[str] = Field(default_factory=list, max_length=20) security_checks: list[str] = Field(default_factory=list, max_length=20) dry_run: bool = True @field_validator("test_selectors", "lint_selectors", "security_checks") @classmethod def selector_ok(cls, values: list[str]) -> list[str]: out: list[str] = [] for item in values: cleaned = item.strip() if cleaned: if re.search(r"[;&|`$<>]", cleaned): raise ValueError("selectors must not contain shell metacharacters") out.append(cleaned[:180]) return out @field_validator("patch_diff") @classmethod def patch_clean(cls, value: str) -> str: return redact(value) class PrepareDeliveryInput(StrictModel): case_id: str = Field(min_length=3, max_length=80, pattern=r"^[A-Za-z0-9][A-Za-z0-9._-]{2,79}$") validation_digest: str = Field(min_length=16, max_length=128) patch_digest: str = Field(min_length=16, max_length=128) repository: str = Field(min_length=1, max_length=200) target_branch: str = Field(default="main", min_length=1, max_length=120) proposed_branch: str = Field(default="support-fix", min_length=1, max_length=120) delivery_actions: list[DeliveryAction] = Field(default_factory=lambda: [DeliveryAction.customer_summary], max_length=4) customer_summary_context: str = Field(default="", max_length=8000) dry_run: bool = True approval_acknowledgement: str | None = Field(default=None, max_length=80) approval_plan_digest: str | None = Field(default=None, max_length=128) @field_validator("repository", "target_branch", "proposed_branch") @classmethod def ref_ok(cls, value: str) -> str: if re.search(r"[\s;&|`$<>]", value): raise ValueError("repository and branch values must not contain whitespace or shell metacharacters") return value class ArtifactRecord(StrictModel): path: str artifact_uri: str | None = None sha256: str size_bytes: int = Field(ge=0) class AuditRecord(StrictModel): event: str at: str case_id: str details: list[KeyValue] = Field(default_factory=list, max_length=20) class HypothesisRank(StrictModel): hypothesis: str rank: int = Field(ge=1) confidence: float = Field(ge=0.0, le=1.0) supporting_source_ids: list[str] contradicting_source_ids: list[str] class CheckResult(StrictModel): name: str status: str class TriageCaseOutput(StrictModel): status: str case_id: str severity: Severity owner: str redaction_count: int = Field(ge=0) missing_setup: list[str] allowed_tenant: bool plan_digest: str evidence_index: list[EvidenceItem] artifacts: list[ArtifactRecord] audit: list[AuditRecord] warnings: list[str] class ReproduceIssueOutput(StrictModel): status: str case_id: str reproduction_digest: str minimal_steps: list[str] safe_health_checks: list[HttpCheck] blocked_health_checks: list[str] artifacts: list[ArtifactRecord] audit: list[AuditRecord] warnings: list[str] class ProposeFixOutput(StrictModel): status: str case_id: str repository: str | None patch_digest: str plan_digest: str hypotheses: list[HypothesisRank] scoped_paths: list[str] artifacts: list[ArtifactRecord] audit: list[AuditRecord] warnings: list[str] class ValidateFixOutput(StrictModel): status: str case_id: str validation_digest: str patch_digest: str checks: list[CheckResult] artifacts: list[ArtifactRecord] audit: list[AuditRecord] warnings: list[str] class PrepareDeliveryOutput(StrictModel): status: str case_id: str plan_digest: str approval_required: bool approved: bool executed_actions: list[str] blocked_actions: list[str] artifacts: list[ArtifactRecord] audit: list[AuditRecord] warnings: list[str] class SupportToFixEngineer(A2AAgent[SupportToFixEngineerConfig, NoAuth]): name = "support-to-fix-engineer" description = "Turns support cases into evidence-backed fix artifacts with approval-gated delivery." version = "0.1.0" config_model = SupportToFixEngineerConfig auth_model = NoAuth pricing = Pricing(price_per_call_usd=0.0, caller_pays_llm=False, notes="Deterministic policy and artifact generation; no LLM credential required.") resources = Resources(cpu="1", memory="512Mi", max_runtime_seconds=600) workspace_access = WorkspaceAccess.dynamic( max_files=160, allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY), require_reason=True, deny_patterns=("**/.env", "**/.env.*", "**/*secret*", "**/*credential*", "**/id_rsa", "**/id_ed25519"), require_human_approval=True, max_total_size_bytes=50 * 1024 * 1024, ) consumer_setup = ConsumerSetup.from_fields( ConsumerSetupField.config("GITEA_BASE_URL", label="Gitea base URL", input_type="url", required=False), ConsumerSetupField.secret("GITEA_TOKEN", label="Gitea token", required=False), ConsumerSetupField.config("GITEA_ALLOWED_REPOS", label="Allowed repositories", required=False), ConsumerSetupField.config("KUBERNETES_API_URL", label="Kubernetes API URL", input_type="url", required=False), ConsumerSetupField.secret("KUBERNETES_TOKEN", label="Kubernetes token", required=False), ConsumerSetupField.config("ARGOCD_BASE_URL", label="Argo CD base URL", input_type="url", required=False), ConsumerSetupField.secret("ARGOCD_TOKEN", label="Argo CD token", required=False), ConsumerSetupField.config("OBSERVABILITY_BASE_URL", label="Observability base URL", input_type="url", required=False), ConsumerSetupField.secret("OBSERVABILITY_TOKEN", label="Observability token", required=False), ConsumerSetupField.config("TENANT_ALLOWLIST", label="Tenant allowlist", required=False), ConsumerSetupField.config("HEALTHCHECK_ALLOWED_HOSTS", label="HTTP health check hosts", required=False), ) egress = EgressPolicy(allow_hosts=(), deny_internet_by_default=True) tools_used = ("pydantic", "workspace") @a2a.tool(description="Intake, redact, correlate evidence, classify severity and ownership for a bounded support case", timeout_seconds=120, idempotent=True) async def triage_case(self, ctx: RunContext[NoAuth], case: TriageCaseInput) -> TriageCaseOutput: await emit_audit(ctx, case.case_id, "triage_started", [kv("dry_run", case.dry_run)]) warnings = warnings_for(case.ticket_text, [e.summary for e in case.evidence]) allowed = tenant_allowed(ctx, case.tenant_id) severity = classify_severity(case.ticket_text, case.evidence) owner = classify_owner(case.affected_services, case.ticket_text, case.recent_changes) evidence = normalize_evidence(case) payload = {"case_id": case.case_id, "customer_id": case.customer_id, "tenant_id": case.tenant_id, "release": case.release, "severity": severity.value, "owner": owner, "allowed_tenant": allowed, "evidence": [e.model_dump(mode="json") for e in evidence]} digest = stable_digest(payload) artifacts = [await write_case_file(ctx, case.case_id, "redacted_case.json", dumps(payload)), await write_case_file(ctx, case.case_id, "triage.json", dumps({**payload, "plan_digest": digest}))] audit = [audit_record(case.case_id, "triage_completed", [kv("plan_digest", digest), kv("severity", severity.value)])] await emit_audit(ctx, case.case_id, "triage_completed", [kv("plan_digest", digest)]) setup = missing_setup(ctx) if setup: warnings.append("Some integrations are not configured; analysis used caller-supplied evidence only.") return TriageCaseOutput(status="triaged" if allowed else "blocked_ungranted_tenant", case_id=case.case_id, severity=severity, owner=owner, redaction_count=count_redactions(case.ticket_text), missing_setup=setup, allowed_tenant=allowed, plan_digest=digest, evidence_index=evidence, artifacts=artifacts, audit=audit, warnings=warnings) @a2a.tool(description="Create a deterministic minimal reproduction and validate HTTP health-check targets against SSRF protections", timeout_seconds=120, idempotent=True) async def reproduce_issue(self, ctx: RunContext[NoAuth], reproduction: ReproduceIssueInput) -> ReproduceIssueOutput: await emit_audit(ctx, reproduction.case_id, "reproduction_started", [kv("dry_run", reproduction.dry_run)]) allowed_hosts = set(reproduction.allowed_health_hosts or split_csv(ctx.consumer_config("HEALTHCHECK_ALLOWED_HOSTS", ""))) safe: list[HttpCheck] = [] blocked: list[str] = [] for check in reproduction.health_checks: try: validate_safe_url(check.url, allowed_hosts=allowed_hosts) safe.append(check) except ValueError as exc: blocked.append(f"{check.name}: {exc}") steps = repro_steps(reproduction) digest = stable_digest({"steps": steps, "safe": [c.model_dump(mode="json") for c in safe], "blocked": blocked}) artifact = await write_case_file(ctx, reproduction.case_id, "reproduction.md", repro_markdown(reproduction, steps, safe, blocked)) audit = [audit_record(reproduction.case_id, "reproduction_completed", [kv("reproduction_digest", digest)])] await emit_audit(ctx, reproduction.case_id, "reproduction_completed", [kv("reproduction_digest", digest)]) return ReproduceIssueOutput(status="reproduced_deterministically", case_id=reproduction.case_id, reproduction_digest=digest, minimal_steps=steps, safe_health_checks=safe, blocked_health_checks=blocked, artifacts=[artifact], audit=audit, warnings=warnings_for(reproduction.redacted_symptom, reproduction.observed_errors)) @a2a.tool(description="Rank hypotheses with provenance and produce a scoped patch.diff without modifying a repository", timeout_seconds=180, idempotent=True) async def propose_fix(self, ctx: RunContext[NoAuth], proposal: ProposeFixInput) -> ProposeFixOutput: await emit_audit(ctx, proposal.case_id, "proposal_started", [kv("dry_run", proposal.dry_run)]) ranked = rank_hypotheses(proposal.hypotheses, proposal.supporting_evidence, proposal.contradicting_evidence) paths = [c.path for c in proposal.proposed_changes] patch = patch_diff(proposal.proposed_changes) patch_digest = stable_digest({"patch_diff": patch, "paths": paths}) plan_digest = stable_digest({"case_id": proposal.case_id, "repository": proposal.repository, "patch_digest": patch_digest, "paths": paths}) diagnosis = {"case_id": proposal.case_id, "repository": proposal.repository, "hypotheses": [h.model_dump(mode="json") for h in ranked], "scoped_paths": paths, "dry_run": proposal.dry_run, "note": "No repository mutation performed. Patch is an output artifact only."} artifacts = [await write_case_file(ctx, proposal.case_id, "diagnosis.json", dumps(diagnosis)), await write_case_file(ctx, proposal.case_id, "patch.diff", patch)] audit = [audit_record(proposal.case_id, "proposal_completed", [kv("patch_digest", patch_digest), kv("plan_digest", plan_digest)])] await emit_audit(ctx, proposal.case_id, "proposal_completed", [kv("patch_digest", patch_digest)]) return ProposeFixOutput(status="patch_proposed_no_repository_write", case_id=proposal.case_id, repository=proposal.repository, patch_digest=patch_digest, plan_digest=plan_digest, hypotheses=ranked, scoped_paths=paths, artifacts=artifacts, audit=audit, warnings=warnings_for(proposal.reproduction_summary, proposal.hypotheses)) @a2a.tool(description="Validate a proposed patch with deterministic static checks and test-plan selectors without executing arbitrary shell", timeout_seconds=180, idempotent=True) async def validate_fix(self, ctx: RunContext[NoAuth], validation: ValidateFixInput) -> ValidateFixOutput: await emit_audit(ctx, validation.case_id, "validation_started", [kv("dry_run", validation.dry_run)]) checks = validation_checks(validation) payload = {"case_id": validation.case_id, "patch_digest": validation.patch_digest, "checks": [c.model_dump(mode="json") for c in checks], "test_selectors": validation.test_selectors, "lint_selectors": validation.lint_selectors, "security_checks": validation.security_checks, "dry_run": validation.dry_run, "note": "No arbitrary shell was executed; selectors are a reviewable validation plan."} digest = stable_digest(payload) artifact = await write_case_file(ctx, validation.case_id, "validation.json", dumps({**payload, "validation_digest": digest})) audit = [audit_record(validation.case_id, "validation_completed", [kv("validation_digest", digest)])] await emit_audit(ctx, validation.case_id, "validation_completed", [kv("validation_digest", digest)]) warnings = [] if all(c.status == "pass" for c in checks) else ["One or more deterministic checks require human review."] return ValidateFixOutput(status="validated_dry_run" if validation.dry_run else "validated_without_deploy", case_id=validation.case_id, validation_digest=digest, patch_digest=validation.patch_digest, checks=checks, artifacts=[artifact], audit=audit, warnings=warnings) @a2a.tool(description="Prepare a reviewable branch/commit/PR draft and customer-safe summary; gated actions require approval bound to the plan digest", timeout_seconds=180, idempotent=True) async def prepare_delivery(self, ctx: RunContext[NoAuth], delivery: PrepareDeliveryInput) -> PrepareDeliveryOutput: await emit_audit(ctx, delivery.case_id, "delivery_started", [kv("dry_run", delivery.dry_run)]) actions = [str(a.value if isinstance(a, DeliveryAction) else a) for a in delivery.delivery_actions] plan = {"case_id": delivery.case_id, "repository": delivery.repository, "target_branch": delivery.target_branch, "proposed_branch": delivery.proposed_branch, "patch_digest": delivery.patch_digest, "validation_digest": delivery.validation_digest, "actions": actions} digest = stable_digest(plan) consequential = any(a in {"draft_branch", "draft_commit", "draft_pr"} for a in actions) approved = consequential and not delivery.dry_run and delivery.approval_acknowledgement == APPROVAL_ACK and delivery.approval_plan_digest == digest approval_required = consequential and not approved executed = actions if approved else (["customer_summary"] if "customer_summary" in actions else []) blocked = [a for a in actions if a not in executed] artifact = await write_case_file(ctx, delivery.case_id, "delivery.md", delivery_doc(delivery, digest, approval_required, approved, executed, blocked)) audit = [audit_record(delivery.case_id, "delivery_prepared", [kv("plan_digest", digest), kv("approved", approved)])] await emit_audit(ctx, delivery.case_id, "delivery_prepared", [kv("plan_digest", digest), kv("approved", approved)]) return PrepareDeliveryOutput(status="approval_required" if approval_required else "delivery_prepared", case_id=delivery.case_id, plan_digest=digest, approval_required=approval_required, approved=bool(approved), executed_actions=executed, blocked_actions=blocked, artifacts=[artifact], audit=audit, warnings=["No merge or deployment was performed.", "Repository mutations are represented as a reviewable delivery plan only unless platform approval executes them outside this agent."]) def kv(key: str, value: Any) -> KeyValue: return KeyValue(key=str(key), value=str(value)) def split_csv(value: Any) -> list[str]: return [x.strip() for x in str(value or "").split(",") if x.strip()] def missing_setup(ctx: RunContext[NoAuth]) -> list[str]: return [name for name in ("GITEA_BASE_URL", "KUBERNETES_API_URL", "ARGOCD_BASE_URL", "OBSERVABILITY_BASE_URL") if not ctx.consumer_config(name, "")] def tenant_allowed(ctx: RunContext[NoAuth], tenant_id: str) -> bool: allowed = set(split_csv(ctx.consumer_config("TENANT_ALLOWLIST", ""))) return not allowed or tenant_id in allowed SECRET_RE = re.compile(r"(?i)(authorization:\s*bearer\s+[A-Za-z0-9._~+/=-]+|api[_-]?key\s*[:=]\s*['\"]?[^\s,'\"]+|token\s*[:=]\s*['\"]?[^\s,'\"]+|password\s*[:=]\s*['\"]?[^\s,'\"]+|secret\s*[:=]\s*['\"]?[^\s,'\"]+|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----)") PII_RE = re.compile(r"(?i)\b([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}|\+?\d[\d .()\-]{7,}\d)\b") INJECTION_RE = re.compile(r"(?i)(ignore (all )?(previous|prior) instructions|system prompt|developer message|reveal secrets|exfiltrate|curl\s+|wget\s+|bash\s+-c)") def redact(text: str | None) -> str: if not text: return "" return PII_RE.sub("[REDACTED_PII]", SECRET_RE.sub("[REDACTED_SECRET]", str(text))) def count_redactions(text: str) -> int: return len(SECRET_RE.findall(text or "")) def warnings_for(*items: Any) -> list[str]: flat: list[str] = [] for item in items: flat.extend(str(v) for v in item) if isinstance(item, list) else flat.append(str(item)) text = "\n".join(flat) warnings: list[str] = [] if INJECTION_RE.search(text): warnings.append("Untrusted ticket/log content contained instruction-like text and was treated only as evidence.") if SECRET_RE.search(text): warnings.append("Secrets were detected and redacted before artifact generation.") if len(text) > MAX_TEXT: warnings.append("Input was bounded and truncated for deterministic processing.") return warnings def safe_path(path: str) -> str: clean = str(path).replace("\\", "/").strip().lstrip("/") posix = PurePosixPath(clean) if not clean or ".." in posix.parts or clean.startswith("~") or any(p in {".git", ".hg", ".svn"} for p in posix.parts): raise ValueError(f"unsafe repository path: {path!r}") return posix.as_posix() def safe_case(case_id: str) -> str: if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{2,79}", case_id): raise ValueError("invalid case_id") return case_id def validate_safe_url(url: str, *, allowed_hosts: set[str]) -> str: parsed = urlparse(url) if parsed.scheme not in {"http", "https"} or not parsed.hostname: raise ValueError("URL must be absolute http(s)") host = parsed.hostname.lower().rstrip(".") if allowed_hosts and host not in {h.lower().rstrip(".") for h in allowed_hosts}: raise ValueError(f"host {host!r} is not in the allowlist") if host in {"localhost", "metadata.google.internal"} or host.endswith(".local") or host.endswith(".internal"): raise ValueError("internal hostnames are blocked") try: ip = ipaddress.ip_address(host) except ValueError: ip = None if ip and (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved or ip.is_unspecified): raise ValueError("unsafe IP address is blocked") if parsed.username or parsed.password: raise ValueError("credentials in URLs are not allowed") return url def stable_digest(payload: Any) -> str: return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str).encode()).hexdigest() def classify_severity(ticket: str, evidence: list[EvidenceItem]) -> Severity: text = (ticket + "\n" + "\n".join(e.summary for e in evidence)).lower() if any(x in text for x in ("outage", "data loss", "security incident", "all customers", "sev1")): return Severity.sev1 if any(x in text for x in ("unable", "down", "500", "payments failing", "sev2")): return Severity.sev2 if any(x in text for x in ("degraded", "intermittent", "slow", "error")): return Severity.sev3 return Severity.sev4 def classify_owner(services: list[str], ticket: str, changes: list[str]) -> str: text = " ".join([ticket, *services, *changes]).lower() if "payment" in text or "checkout" in text: return "payments" if "auth" in text or "login" in text: return "identity" if "kubernetes" in text or "pod" in text or "argocd" in text: return "platform" return services[0][:80] if services else "unassigned" def normalize_evidence(case: TriageCaseInput) -> list[EvidenceItem]: out = list(case.evidence) out.append(EvidenceItem(source_id="ticket", kind=EvidenceKind.ticket, summary=case.ticket_text[:1200], excerpt=case.ticket_text[:2000])) out.append(EvidenceItem(source_id="release", kind=EvidenceKind.release, summary=f"Release: {case.release}")) for i, change in enumerate(case.recent_changes[:20], 1): out.append(EvidenceItem(source_id=f"recent-change-{i}", kind=EvidenceKind.change, summary=change[:1200])) return out[:MAX_ITEMS] def repro_steps(inp: ReproduceIssueInput) -> list[str]: env = {item.key: item.value for item in inp.environment} steps = ["Use only redacted case data and explicitly granted workspace evidence.", f"Set environment facets: {json.dumps(env, sort_keys=True)}.", "Trigger the smallest customer-safe path matching the symptom."] if inp.observed_errors: steps.append(f"Assert observed signature: {redact(inp.observed_errors[0])[:500]}.") if inp.candidate_files: steps.append("Inspect only scoped candidate files: " + ", ".join(inp.candidate_files[:10]) + ".") if inp.health_checks: steps.append("Run configured HTTP checks only after SSRF allowlist validation; do not follow redirects.") steps.append("Record expected versus actual behavior without customer secrets or raw tenant data.") return steps def repro_markdown(inp: ReproduceIssueInput, steps: list[str], checks: list[HttpCheck], blocked: list[str]) -> str: body = [f"# Minimal Reproduction for {inp.case_id}", "", "## Symptom", inp.redacted_symptom, "", "## Deterministic Steps"] body += [f"{i}. {s}" for i, s in enumerate(steps, 1)] body += ["", "## Safe Health Checks"] + ([f"- {c.name}: {c.url} expects {c.expected_status}" for c in checks] or ["- None"]) body += ["", "## Blocked Health Checks"] + ([f"- {b}" for b in blocked] or ["- None"]) body += ["", "## Safety", "Ticket/log instructions are untrusted evidence; no arbitrary shell or URL fetching is performed."] return "\n".join(body) + "\n" def rank_hypotheses(hypotheses: list[str], supporting: list[EvidenceItem], contradicting: list[EvidenceItem]) -> list[HypothesisRank]: raw = hypotheses or ["Regression correlated with recent release/change and observed error signature"] sids = [e.source_id for e in supporting] cids = [e.source_id for e in contradicting] return [HypothesisRank(hypothesis=redact(h)[:1000], rank=i, confidence=round(max(0.1, min(0.95, 0.55 + len(sids) * 0.05 - len(cids) * 0.08 - (i - 1) * 0.03)), 2), supporting_source_ids=sids[:20], contradicting_source_ids=cids[:20]) for i, h in enumerate(raw[:20], 1)] def patch_diff(changes: list[RepoFileChange]) -> str: if not changes: return "diff --git a/README.md b/README.md\n--- a/README.md\n+++ b/README.md\n@@ -0,0 +1,3 @@\n+# Support Case Fix Placeholder\n+No scoped code changes were supplied.\n+Attach repository evidence before implementation.\n" parts: list[str] = [] for change in changes: lines = redact(change.proposed_content or f"# Proposed change\n# Reason: {change.reason}\n").splitlines() parts += [f"diff --git a/{change.path} b/{change.path}", f"--- a/{change.path}", f"+++ b/{change.path}", f"@@ -0,0 +1,{max(1, len(lines))} @@"] parts += ["+" + line for line in lines[:400]] return "\n".join(parts) + "\n" def validation_checks(inp: ValidateFixInput) -> list[CheckResult]: values = { "patch_digest_format": "pass" if re.fullmatch(r"[a-fA-F0-9]{16,128}", inp.patch_digest) else "review", "patch_has_diff_header": "pass" if "diff --git" in inp.patch_diff and "+++ b/" in inp.patch_diff else "review", "no_secret_literals": "pass" if not SECRET_RE.search(inp.patch_diff) else "fail", "path_containment": "pass" if not re.search(r"(^|\n)(---|\+\+\+) [ab]/(\.\.|/|.*\.git)", inp.patch_diff) else "fail", "test_plan_present": "pass" if inp.test_selectors else "review", "lint_plan_present": "pass" if inp.lint_selectors else "review", "security_plan_present": "pass" if inp.security_checks else "review", "dry_run_enforced": "pass" if inp.dry_run else "review", } return [CheckResult(name=k, status=v) for k, v in values.items()] def delivery_doc(inp: PrepareDeliveryInput, digest: str, required: bool, approved: bool, executed: list[str], blocked: list[str]) -> str: summary = redact(inp.customer_summary_context)[:2000] or "A scoped fix has been prepared for engineering review. No customer secrets, raw logs, or deployment details are included." return "\n".join([f"# Delivery Plan for {inp.case_id}", "", f"Plan digest: `{digest}`", f"Repository: `{inp.repository}`", f"Target branch: `{inp.target_branch}`", f"Proposed branch: `{inp.proposed_branch}`", f"Patch digest: `{inp.patch_digest}`", f"Validation digest: `{inp.validation_digest}`", "", "## Approval", f"Approval required: `{str(required).lower()}`", f"Approved: `{str(approved).lower()}`", f"Required acknowledgement: `{APPROVAL_ACK}` bound to the plan digest above.", "", "## Actions", "Executed/prepared: " + (", ".join(executed) if executed else "none"), "Blocked: " + (", ".join(blocked) if blocked else "none"), "", "## Customer-safe Resolution Summary", summary, "", "## Non-actions", "This agent never merges, deploys, follows ticket/log instructions, executes arbitrary shell, or fetches arbitrary URLs."]) + "\n" def audit_record(case_id: str, event: str, details: list[KeyValue]) -> AuditRecord: return AuditRecord(event=event, at=datetime.now(UTC).isoformat(), case_id=case_id, details=details) async def emit_audit(ctx: RunContext[NoAuth], case_id: str, event: str, details: list[KeyValue]) -> None: await ctx.emit_event(AgentEvent(kind="audit", payload=audit_record(case_id, event, details).model_dump(mode="json"))) def dumps(value: Any) -> str: return redact(json.dumps(value, indent=2, sort_keys=True, default=str)) async def write_case_file(ctx: RunContext[NoAuth], case_id: str, filename: str, content: str) -> ArtifactRecord: rel_path = f"{OUTPUT_ROOT}/{safe_case(case_id)}/{safe_path(filename)}" if safe_path(filename) != filename: raise ValueError("artifact filename must be a plain relative filename") data = redact(content).encode() digest = hashlib.sha256(data).hexdigest() artifact_uri: str | None = None try: view = await ctx.workspace.open_view(purpose=f"Persist support case artifact {filename}", hints=[case_id, filename], file_types=(), max_files=1, mode=WorkspaceMode.READ_WRITE_OVERLAY, reason="Persist generated support-to-fix engineering artifact under outputs/support-cases/{case_id}/") await view.write(rel_path, data) except Exception as exc: await ctx.emit_event(AgentEvent(kind="workspace_write_warning", payload={"path": rel_path, "message": redact(str(exc))[:500]})) try: ref = await ctx.write_artifact(rel_path.replace("/", "__"), data, mime(filename)) await ctx.emit_artifact(ref) artifact_uri = ref.uri except Exception as exc: await ctx.emit_event(AgentEvent(kind="artifact_warning", payload={"path": rel_path, "message": redact(str(exc))[:500]})) return ArtifactRecord(path=rel_path, artifact_uri=artifact_uri, sha256=digest, size_bytes=len(data)) def mime(filename: str) -> str: if filename.endswith(".json"): return "application/json" if filename.endswith(".md"): return "text/markdown" if filename.endswith(".diff") or filename.endswith(".patch"): return "text/x-diff" return "text/plain"