docs/code: clarify A2H2A prototype status and disable execution

- Add Implementation Status section to A2H2A spec

- Clarify Google Chat is notification-only

- Add target approval architecture requirements

- Add critical A2H2A safety principle to RUNBOOK.md

- Remove unsafe approval/rejection endpoints from server.py

- Enforce server-side parameter_hash calculation

- Disable execution for all tools in A2H2A_TOOL_ALLOW_LIST
This commit is contained in:
Chris Christiansen 2026-09-04 12:00:07 +00:00
parent 79b100caff
commit 3b6e4e4890
3 changed files with 387 additions and 2 deletions

View File

@ -0,0 +1,212 @@
# OSVxCC A2H2A Ticketing System Specification
## 1. Introduksjon
A2H2A (Agent-to-Human-to-Agent) er en uomgåelig Approval Gate mellom autonome observatører/analytikere og operative eksekveringsagenter. Ingen destruktive eller privilegerte handlinger skal utføres i GCP-infrastrukturen uten at autorisert operatør eksplisitt har validert og godkjent handlingen via et kryptografisk signert grensesnitt.
## Implementation Status
**Current prototype — proposal recording only; approval and execution disabled.**
- The prototype may accept an allow-listed ticket proposal and store it as `PENDING`.
- Google Chat is notification-only. It does not verify or prove the identity of an approver.
- The current `/a2h2a/review/{ticket_id}` link is conceptual only.
- No working approval or rejection endpoint currently exists.
- No one-time action token currently exists.
- No execution worker currently exists.
- No proposed tool is executable through A2H2A at this stage.
## 2. JSON Datamodell (a2h2a-ticket-v1.json)
```json
{
"ticket_id": "A2H2A-20260904-8B3D",
"timestamp": "2026-09-04T14:20:05Z",
"severity": "CRITICAL",
"category": "SECURITY",
"source": {
"reporter": "tyr-compliance-agent",
"trigger": "Secret scanned in public repository",
"affected_service": "mcp-server-key",
"project_id": "propane-will-491900-m5",
"region": "global"
},
"context": {
"summary": "En aktiv Secret Manager-nøkkel ble funnet i en offentlig Gitea-commit.",
"evidence_logs": [
"gitea.commit.url: https://git.vauco.no/chris/OSVauco/commit/a1b2c3d4...",
"secret.name: projects/357036551735/secrets/mcp-server-key/versions/2"
]
},
"proposed_action": {
"action_type": "AUTOMATED_REMEDIATION",
"runbook_reference": "docs/RUNBOOK.md#hendelse-eksponert-hemmelighet",
"execution_tool": "opax.secrets.rotate",
"parameters": {
"secret_name": "mcp-server-key",
"disable_old_versions": true,
"notify_channel": "#security-alerts"
},
"parameter_hash": "sha256:f4a2...d8e0",
"rollback_plan": "Manuell re-aktivering av forrige hemmelighetsversjon via `gcloud secrets versions enable` etter verifisering."
},
"governance": {
"approval_status": "PENDING",
"authorized_approver": "chris.christiansen@vauco.no",
"requires_mfa": true,
"timeout_minutes": 15
}
}
```
`The ticket model is a proposal model. Fields required for secure human approval—such as approval_token_hash, verified_approver_identity, approval timestamp, token_used_at, and immutable audit-event references—are target-design fields and are not implemented in the current prototype.`
## 3. Sikkerhetsgarantier
### 3.1 Constant-Time Authentication
- Sammenligning av alle signaturer og tokens **skal** utføres med `secrets.compare_digest` eller tilsvarende for å forhindre timing-angrep.
### 3.2 Anti-Tampering
- En SHA-256 hash av `proposed_action.parameters` **skal** beregnes før ticketen sendes til operatør.
- Før eksekvering **skal** OPAX verifisere at hashen av parameterne den mottok for eksekvering stemmer overens med `parameter_hash` i den godkjente ticketen.
### 3.3 Replay-Beskyttelse
- Hver callback fra Slack/Discord **skal** inneholde et timestamp.
- Serveren **skal** avvise callbacks hvor `current_time - callback_timestamp > 300` sekunder for å forhindre gjenbruk av gamle godkjenninger.
### 3.4 Time-To-Live (TTL)
- Ubehandlede tickets utløper automatisk basert på alvorlighetsgrad:
- **CRITICAL:** 15 minutter
- **HIGH:** 60 minutter
- **MEDIUM/LOW:** 24 timer
## 4. API-Endepunkter (Prototype)
### 4.1 POST /api/v1/a2h2a/tickets
1. Motta ticket-payload fra en autorisert intern agent (f.eks. TYR).
2. Valider payloaden mot en Pydantic-modell basert på schemaet over.
3. Beregn `parameter_hash` server-side.
4. Lagre ticketen i en Firestore-collection (`a2h2a_tickets`) med status `PENDING`.
5. Formater og send en Google Chat-melding med et konseptuelt link til en fremtidig godkjennings-UI.
`Google Chat delivers a notification and review link only; it must never be used as evidence of the approver's identity or as the authority to execute a tool.`
## Target Approval Architecture — Required Before Production
The eventual review URL may use:
`/a2h2a/review/{ticket_id}?token=<opaque-random-token>`
- The token is generated with a cryptographically secure random generator.
- The raw token is returned once in the notification link and is never persisted.
- Only the SHA-256 token hash is stored server-side.
- Approval/rejection backend verifies IAP-verified identity, PENDING state, TTL, unused matching token hash, authorized approver, server-side canonical parameter hash, and tool allow-list.
- Approval only updates the ticket state.
- A separate worker later executes only server-stored, immutable, approved actions.
- Every audit event is a target requirement; do not claim it is already implemented.
## Target audit requirements
`Immutable audit events for proposal, approval, rejection, expiry, execution start, execution success, execution failure, and rollback are required before production activation; they are not implemented in the current prototype.`
## 5. Google Chat / Slack Block Kit Payload (Konseptuelt Eksempel)
```json
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🚨 A2H2A Godkjenning Kreves: CRITICAL"
}
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": "*Kategori:*
SECURITY"},
{"type": "mrkdwn", "text": "*Tjeneste:*
mcp-server-key"},
{"type": "mrkdwn", "text": "*Handling:*
`opax.secrets.rotate`"},
{"type": "mrkdwn", "text": "*Tidsfrist:*
15 minutter"}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Sammendrag:*
En aktiv Secret Manager-nøkkel ble funnet i en offentlig Gitea-commit."
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "✅ Godkjenn",
"emoji": true
},
"style": "primary",
"value": "approve_A2H2A-20260904-8B3D"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "❌ Avvis",
"emoji": true
},
"style": "danger",
"value": "reject_A2H2A-20260904-8B3D"
}
]
}
]
}
```
## 6. Eksempel-Ticket (SECRET_EXPOSURE)
```json
{
"ticket_id": "A2H2A-20260904-1C4F",
"timestamp": "2026-09-04T18:05:10Z",
"severity": "CRITICAL",
"category": "SECURITY",
"source": {
"reporter": "tyr-scanner-agent",
"trigger": "Audit log anomaly: Unexpected access to mcp-server-key v2",
"affected_service": "mcp-server-key",
"project_id": "propane-will-491900-m5",
"region": "global"
},
"context": {
"summary": "Mistenkelig tilgang til mcp-server-key v2 fra en ukjent IP-adresse (35.22.88.101) utenfor GCP-nettverket.",
"evidence_logs": [
"log_explorer_link: https://console.cloud.google.com/logs/query;query=..."
]
},
"proposed_action": {
"action_type": "AUTOMATED_REMEDIATION",
"runbook_reference": "docs/RUNBOOK.md#hendelse-eksponert-hemmelighet",
"execution_tool": "opax.secrets.rotate",
"parameters": {
"secret_name": "mcp-server-key",
"disable_old_versions": true,
"notify_channel": "#security-alerts"
},
"parameter_hash": "sha256:f4a26901a511e6cb716b5a86552631521a0e71383921034293739886313ad8e0",
"rollback_plan": "Manuell re-aktivering av forrige hemmelighetsversjon (v3) via `gcloud secrets versions enable` etter verifisering."
},
"governance": {
"approval_status": "PENDING",
"authorized_approver": "chris.christiansen@vauco.no",
"requires_mfa": true,
"timeout_minutes": 15
}
}
```

View File

@ -229,3 +229,17 @@ En Cloud Build-pipeline feiler på det siste "deploy to Cloud Run"-steget. Feilm
### Status
🟡 **MODERAT TIL AVKLART.** Vanligvis en operasjonell feil forårsaket av en compliance-endring.
---
## 7. Sikkerhet og Hendelseshåndtering
Se følgende dokumenter for detaljerte prosedyrer:
- `docs/SECURITY.md`: Overordnet sikkerhetsarkitektur.
- `docs/INCIDENT_RESPONSE.md`: Prosedyrer for håndtering av sikkerhetshendelser.
- `docs/SECRET_MANAGEMENT.md`: Retningslinjer for håndtering av secrets.
### Kritisk Sikkerhetsprinsipp for A2H2A
**No privileged action may be authorized by a notification, a URL parameter, or a client-supplied identity. Approval requires a verified identity boundary and server-side validation of an immutable ticket.**

View File

@ -48,10 +48,57 @@ except Exception as e:
from email.mime.text import MIMEText
from email.mime.text import MIMEText
from datetime import datetime, timezone, timedelta
from typing import Any, Optional
from typing import Any, Optional, Dict, List
import logging
from pydantic import BaseModel
from pydantic import BaseModel, Field
import hashlib
# --- A2H2A Pydantic Models ---
class TicketSource(BaseModel):
reporter: str
trigger: str
affected_service: str
project_id: str
region: str
class TicketContext(BaseModel):
summary: str
evidence_logs: List[str]
class ProposedAction(BaseModel):
action_type: str
runbook_reference: str
execution_tool: str
parameters: Dict
parameter_hash: Optional[str] = None
rollback_plan: str
class Governance(BaseModel):
approval_status: str = "PENDING"
authorized_approver: str
requires_mfa: bool = True
timeout_minutes: int = 15
class A2H2ATicket(BaseModel):
ticket_id: str
timestamp: datetime
severity: str # CRITICAL, HIGH, MEDIUM, LOW
category: str
source: TicketSource
context: TicketContext
proposed_action: ProposedAction
governance: Governance
def calculate_parameter_hash(self) -> str:
"""Beregn SHA-256 hash av parameters i kanonisk form."""
param_json = json.dumps(self.proposed_action.parameters, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(param_json.encode()).hexdigest()
def is_expired(self) -> bool:
"""Sjekk om ticket er utløpt"""
from datetime import datetime, timedelta
expiry = self.timestamp + timedelta(minutes=self.governance.timeout_minutes)
return datetime.utcnow() > expiry.replace(tzinfo=None)
class ConfirmationRequired(BaseModel):
message: str
@ -97,6 +144,118 @@ async def sse_endpoint(request: Request):
"""Handles Server-Sent Events for external clients like Perplexity."""
return StreamingResponse(sse_event_stream(request), media_type="text/event-stream")
# --- A2H2A API Endpoints ---
# --- A2H2A Secure Design Placeholder ---
#
# The following logic is a placeholder for a secure A2H2A (Agent-to-Human-to-Agent)
# approval workflow. The key principles are:
#
# 1. NOTIFICATION, NOT ACTION: Google Chat messages are for notification only. They
# do not carry any identity or authorization guarantee.
# 2. VERIFIED IDENTITY: Human approval must happen through a protected surface that
# verifies the user's identity (e.g., a web app protected by Google IAP).
# 3. ATOMIC, IMMUTABLE ACTIONS: The ticket stored in the backend (Firestore) is the
# source of truth. The proposed action, its parameters, and the server-generated
# parameter hash are immutable.
# 4. ONE-TIME USE TOKENS: The approval UI should be accessed via a link containing a
# cryptographically random, single-use token that is invalidated after the first
# approval or rejection, or after the ticket's TTL expires.
# 5. DECOUPLED EXECUTION: The approval action only changes the ticket's state to
# 'APPROVED'. A separate, secure worker process will poll for approved tickets and
# execute them, ensuring a clean separation of concerns. This worker validates the
# ticket's integrity (non-expired, parameter hash match) before execution.
#
# This implementation only covers the safe recording of a proposed ticket.
# Execution is explicitly disabled.
# A strict allow-list of tools that can be proposed in an A2H2A ticket.
# `execution_enabled: False` means the tool can be proposed but not executed.
A2H2A_TOOL_ALLOW_LIST = {
"example_tool_1": {"execution_enabled": False, "description": "A sample tool that does something harmless."},
"example_tool_2": {"execution_enabled": False, "description": "Another sample tool."},
# Add real, vetted tools here with execution_enabled: True ONLY after extensive security review.
}
@app.post("/api/v1/a2h2a/tickets")
async def create_a2h2a_ticket(ticket: A2H2ATicket, request: Request):
"""
Opprett ny A2H2A ticket og send til Google Chat for godkjenning.
Dette er kun en prototype for sikker registrering, ingen verktøy blir kjørt.
"""
try:
await _verify_auth(request)
# Security check: Reject if caller provides a parameter_hash.
if ticket.proposed_action.parameter_hash is not None:
raise HTTPException(status_code=400, detail="Do not provide a parameter_hash. The server will generate it.")
# Security check: Ensure the proposed tool is in the allow-list.
tool_name = ticket.proposed_action.execution_tool
if tool_name not in A2H2A_TOOL_ALLOW_LIST:
raise HTTPException(status_code=400, detail=f"Tool '{tool_name}' is not on the approved A2H2A allow-list.")
# Generate parameter hash server-side for integrity.
ticket.proposed_action.parameter_hash = ticket.calculate_parameter_hash()
from google.cloud import firestore
db = firestore.Client(project=GOOGLE_CLOUD_PROJECT)
ticket_data = ticket.model_dump(mode='json')
ticket_data['created_at'] = datetime.utcnow().isoformat()
ticket_data['status'] = 'PENDING'
doc_ref = db.collection('a2h2a_tickets').document(ticket.ticket_id)
doc_ref.set(ticket_data)
# The approval URL now points to a conceptual, IAP-protected UI.
approval_ui_url = f"https://opax.vauco.no/a2h2a/review/{ticket.ticket_id}"
google_chat_webhook = os.getenv('A2H2A_GOOGLE_CHAT_WEBHOOK')
if google_chat_webhook:
chat_payload = {
"cards": [
{
"header": {
"title": f"🚨 A2H2A Proposal: {ticket.severity}",
"subtitle": "OSVxCC Security (Prototype - No Execution)",
"image": {"imageUrl": "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png"}
},
"sections": [
{
"widgets": [
{
"textParagraph": {
"text": f"<b>Category:</b> {ticket.category}<br>"
f"<b>Service:</b> {ticket.source.affected_service}<br>"
f"<b>Tool:</b> {ticket.proposed_action.execution_tool}<br>"
f"<b>Summary:</b> {ticket.context.summary}"
}
},
{
"buttons": [
{
"textButton": {
"text": "REVIEW IN APPROVAL UI",
"onClick": {"openLink": {"url": approval_ui_url}}
}
}
]
}
]
}
]
}
]
}
import httpx
async with httpx.AsyncClient() as client:
await client.post(google_chat_webhook, json=chat_payload)
return {"status": "success", "ticket_id": ticket.ticket_id, "parameter_hash": ticket.proposed_action.parameter_hash}
except Exception as e:
logger.error(f"Feil ved opprettelse av A2H2A ticket: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ── Service discovery ───────────────────────────────────────────────────────
OSVAUCO_AGENT_URL = os.environ.get("OSVAUCO_AGENT_URL", "") # f.eks. https://osvauco-agent-....run.app
MCP_SECRET = os.environ.get("MCP_SECRET", "")