feat(agent-call): implement remediation plan

This commit is contained in:
2026-09-13 19:20:28 +08:00
parent d30314c7d1
commit b95d2aee81
25 changed files with 3159 additions and 535 deletions
+2023 -425
View File
File diff suppressed because it is too large Load Diff
+58 -14
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import hashlib
import json
import os
from http import HTTPStatus
@@ -15,6 +16,7 @@ READ_SCOPE = "outbound.read"
CONTROL_SCOPE = "outbound.control"
UPLOAD_SCOPE = "recording.upload"
COMPLETE_SCOPE = "recording.complete"
REPLAY_SCOPE = "outbound.replay"
class AgentCallHandler(BaseHTTPRequestHandler):
@@ -37,7 +39,7 @@ class AgentCallHandler(BaseHTTPRequestHandler):
raise ServiceError("TENANT_ID_REQUIRED", "X-Tenant-ID is required", 400)
return value
def _authorize(self, scope: str, tenant_id: str | None = None) -> None:
def _authorize(self, scope: str, tenant_id: str | None = None) -> dict[str, Any]:
token_header = self.headers.get("Authorization", "")
if not token_header.startswith("Bearer "):
raise ServiceError("UNAUTHORIZED", "bearer authentication is required", 401)
@@ -49,12 +51,24 @@ class AgentCallHandler(BaseHTTPRequestHandler):
if not isinstance(identity, dict):
raise ServiceError("UNAUTHORIZED", "bearer token is not authorized", 401)
scopes = identity.get("scopes", [])
tenant_ids = identity.get("tenant_ids", [])
if not isinstance(scopes, list) or not all(
isinstance(item, str) for item in scopes
):
raise ServiceError("UNAUTHORIZED", "token scopes are invalid", 401)
if not isinstance(tenant_ids, list) or not all(
isinstance(item, str) for item in tenant_ids
):
raise ServiceError("UNAUTHORIZED", "token tenant scope is invalid", 401)
if scope not in scopes and "*" not in scopes:
raise ServiceError("FORBIDDEN", "required scope is missing", 403)
if tenant_id is not None:
tenant_ids = identity.get("tenant_ids", [])
if "*" not in tenant_ids and tenant_id not in tenant_ids:
raise ServiceError("FORBIDDEN", "tenant scope is missing", 403)
if (
tenant_id is not None
and "*" not in tenant_ids
and tenant_id not in tenant_ids
):
raise ServiceError("FORBIDDEN", "tenant scope is missing", 403)
return identity
def _body(self) -> dict[str, Any]:
raw_length = self.headers.get("Content-Length")
@@ -69,6 +83,8 @@ class AgentCallHandler(BaseHTTPRequestHandler):
)
try:
raw = self.rfile.read(length)
if len(raw) != length:
raise ValidationError("BODY_TRUNCATED", "request body was truncated")
body = _loads(raw.decode("utf-8"))
except UnicodeDecodeError as exc:
raise ValidationError(
@@ -80,10 +96,21 @@ class AgentCallHandler(BaseHTTPRequestHandler):
def _header(self, name: str) -> str:
value = self.headers.get(name, "")
if not value:
if not value or len(value) > 128 or any(ch.isspace() for ch in value):
raise ServiceError("HEADER_REQUIRED", f"{name} is required", 400)
return value
def _actor_id(self) -> str:
token_header = self.headers.get("Authorization", "")
token = token_header[7:] if token_header.startswith("Bearer ") else ""
identity = _loads(os.environ.get("HTTP_TOKENS"), {})
principal = identity.get(token) if isinstance(identity, dict) else None
if isinstance(principal, dict):
value = principal.get("client_id") or principal.get("subject")
if isinstance(value, str) and value:
return value
return "token:" + hashlib.sha256(token.encode("utf-8")).hexdigest()[:16]
def _send(
self, status: int, body: Any, content_type: str = "application/json"
) -> None:
@@ -195,7 +222,7 @@ class AgentCallHandler(BaseHTTPRequestHandler):
and parts[5] == "replays"
):
tenant_id = self._tenant_id()
self._authorize(CONTROL_SCOPE, tenant_id)
self._authorize(REPLAY_SCOPE, tenant_id)
result = self.service.create_replay(
tenant_id,
"call",
@@ -213,7 +240,7 @@ class AgentCallHandler(BaseHTTPRequestHandler):
and parts[5] == "replays"
):
tenant_id = self._tenant_id()
self._authorize(CONTROL_SCOPE, tenant_id)
self._authorize(REPLAY_SCOPE, tenant_id)
result = self.service.create_replay(
tenant_id,
"command",
@@ -231,8 +258,9 @@ class AgentCallHandler(BaseHTTPRequestHandler):
):
tenant_id = self._tenant_id()
self._authorize(UPLOAD_SCOPE, tenant_id)
idempotency_key = self._header("Idempotency-Key")
result_status, result = self.service.create_upload(
tenant_id, self._body()
tenant_id, self._body(), idempotency_key, self._actor_id()
)
self._send(result_status, result)
return
@@ -245,7 +273,10 @@ class AgentCallHandler(BaseHTTPRequestHandler):
):
tenant_id = self._tenant_id()
self._authorize(COMPLETE_SCOPE, tenant_id)
result = self.service.complete_upload(tenant_id, parts[4], self._body())
idempotency_key = self._header("Idempotency-Key")
result = self.service.complete_upload(
tenant_id, parts[4], self._body(), idempotency_key, self._actor_id()
)
self._send(200, result)
return
if method == "PUT" and len(parts) == 3 and parts[:2] == ["_mock", "oss"]:
@@ -260,8 +291,8 @@ class AgentCallHandler(BaseHTTPRequestHandler):
if not raw_length:
raise ValidationError("BODY_REQUIRED", "upload body is required")
length = _integer(raw_length)
if length > self.service.profile.get("limits", {}).get(
"recording_max_bytes", 16777216
if length < 0 or length > self.service.profile.get("limits", {}).get(
"max_recording_bytes", 16777216
):
raise ServiceError(
"BODY_TOO_LARGE",
@@ -274,6 +305,10 @@ class AgentCallHandler(BaseHTTPRequestHandler):
raise ServiceError(
"UPLOAD_INTERRUPTED", "upload connection interrupted", 400
) from exc
if len(data) != length:
raise ServiceError(
"UPLOAD_INTERRUPTED", "upload body was truncated", 400
)
upload = self.service.store.one(
"SELECT tenant_id FROM uploads WHERE upload_id=?", (parts[2],)
)
@@ -287,7 +322,7 @@ class AgentCallHandler(BaseHTTPRequestHandler):
self._error(exc, request_id)
except (BrokenPipeError, ConnectionResetError):
return
except Exception:
except (OSError, RuntimeError, TypeError, ValueError, KeyError):
self._error(
ServiceError("INTERNAL_ERROR", "request could not be completed", 500),
request_id,
@@ -304,8 +339,17 @@ class AgentCallHandler(BaseHTTPRequestHandler):
if not isinstance(identity, dict):
return False
tenant_ids = identity.get("tenant_ids", [])
scopes = identity.get("scopes", [])
if not isinstance(tenant_ids, list) or not all(
isinstance(item, str) for item in tenant_ids
):
return False
if not isinstance(scopes, list) or not all(
isinstance(item, str) for item in scopes
):
return False
return ("*" in tenant_ids or tenant_id in tenant_ids) and (
scope in identity.get("scopes", []) or "*" in identity.get("scopes", [])
scope in scopes or "*" in scopes
)
def do_GET(self) -> None:
+40 -3
View File
@@ -6,6 +6,7 @@ ARI, RTP, AI-stream, or OSS integration.
from __future__ import annotations
import hashlib
import json
import threading
from pathlib import Path
@@ -82,9 +83,23 @@ class MockRTP:
def __init__(self, journal: ProtocolJournal) -> None:
self.journal = journal
def packet(self, call_id: str, direction: str, sequence: int) -> None:
def packet(
self,
call_id: str,
direction: str,
sequence: int,
payload: bytes | None = None,
) -> None:
payload = payload if payload is not None else b"\x01\x02" * 160
self.journal.append(
"rtp", "packet", call_id=call_id, direction=direction, sequence=sequence
"rtp",
"packet",
call_id=call_id,
direction=direction,
sequence=sequence,
payload_bytes=len(payload),
payload_sha256=hashlib.sha256(payload).hexdigest(),
non_silent=any(payload),
)
@@ -93,7 +108,12 @@ class MockAI:
self.journal = journal
def turn(
self, call_id: str, turn_index: int, speaker: str, playback_state: str
self,
call_id: str,
turn_index: int,
speaker: str,
playback_state: str,
text: str | None = None,
) -> None:
self.journal.append(
"ai",
@@ -102,4 +122,21 @@ class MockAI:
turn_index=turn_index,
speaker=speaker,
playback_state=playback_state,
text=text or f"mock-{speaker}-turn-{turn_index}",
)
self.journal.append(
"ai",
"playback",
call_id=call_id,
turn_index=turn_index,
state="sent",
)
self.journal.append(
"ai",
"playback",
call_id=call_id,
turn_index=turn_index,
state=playback_state
if playback_state in {"confirmed", "playback_confirmed", "unknown"}
else "unknown",
)