diff --git a/backend/apps/agents/browser/route_write.py b/backend/apps/agents/browser/route_write.py index c424edca..9fd97df9 100644 --- a/backend/apps/agents/browser/route_write.py +++ b/backend/apps/agents/browser/route_write.py @@ -19,6 +19,7 @@ SAFETY (this IS the posture flip away from GET/HEAD-only, so the walls are belt- """ import json +import logging import os import re import time @@ -32,6 +33,8 @@ from typeguard import typechecked from backend.apps.social_shims.session_source import get_session +logger = logging.getLogger(__name__) + WRITE_METHODS = frozenset({"POST", "PUT", "PATCH", "DELETE"}) # CSRF header a site derives from a cookie (so it survives a fresh borrowed session). Small on # purpose: most cookie-auth internal APIs need nothing extra; this covers the common header case. @@ -206,5 +209,10 @@ def replay_write(method: str, url: str, body: Dict[str, Any], origin: str, try: status, text = issue_request(method, url, body, headers) except Exception as e: + # UNEXPECTED: the request itself blew up (network/DNS/TLS), not a same-origin/captured refusal. Fails open to the UI, so log it or the broken route-write tier is invisible. + logger.warning(f"[route-write] {method} {url} request FAILED (fast path broken here): {e}") return ReplayOutcome(ok=False, error=str(e)[:160], latency_ms=int((time.monotonic() - t0) * 1000)) - return outcome_from_response(status, text, int((time.monotonic() - t0) * 1000)) + out = outcome_from_response(status, text, int((time.monotonic() - t0) * 1000)) + if not out.ok: + logger.info(f"[route-write] {method} {url} rejected by site: HTTP {out.status}") + return out diff --git a/backend/apps/agents/browser/site_write_registry.py b/backend/apps/agents/browser/site_write_registry.py index 6cf672c7..b17eb151 100644 --- a/backend/apps/agents/browser/site_write_registry.py +++ b/backend/apps/agents/browser/site_write_registry.py @@ -11,6 +11,7 @@ Live-validated end to end on Reddit (comment 271ms + reversible delete 246ms, ty """ import asyncio +import logging import os import time from typing import Any, Callable, Dict, FrozenSet, List, Tuple @@ -22,6 +23,8 @@ from typeguard import typechecked from backend.apps.agents.browser import route_write from backend.apps.reddit_mcp_shim import reddit_writes +logger = logging.getLogger(__name__) + class WriteResult(BaseModel): """The typed outcome of an API-first write. `receipt` is the site's own id/permalink, the @@ -133,6 +136,13 @@ async def api_write(domain: str, action: str, params: Dict[str, Any]) -> WriteRe return WriteResult(ok=True, action=action, domain=d, receipt=receipt_str(receipt), latency_ms=int((time.monotonic() - t0) * 1000)) - except Exception as e: + except reddit_writes.RedditError as e: + # EXPECTED site-side reject (not logged in, rate-limited, bad params): the model sees it and falls back; info, not an alarm. (Future adapters should raise their own recognizable reject type to land here.) + logger.info(f"[api-write] {d}/{action} rejected by the site: {e}") + return WriteResult(ok=False, action=action, domain=d, error=str(e)[:200], + latency_ms=int((time.monotonic() - t0) * 1000)) + except Exception as e: + # UNEXPECTED: the adapter code itself threw (a bug, or the site changed shape). It fails open to the UI, so without this WARNING a systemically-broken fast path is INVISIBLE (looks like the tier just isn't used). + logger.warning(f"[api-write] {d}/{action} adapter FAILED unexpectedly (fast path broken here): {e}") return WriteResult(ok=False, action=action, domain=d, error=str(e)[:200], latency_ms=int((time.monotonic() - t0) * 1000))