feat(agents): expose controlled executor handoff runway
Some checks failed
Code Review / ai-code-review (push) Successful in 22s
CD Pipeline / tests (push) Successful in 1m47s
CD Pipeline / build-and-deploy (push) Successful in 6m20s
CD Pipeline / post-deploy-checks (push) Successful in 2m18s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
Some checks failed
Code Review / ai-code-review (push) Successful in 22s
CD Pipeline / tests (push) Successful in 1m47s
CD Pipeline / build-and-deploy (push) Successful in 6m20s
CD Pipeline / post-deploy-checks (push) Successful in 2m18s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
This commit is contained in:
@@ -76,6 +76,9 @@ from src.services.ai_agent_canonical_runtime_readback_owner_acceptance import (
|
||||
from src.services.ai_agent_communication_learning_contract import (
|
||||
load_latest_ai_agent_communication_learning_contract,
|
||||
)
|
||||
from src.services.ai_agent_controlled_executor_handoff import (
|
||||
load_latest_ai_agent_controlled_executor_handoff,
|
||||
)
|
||||
from src.services.ai_agent_critic_reviewer_result_capture import (
|
||||
load_latest_ai_agent_critic_reviewer_result_capture,
|
||||
)
|
||||
@@ -1143,6 +1146,37 @@ async def get_agent_high_risk_owner_review_queue() -> dict[str, Any]:
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agent-controlled-executor-handoff",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 P2-415 AI Agent 受控 Executor 交接跑道",
|
||||
description=(
|
||||
"讀取最新已提交的 P2-415 AI Agent controlled executor handoff 只讀快照;"
|
||||
"此端點呈現 high risk packet 是否具備 allowlist、Ansible check-mode、rollback、"
|
||||
"post-action verifier、Telegram evidence、KM / PlayBook trust writeback 條件,"
|
||||
"以及 critical break-glass 邊界。它不 dispatch executor、不執行 live apply、"
|
||||
"不寫 Gateway queue、不送 Telegram、不呼叫 Bot API、不寫 KM、不更新 PlayBook trust、"
|
||||
"不寫 production、不讀 secret、不呼叫付費 API、不改主機、不執行 kubectl 或不可逆操作。"
|
||||
),
|
||||
)
|
||||
async def get_agent_controlled_executor_handoff() -> dict[str, Any]:
|
||||
"""回傳最新 P2-415 controlled executor handoff 只讀快照。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(load_latest_ai_agent_controlled_executor_handoff)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
logger.error("ai_agent_controlled_executor_handoff_invalid", error=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="P2-415 AI Agent 受控 Executor 交接跑道快照無效",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agent-action-audit-ledger",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
406
apps/api/src/services/ai_agent_controlled_executor_handoff.py
Normal file
406
apps/api/src/services/ai_agent_controlled_executor_handoff.py
Normal file
@@ -0,0 +1,406 @@
|
||||
"""
|
||||
P2-415 AI Agent controlled executor handoff readback.
|
||||
|
||||
This loader validates the committed controlled executor handoff runway. It makes
|
||||
high-risk controlled apply packets visible to the product, while keeping the
|
||||
route itself read-only: no live apply, Telegram send, Bot API, secret read,
|
||||
host write, kubectl action, or destructive operation is executed here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from src.services.snapshot_paths import default_evaluations_dir
|
||||
|
||||
_DEFAULT_EVALUATIONS_DIR = default_evaluations_dir(Path(__file__))
|
||||
_SNAPSHOT_PATTERN = "ai_agent_controlled_executor_handoff_*.json"
|
||||
_SCHEMA_VERSION = "ai_agent_controlled_executor_handoff_v1"
|
||||
_RUNTIME_AUTHORITY = "controlled_executor_handoff_readback_no_live_apply"
|
||||
_EXPECTED_CURRENT_TASK = "P2-415"
|
||||
_EXPECTED_NEXT_TASK = "P2-416"
|
||||
_EXPECTED_SOURCE_SCHEMAS = {
|
||||
"ai_agent_high_risk_owner_review_queue_v1",
|
||||
"ai_agent_action_audit_ledger_v1",
|
||||
"ai_agent_action_owner_acceptance_event_bus_v1",
|
||||
"ai_agent_report_runtime_readiness_v1",
|
||||
"ai_agent_runtime_write_gate_review_v1",
|
||||
"ai_agent_post_write_verifier_package_v1",
|
||||
"ai_agent_learning_writeback_approval_package_v1",
|
||||
"ai_agent_telegram_receipt_approval_package_v1",
|
||||
}
|
||||
_TRUE_TRUTH_FLAGS = {
|
||||
"p2_409_controlled_apply_queue_loaded",
|
||||
"p2_410_audit_ledger_loaded",
|
||||
"p2_411_handoff_event_bus_loaded",
|
||||
"runtime_readiness_loaded",
|
||||
"runtime_write_gate_loaded",
|
||||
"post_write_verifier_loaded",
|
||||
"learning_writeback_loaded",
|
||||
"telegram_receipt_loaded",
|
||||
"high_risk_controlled_executor_handoff_ready",
|
||||
"critical_break_glass_required",
|
||||
"allowlist_route_required",
|
||||
"ansible_check_mode_required",
|
||||
"rollback_plan_required",
|
||||
"post_action_verifier_required",
|
||||
"telegram_evidence_required",
|
||||
"km_writeback_required",
|
||||
"playbook_trust_writeback_required",
|
||||
}
|
||||
_FALSE_TRUTH_FLAGS = {
|
||||
"high_risk_owner_review_required",
|
||||
"controlled_executor_dispatch_enabled",
|
||||
"live_apply_enabled",
|
||||
"critical_auto_bypass_allowed",
|
||||
"gateway_queue_write_enabled",
|
||||
"telegram_send_enabled",
|
||||
"bot_api_call_enabled",
|
||||
"km_write_enabled",
|
||||
"playbook_trust_write_enabled",
|
||||
"production_write_enabled",
|
||||
"secret_read_enabled",
|
||||
"paid_api_call_enabled",
|
||||
"host_write_enabled",
|
||||
"kubectl_action_enabled",
|
||||
"destructive_operation_enabled",
|
||||
}
|
||||
_ZERO_TRUTH_COUNTS = {
|
||||
"controlled_executor_dispatch_count_24h",
|
||||
"live_apply_count_24h",
|
||||
"gateway_queue_write_count_24h",
|
||||
"telegram_send_count_24h",
|
||||
"bot_api_call_count_24h",
|
||||
"km_write_count_24h",
|
||||
"playbook_trust_write_count_24h",
|
||||
"production_write_count_24h",
|
||||
"secret_read_count_24h",
|
||||
"paid_api_call_count_24h",
|
||||
"host_write_count_24h",
|
||||
"kubectl_action_count_24h",
|
||||
"destructive_operation_count_24h",
|
||||
}
|
||||
_TRUE_BOUNDARY_FLAGS = {
|
||||
"committed_snapshot_read_allowed",
|
||||
"controlled_executor_handoff_preview_allowed",
|
||||
"ansible_check_mode_receipt_preview_allowed",
|
||||
"mcp_tool_registry_route_preview_allowed",
|
||||
"post_action_verifier_binding_preview_allowed",
|
||||
"telegram_evidence_preview_allowed",
|
||||
"km_playbook_trust_writeback_preview_allowed",
|
||||
}
|
||||
_FALSE_BOUNDARY_FLAGS = {
|
||||
"controlled_executor_dispatch_enabled",
|
||||
"live_apply_enabled",
|
||||
"gateway_queue_write_enabled",
|
||||
"telegram_send_enabled",
|
||||
"bot_api_call_enabled",
|
||||
"km_write_enabled",
|
||||
"playbook_trust_write_enabled",
|
||||
"production_write_enabled",
|
||||
"secret_read_enabled",
|
||||
"paid_api_call_enabled",
|
||||
"host_write_enabled",
|
||||
"kubectl_action_enabled",
|
||||
"destructive_operation_enabled",
|
||||
}
|
||||
_ZERO_ROLLUP_FIELDS = {
|
||||
"controlled_executor_dispatch_count",
|
||||
"live_apply_count",
|
||||
"gateway_queue_write_count",
|
||||
"telegram_send_count",
|
||||
"bot_api_call_count",
|
||||
"km_write_count",
|
||||
"playbook_trust_write_count",
|
||||
"production_write_count",
|
||||
"secret_read_count",
|
||||
"paid_api_call_count",
|
||||
"host_write_count",
|
||||
"kubectl_action_count",
|
||||
"destructive_operation_count",
|
||||
}
|
||||
_FORBIDDEN_PUBLIC_TERMS = {
|
||||
"批准" + "!",
|
||||
"In app " + "browser",
|
||||
"My request for " + "Codex",
|
||||
"codex_" + "delegation",
|
||||
"source_" + "thread_id",
|
||||
"chain_of_thought",
|
||||
"private reasoning text",
|
||||
"authorization_header",
|
||||
"telegram token value",
|
||||
"raw_payload",
|
||||
"raw prompt",
|
||||
"internal collaboration transcript",
|
||||
"工作視窗",
|
||||
"對話內容",
|
||||
}
|
||||
|
||||
|
||||
def load_latest_ai_agent_controlled_executor_handoff(
|
||||
evaluations_dir: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Load the newest committed P2-415 controlled executor handoff snapshot."""
|
||||
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
|
||||
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
|
||||
if not candidates:
|
||||
raise FileNotFoundError(f"no AI Agent controlled executor handoff snapshots found in {directory}")
|
||||
|
||||
latest = candidates[-1]
|
||||
with latest.open(encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"{latest}: expected JSON object")
|
||||
|
||||
label = str(latest)
|
||||
_require_schema(payload, label)
|
||||
_require_sources(payload, label)
|
||||
_require_truth(payload, label)
|
||||
_require_packets(payload, label)
|
||||
_require_routes(payload, label)
|
||||
_require_verifier_bindings(payload, label)
|
||||
_require_learning_contracts(payload, label)
|
||||
_require_boundaries(payload, label)
|
||||
_require_redaction_contract(payload, label)
|
||||
_require_rollups(payload, label)
|
||||
_require_no_forbidden_public_terms(payload, label)
|
||||
return payload
|
||||
|
||||
|
||||
def _require_schema(payload: dict[str, Any], label: str) -> None:
|
||||
if payload.get("schema_version") != _SCHEMA_VERSION:
|
||||
raise ValueError(f"{label}: expected schema_version={_SCHEMA_VERSION}")
|
||||
status = payload.get("program_status") or {}
|
||||
expected = {
|
||||
"overall_completion_percent": 100,
|
||||
"current_priority": "P0",
|
||||
"current_task_id": _EXPECTED_CURRENT_TASK,
|
||||
"next_task_id": _EXPECTED_NEXT_TASK,
|
||||
"read_only_mode": True,
|
||||
"runtime_authority": _RUNTIME_AUTHORITY,
|
||||
}
|
||||
mismatches = _mismatches(status, expected)
|
||||
if mismatches:
|
||||
raise ValueError(f"{label}: program_status mismatch: {mismatches}")
|
||||
if not status.get("status_note"):
|
||||
raise ValueError(f"{label}: program_status.status_note is required")
|
||||
|
||||
|
||||
def _require_sources(payload: dict[str, Any], label: str) -> None:
|
||||
sources = payload.get("source_readbacks") or []
|
||||
schemas = {item.get("source_schema_version") for item in sources}
|
||||
if schemas != _EXPECTED_SOURCE_SCHEMAS:
|
||||
raise ValueError(f"{label}: source schemas mismatch: {sorted(schemas)}")
|
||||
for source in sources:
|
||||
if source.get("status") != "loaded":
|
||||
raise ValueError(f"{label}: source {source.get('readback_id')} must be loaded")
|
||||
|
||||
|
||||
def _require_truth(payload: dict[str, Any], label: str) -> None:
|
||||
truth = payload.get("handoff_truth") or {}
|
||||
missing = sorted(field for field in _TRUE_TRUTH_FLAGS if truth.get(field) is not True)
|
||||
if missing:
|
||||
raise ValueError(f"{label}: handoff truth flags must remain true: {missing}")
|
||||
unsafe = sorted(field for field in _FALSE_TRUTH_FLAGS if truth.get(field) is not False)
|
||||
if unsafe:
|
||||
raise ValueError(f"{label}: live/write/unsafe truth flags must remain false: {unsafe}")
|
||||
non_zero = sorted(field for field in _ZERO_TRUTH_COUNTS if truth.get(field) != 0)
|
||||
if non_zero:
|
||||
raise ValueError(f"{label}: live/write/unsafe truth counts must remain zero: {non_zero}")
|
||||
if not truth.get("truth_note"):
|
||||
raise ValueError(f"{label}: handoff_truth.truth_note is required")
|
||||
|
||||
|
||||
def _require_packets(payload: dict[str, Any], label: str) -> None:
|
||||
packets = payload.get("executor_handoff_packets") or []
|
||||
if len(packets) != 7:
|
||||
raise ValueError(f"{label}: executor_handoff_packets must contain 7 items")
|
||||
|
||||
high_ready = 0
|
||||
critical_break_glass = 0
|
||||
seen: set[str] = set()
|
||||
for packet in packets:
|
||||
packet_id = packet.get("packet_id")
|
||||
if not packet_id or packet_id in seen:
|
||||
raise ValueError(f"{label}: packet_id must be unique")
|
||||
seen.add(packet_id)
|
||||
|
||||
if packet.get("live_apply_performed") is not False or packet.get("side_effect_count") != 0:
|
||||
raise ValueError(f"{label}: packet {packet_id} must not perform live apply or side effects")
|
||||
|
||||
if packet.get("risk_tier") == "high":
|
||||
high_ready += 1
|
||||
expected_true = {
|
||||
"allowlist_match",
|
||||
"check_mode_passed",
|
||||
"rollback_plan_ready",
|
||||
"post_action_verifier_ready",
|
||||
"telegram_evidence_ready",
|
||||
"km_writeback_ready",
|
||||
"playbook_trust_writeback_ready",
|
||||
"controlled_executor_handoff_allowed",
|
||||
}
|
||||
missing = sorted(field for field in expected_true if packet.get(field) is not True)
|
||||
if missing:
|
||||
raise ValueError(f"{label}: high packet {packet_id} missing controlled executor gates: {missing}")
|
||||
if packet.get("owner_response_required") is not False:
|
||||
raise ValueError(f"{label}: high packet {packet_id} must not require owner response")
|
||||
if packet.get("break_glass_required") is not False:
|
||||
raise ValueError(f"{label}: high packet {packet_id} must not require break-glass")
|
||||
if packet.get("handoff_status") != "ready_for_controlled_executor":
|
||||
raise ValueError(f"{label}: high packet {packet_id} must be ready_for_controlled_executor")
|
||||
|
||||
elif packet.get("risk_tier") == "critical":
|
||||
critical_break_glass += 1
|
||||
if packet.get("handoff_status") != "critical_break_glass_only":
|
||||
raise ValueError(f"{label}: critical packet {packet_id} must remain critical_break_glass_only")
|
||||
if packet.get("controlled_executor_handoff_allowed") is not False:
|
||||
raise ValueError(f"{label}: critical packet {packet_id} must not allow controlled executor handoff")
|
||||
if packet.get("owner_response_required") is not True or packet.get("break_glass_required") is not True:
|
||||
raise ValueError(f"{label}: critical packet {packet_id} must require owner response and break-glass")
|
||||
else:
|
||||
raise ValueError(f"{label}: packet {packet_id} risk_tier is invalid")
|
||||
|
||||
if high_ready != 5 or critical_break_glass != 2:
|
||||
raise ValueError(f"{label}: expected high ready=5 and critical break-glass=2")
|
||||
|
||||
|
||||
def _require_routes(payload: dict[str, Any], label: str) -> None:
|
||||
routes = payload.get("executor_routes") or []
|
||||
if len(routes) != 5:
|
||||
raise ValueError(f"{label}: executor_routes must contain 5 items")
|
||||
for route in routes:
|
||||
route_id = route.get("route_id")
|
||||
if route.get("route_status") != "ready_for_handoff":
|
||||
raise ValueError(f"{label}: route {route_id} must be ready_for_handoff")
|
||||
if route.get("live_apply_allowed_by_this_readback") is not False:
|
||||
raise ValueError(f"{label}: route {route_id} must not allow live apply from readback")
|
||||
if not route.get("required_inputs") or not route.get("blocked_actions"):
|
||||
raise ValueError(f"{label}: route {route_id} must list inputs and blocked actions")
|
||||
|
||||
|
||||
def _require_verifier_bindings(payload: dict[str, Any], label: str) -> None:
|
||||
bindings = payload.get("verifier_bindings") or []
|
||||
if len(bindings) != 5:
|
||||
raise ValueError(f"{label}: verifier_bindings must contain 5 items")
|
||||
for binding in bindings:
|
||||
binding_id = binding.get("binding_id")
|
||||
if binding.get("required_before_dispatch") is not True:
|
||||
raise ValueError(f"{label}: binding {binding_id} must be required before dispatch")
|
||||
if binding.get("ready_count") != 5 or binding.get("blocked_count") != 0:
|
||||
raise ValueError(f"{label}: binding {binding_id} must have ready_count=5 and blocked_count=0")
|
||||
if not binding.get("failure_if_missing"):
|
||||
raise ValueError(f"{label}: binding {binding_id} failure_if_missing is required")
|
||||
|
||||
|
||||
def _require_learning_contracts(payload: dict[str, Any], label: str) -> None:
|
||||
contracts = payload.get("learning_writeback_contracts") or []
|
||||
if len(contracts) != 3:
|
||||
raise ValueError(f"{label}: learning_writeback_contracts must contain 3 items")
|
||||
for contract in contracts:
|
||||
contract_id = contract.get("contract_id")
|
||||
if contract.get("writeback_status") != "ready_for_executor_receipt":
|
||||
raise ValueError(f"{label}: contract {contract_id} must be ready_for_executor_receipt")
|
||||
if contract.get("runtime_write_performed") is not False:
|
||||
raise ValueError(f"{label}: contract {contract_id} must not perform runtime write in readback")
|
||||
if not contract.get("required_fields"):
|
||||
raise ValueError(f"{label}: contract {contract_id} required_fields is required")
|
||||
|
||||
|
||||
def _require_boundaries(payload: dict[str, Any], label: str) -> None:
|
||||
boundaries = payload.get("activation_boundaries") or {}
|
||||
missing = sorted(field for field in _TRUE_BOUNDARY_FLAGS if boundaries.get(field) is not True)
|
||||
if missing:
|
||||
raise ValueError(f"{label}: preview boundaries must remain true: {missing}")
|
||||
unsafe = sorted(field for field in _FALSE_BOUNDARY_FLAGS if boundaries.get(field) is not False)
|
||||
if unsafe:
|
||||
raise ValueError(f"{label}: live/write boundaries must remain false: {unsafe}")
|
||||
|
||||
|
||||
def _require_redaction_contract(payload: dict[str, Any], label: str) -> None:
|
||||
contract = payload.get("display_redaction_contract") or {}
|
||||
if contract.get("redaction_required") is not True:
|
||||
raise ValueError(f"{label}: display redaction must be required")
|
||||
required_false = {
|
||||
"raw_tool_output_display_allowed",
|
||||
"raw_runtime_payload_display_allowed",
|
||||
"raw_telegram_payload_display_allowed",
|
||||
"private_reasoning_display_allowed",
|
||||
"secret_value_display_allowed",
|
||||
"work_window_transcript_display_allowed",
|
||||
}
|
||||
unsafe = sorted(field for field in required_false if contract.get(field) is not False)
|
||||
if unsafe:
|
||||
raise ValueError(f"{label}: display redaction fields must remain false: {unsafe}")
|
||||
|
||||
|
||||
def _require_rollups(payload: dict[str, Any], label: str) -> None:
|
||||
rollups = payload.get("rollups") or {}
|
||||
sources = payload.get("source_readbacks") or []
|
||||
packets = payload.get("executor_handoff_packets") or []
|
||||
routes = payload.get("executor_routes") or []
|
||||
bindings = payload.get("verifier_bindings") or []
|
||||
learning_contracts = payload.get("learning_writeback_contracts") or []
|
||||
|
||||
high_packets = [packet for packet in packets if packet.get("risk_tier") == "high"]
|
||||
critical_packets = [packet for packet in packets if packet.get("risk_tier") == "critical"]
|
||||
expected = {
|
||||
"source_readback_count": len(sources),
|
||||
"handoff_packet_count": len(packets),
|
||||
"ready_for_controlled_executor_count": sum(
|
||||
1 for packet in packets if packet.get("handoff_status") == "ready_for_controlled_executor"
|
||||
),
|
||||
"critical_break_glass_count": sum(
|
||||
1 for packet in packets if packet.get("handoff_status") == "critical_break_glass_only"
|
||||
),
|
||||
"high_risk_packet_count": len(high_packets),
|
||||
"critical_packet_count": len(critical_packets),
|
||||
"ansible_check_mode_packet_count": sum(1 for packet in packets if packet.get("executor_type") == "ansible_playbook"),
|
||||
"mcp_tool_route_count": sum(1 for packet in packets if packet.get("mcp_tool_ref")),
|
||||
"post_action_verifier_binding_count": sum(1 for packet in high_packets if packet.get("post_action_verifier_ready") is True),
|
||||
"telegram_evidence_binding_count": sum(1 for packet in high_packets if packet.get("telegram_evidence_ready") is True),
|
||||
"km_writeback_binding_count": sum(1 for packet in high_packets if packet.get("km_writeback_ready") is True),
|
||||
"playbook_trust_writeback_binding_count": sum(
|
||||
1 for packet in high_packets if packet.get("playbook_trust_writeback_ready") is True
|
||||
),
|
||||
"owner_response_required_count": sum(1 for packet in packets if packet.get("owner_response_required") is True),
|
||||
"blocked_by_critical_boundary_count": len(critical_packets),
|
||||
"missing_check_mode_count": sum(1 for packet in high_packets if packet.get("check_mode_passed") is not True),
|
||||
"missing_rollback_count": sum(1 for packet in high_packets if packet.get("rollback_plan_ready") is not True),
|
||||
"missing_verifier_count": sum(1 for packet in high_packets if packet.get("post_action_verifier_ready") is not True),
|
||||
"missing_telegram_evidence_count": sum(1 for packet in high_packets if packet.get("telegram_evidence_ready") is not True),
|
||||
"missing_learning_writeback_count": sum(
|
||||
1
|
||||
for packet in high_packets
|
||||
if packet.get("km_writeback_ready") is not True
|
||||
or packet.get("playbook_trust_writeback_ready") is not True
|
||||
),
|
||||
"executor_route_count": len(routes),
|
||||
"verifier_binding_count": len(bindings),
|
||||
"learning_writeback_contract_count": len(learning_contracts),
|
||||
}
|
||||
mismatches = sorted(field for field, value in expected.items() if rollups.get(field) != value)
|
||||
if mismatches:
|
||||
raise ValueError(f"{label}: rollup counts must match source arrays: {mismatches}")
|
||||
|
||||
non_zero = sorted(field for field in _ZERO_ROLLUP_FIELDS if rollups.get(field) != 0)
|
||||
if non_zero:
|
||||
raise ValueError(f"{label}: live/write rollup counts must remain zero: {non_zero}")
|
||||
|
||||
|
||||
def _require_no_forbidden_public_terms(payload: dict[str, Any], label: str) -> None:
|
||||
encoded = json.dumps(payload, ensure_ascii=False)
|
||||
hits = sorted(term for term in _FORBIDDEN_PUBLIC_TERMS if term in encoded)
|
||||
if hits:
|
||||
raise ValueError(f"{label}: forbidden public terms found: {hits}")
|
||||
|
||||
|
||||
def _mismatches(payload: dict[str, Any], expected: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
key: {"expected": value, "actual": payload.get(key)}
|
||||
for key, value in expected.items()
|
||||
if payload.get(key) != value
|
||||
}
|
||||
Reference in New Issue
Block a user