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
|
context: Context
|
||||||
severity: str
|
severity: str
|
||||||
governance: Governance
|
governance: Governance
|
||||||
|
dry_run: bool = False
|
||||||
|
|
||||||
def create_a2h2a_ticket(ticket: A2H2ATicket) -> dict:
|
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())
|
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. "
|
"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. "
|
"Svar på norsk (bokmål) med mindre annet er bedt om. "
|
||||||
"HITL: ikkje kjør terraform/deploy utan godkjenning frå Chris."
|
"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
|
context: Context
|
||||||
severity: str
|
severity: str
|
||||||
governance: Governance
|
governance: Governance
|
||||||
|
dry_run: bool = False
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# A2H2A (Human-in-the-Loop)
|
# 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_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:
|
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
|
from google.cloud import firestore
|
||||||
import uuid, hashlib, hmac, os
|
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