File size: 3,875 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""
Typed errors for the interactive service.

Every error carries a stable ``code`` so the router can map it to
the right HTTP status without leaking internal details, and so the
frontend can branch on the code string instead of parsing messages.

Design rule: the service NEVER raises raw ``Exception``. Unexpected
exceptions are caught at the router boundary and wrapped in
``InteractiveError(code='internal', …)`` so observability stays
tidy. See ``router.py`` handlers.
"""
from __future__ import annotations

from typing import Any, Dict, Optional


class InteractiveError(Exception):
    """Base β€” all interactive-service errors inherit from this."""

    #: Default HTTP status when the router maps this to a response.
    http_status: int = 500
    #: Stable machine-readable code. Frontend branches on this.
    code: str = "internal"

    def __init__(
        self,
        message: str,
        *,
        code: Optional[str] = None,
        http_status: Optional[int] = None,
        data: Optional[Dict[str, Any]] = None,
    ) -> None:
        super().__init__(message)
        self.message = message
        if code is not None:
            self.code = code
        if http_status is not None:
            self.http_status = http_status
        self.data = dict(data or {})

    def to_dict(self) -> Dict[str, Any]:
        return {
            "ok": False,
            "code": self.code,
            "error": self.message,
            "data": self.data,
        }


# ── Configuration / feature-flag ──────────────────────────────────

class ServiceDisabledError(InteractiveError):
    http_status = 503
    code = "service_disabled"


# ── Input validation ──────────────────────────────────────────────

class InvalidInputError(InteractiveError):
    http_status = 400
    code = "invalid_input"


# ── Authorization ─────────────────────────────────────────────────

class NotAuthenticatedError(InteractiveError):
    http_status = 401
    code = "not_authenticated"


class NotAuthorizedError(InteractiveError):
    """Authenticated but the resource doesn't belong to the caller."""

    http_status = 404  # probe-safe β€” 404 hides existence
    code = "not_found"


# ── Resource lookup ───────────────────────────────────────────────

class NotFoundError(InteractiveError):
    http_status = 404
    code = "not_found"


# ── Structural / graph ────────────────────────────────────────────

class GraphError(InteractiveError):
    http_status = 422
    code = "graph_invalid"


class CapacityError(InteractiveError):
    """Branching graph or similar structure exceeds configured cap."""

    http_status = 422
    code = "capacity_exceeded"


# ── Policy ────────────────────────────────────────────────────────

class PolicyBlockError(InteractiveError):
    http_status = 403
    code = "policy_blocked"


class ConsentRequiredError(InteractiveError):
    http_status = 403
    code = "consent_required"


# ── Runtime ───────────────────────────────────────────────────────

class InvalidStateError(InteractiveError):
    http_status = 409
    code = "invalid_state"


class CooldownError(InteractiveError):
    http_status = 429
    code = "cooldown"