feat(a2h2a): add ticket dry-run previews
This commit is contained in:
parent
0d2f1f3b17
commit
8548d41e22
|
|
@ -232,9 +232,10 @@ class A2H2ATicket(BaseModel):
|
|||
context: Context
|
||||
severity: str
|
||||
governance: Governance
|
||||
dry_run: bool = False
|
||||
|
||||
def create_a2h2a_ticket(ticket: A2H2ATicket) -> dict:
|
||||
"""Create a new A2H2A ticket for a sensitive operation."""
|
||||
"""Create a new A2H2A ticket for a sensitive operation. If dry_run is True, a preview is returned and no ticket is created."""
|
||||
return _call_tool("create_a2h2a_ticket", ticket.dict())
|
||||
|
||||
|
||||
|
|
@ -296,6 +297,12 @@ _INSTRUCTION_TEMPLATE = (
|
|||
"Når brukaren ber om data — KALL ALLTID rett tool før du svarar. "
|
||||
"Svar på norsk (bokmål) med mindre annet er bedt om. "
|
||||
"HITL: ikkje kjør terraform/deploy utan godkjenning frå Chris."
|
||||
"### A2H2A dry-run safety rule\n"
|
||||
"If a user requests a dry run, preview, draft, utkast, \"ikke opprett sak\", \"ikke lagre\", \"do not create\", or \"do not save\":\n"
|
||||
"- call create_a2h2a_ticket with dry_run=true;\n"
|
||||
"- return the preview only;\n"
|
||||
"- never claim a ticket was created.\n"
|
||||
"Set dry_run=false only when the user explicitly asks to create a real ticket."
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ class A2H2ATicket(BaseModel):
|
|||
context: Context
|
||||
severity: str
|
||||
governance: Governance
|
||||
dry_run: bool = False
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# A2H2A (Human-in-the-Loop)
|
||||
|
|
|
|||
49
agents/core-logic/test_agent.py
Normal file
49
agents/core-logic/test_agent.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
from agent import (
|
||||
A2H2ATicket,
|
||||
Context,
|
||||
Governance,
|
||||
ProposedAction,
|
||||
TicketSource,
|
||||
create_a2h2a_ticket,
|
||||
)
|
||||
|
||||
|
||||
@patch("agent._call_tool")
|
||||
def test_create_a2h2a_ticket_forwards_dry_run(mock_call_tool):
|
||||
dry_run_ticket = A2H2ATicket(
|
||||
dry_run=True,
|
||||
source=TicketSource(
|
||||
reporter="test",
|
||||
trigger="test",
|
||||
affected_service="test",
|
||||
project_id="test",
|
||||
),
|
||||
proposed_action=ProposedAction(
|
||||
execution_tool="test",
|
||||
tool_parameters={},
|
||||
),
|
||||
context=Context(
|
||||
summary="test",
|
||||
user_justification="test",
|
||||
conversation_history=[],
|
||||
),
|
||||
severity="LOW",
|
||||
governance=Governance(
|
||||
approval_status="PENDING",
|
||||
authorized_approver="test",
|
||||
requires_mfa=True,
|
||||
timeout_minutes=15,
|
||||
),
|
||||
)
|
||||
|
||||
create_a2h2a_ticket(dry_run_ticket)
|
||||
|
||||
mock_call_tool.assert_called_once()
|
||||
tool_name, payload = mock_call_tool.call_args.args
|
||||
|
||||
assert tool_name == "create_a2h2a_ticket"
|
||||
assert payload["dry_run"] is True
|
||||
assert payload["source"]["reporter"] == "test"
|
||||
assert payload["proposed_action"]["execution_tool"] == "test"
|
||||
|
|
@ -372,7 +372,22 @@ async def list_open_issues(p): return await _gitea_get(f"/repos/{p.g
|
|||
async def create_issue(p): return await _gitea_post(f"/repos/{p.get('repo', GITEA_REPO)}/issues", {"title": p.get("title"), "body": p.get("body", "")})
|
||||
|
||||
async def create_a2h2a_ticket(p: dict) -> dict:
|
||||
"""Creates an A2H2A governance ticket."""
|
||||
preview = {
|
||||
"source": p.get("source"),
|
||||
"proposed_action": p.get("proposed_action"),
|
||||
"context": p.get("context"),
|
||||
"severity": p.get("severity"),
|
||||
"governance": p.get("governance"),
|
||||
"dry_run": bool(p.get("dry_run")),
|
||||
}
|
||||
|
||||
if preview["dry_run"]:
|
||||
return {
|
||||
"dry_run": True,
|
||||
"message": "Ticket preview only; no ticket was created.",
|
||||
"proposed_ticket": preview,
|
||||
}
|
||||
|
||||
from google.cloud import firestore
|
||||
import uuid, hashlib, hmac, os
|
||||
|
||||
|
|
|
|||
57
opax-mcp/test_dry_run.py
Normal file
57
opax-mcp/test_dry_run.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Mock the firestore client before it's imported by the server
|
||||
@patch("google.cloud.firestore.Client")
|
||||
def test_create_a2h2a_ticket_dry_run(mock_firestore_client):
|
||||
from server import create_a2h2a_ticket
|
||||
import asyncio
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_firestore_client.return_value = mock_db
|
||||
|
||||
dry_run_payload = {
|
||||
"dry_run": True,
|
||||
"source": {"reporter": "test", "trigger": "test", "affected_service": "test", "project_id": "test"},
|
||||
"proposed_action": {"execution_tool": "test", "tool_parameters": {}},
|
||||
"context": {"summary": "test", "user_justification": "test", "conversation_history": []},
|
||||
"severity": "LOW",
|
||||
"governance": {"approval_status": "PENDING", "authorized_approver": "test", "requires_mfa": True, "timeout_minutes": 15}
|
||||
}
|
||||
|
||||
result = asyncio.run(create_a2h2a_ticket(dry_run_payload))
|
||||
|
||||
assert result["dry_run"] is True
|
||||
assert result["message"] == "Ticket preview only; no ticket was created."
|
||||
assert "proposed_ticket" in result
|
||||
mock_db.collection.assert_not_called()
|
||||
|
||||
@patch("google.cloud.firestore.Client")
|
||||
def test_create_a2h2a_ticket_live_run(mock_firestore_client):
|
||||
from server import create_a2h2a_ticket
|
||||
import asyncio
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_firestore_client.return_value = mock_db
|
||||
mock_collection = MagicMock()
|
||||
mock_db.collection.return_value = mock_collection
|
||||
mock_document = MagicMock()
|
||||
mock_collection.document.return_value = mock_document
|
||||
|
||||
live_run_payload = {
|
||||
"dry_run": False,
|
||||
"source": {"reporter": "test", "trigger": "test", "affected_service": "test", "project_id": "test"},
|
||||
"proposed_action": {"execution_tool": "test", "tool_parameters": {}},
|
||||
"context": {"summary": "test", "user_justification": "test", "conversation_history": []},
|
||||
"severity": "LOW",
|
||||
"governance": {"approval_status": "PENDING", "authorized_approver": "test", "requires_mfa": True, "timeout_minutes": 15}
|
||||
}
|
||||
|
||||
result = asyncio.run(create_a2h2a_ticket(live_run_payload))
|
||||
|
||||
assert result["status"] == "created"
|
||||
assert "ticket_id" in result
|
||||
assert "review_url" in result
|
||||
mock_db.collection.assert_called_once_with("a2h2a_tickets")
|
||||
mock_collection.document.assert_called_once()
|
||||
mock_document.set.assert_called_once()
|
||||
Loading…
Reference in New Issue
Block a user