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:
parent
79b100caff
commit
3b6e4e4890
212
docs/A2H2A_TICKETING_SPEC.md
Normal file
212
docs/A2H2A_TICKETING_SPEC.md
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -229,3 +229,17 @@ En Cloud Build-pipeline feiler på det siste "deploy to Cloud Run"-steget. Feilm
|
||||||
### Status
|
### Status
|
||||||
|
|
||||||
🟡 **MODERAT TIL AVKLART.** Vanligvis en operasjonell feil forårsaket av en compliance-endring.
|
🟡 **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.**
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,57 @@ except Exception as e:
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional, Dict, List
|
||||||
import logging
|
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):
|
class ConfirmationRequired(BaseModel):
|
||||||
message: str
|
message: str
|
||||||
|
|
@ -97,6 +144,118 @@ async def sse_endpoint(request: Request):
|
||||||
"""Handles Server-Sent Events for external clients like Perplexity."""
|
"""Handles Server-Sent Events for external clients like Perplexity."""
|
||||||
return StreamingResponse(sse_event_stream(request), media_type="text/event-stream")
|
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 ───────────────────────────────────────────────────────
|
# ── Service discovery ───────────────────────────────────────────────────────
|
||||||
OSVAUCO_AGENT_URL = os.environ.get("OSVAUCO_AGENT_URL", "") # f.eks. https://osvauco-agent-....run.app
|
OSVAUCO_AGENT_URL = os.environ.get("OSVAUCO_AGENT_URL", "") # f.eks. https://osvauco-agent-....run.app
|
||||||
MCP_SECRET = os.environ.get("MCP_SECRET", "")
|
MCP_SECRET = os.environ.get("MCP_SECRET", "")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user