fix(agent99): deliver visual lifecycle cards

This commit is contained in:
ogt
2026-07-15 17:31:18 +08:00
parent b9ee3c5cbb
commit d471b3cfe8
9 changed files with 553 additions and 19 deletions

View File

@@ -2,10 +2,15 @@
from __future__ import annotations
import base64
import binascii
import hashlib
import secrets
import struct
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, model_validator
class Agent99TelegramReceipt(BaseModel):
@@ -75,6 +80,50 @@ class Agent99CompletionCallbackRequest(BaseModel):
occurred_at: datetime
class Agent99TelegramVisual(BaseModel):
"""Bounded Agent99-rendered PNG; never accepts paths or arbitrary media."""
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
schema_version: Literal["agent99_telegram_visual_v1"]
media_type: Literal["image/png"]
generated_by: Literal["agent99_system_drawing_v1"]
width: Literal[1200]
height: Literal[760]
sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
content_base64: str = Field(
min_length=32,
max_length=1_400_000,
pattern=r"^[A-Za-z0-9+/]+={0,2}$",
repr=False,
)
@model_validator(mode="after")
def validate_bounded_png(self) -> Agent99TelegramVisual:
try:
raw = base64.b64decode(self.content_base64, validate=True)
except (ValueError, binascii.Error) as exc:
raise ValueError("agent99_visual_base64_invalid") from exc
if len(raw) > 1024 * 1024:
raise ValueError("agent99_visual_png_exceeds_1_mib")
if (
len(raw) < 24
or raw[:8] != b"\x89PNG\r\n\x1a\n"
or raw[12:16] != b"IHDR"
):
raise ValueError("agent99_visual_png_header_invalid")
width, height = struct.unpack(">II", raw[16:24])
if width != self.width or height != self.height:
raise ValueError("agent99_visual_dimensions_invalid")
digest = hashlib.sha256(raw).hexdigest()
if not secrets.compare_digest(digest, self.sha256):
raise ValueError("agent99_visual_sha256_mismatch")
return self
def png_bytes(self) -> bytes:
return base64.b64decode(self.content_base64, validate=True)
class Agent99TelegramLifecycleRequest(BaseModel):
"""Bounded Agent99 event card handed to the canonical Telegram gateway."""
@@ -119,4 +168,5 @@ class Agent99TelegramLifecycleRequest(BaseModel):
occurrences: int = Field(default=1, ge=1, le=1_000_000)
next_update: str = Field(min_length=1, max_length=300)
reply_to_message_id: int | None = Field(default=None, ge=1)
visual: Agent99TelegramVisual | None = None
occurred_at: datetime