File size: 5,341 Bytes
d60732b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
123
124
125
126
127
128
129
130
131
"""Shared functions for API routes — breaks circular import.

Route modules import from here instead of api_server.py.
"""
import logging
import time
from typing import Any, Optional

# [G5-FIX] Header import at module level — verify_admin uses it in signature
try:
    from fastapi import Header
except ImportError:
    Header = None  # type: ignore

logger = logging.getLogger("scp.api")


# Default value for authorization header (used if Header not available)
_AUTH_DEFAULT = Header("", alias="Authorization") if Header else ""


# [Fix 4-a-006 / Phase 3-A — DNA #5, #14, #19]
# TẠI SAO verify_admin + rate-limit helpers are now IMPORTED from
# scp.security.auth (not defined here):
#   Before: TWO divergent `verify_admin` functions existed:
#     - helpers.py: HTTPBearer-based, NO rate limiting, 401 on no-config (dead code).
#     - _shared.py (HERE): Header-based, HAS rate limiting, 503 on no-config (WRONG).
#   After: ONE canonical impl in scp.security.auth (with rate limiting + 401 on
#   no-config — best of both). _shared.py re-exports it so existing
#   `from scp.api._shared import verify_admin` imports keep working. If you
#   need to change auth behavior, edit scp.security.auth — not this file.
from scp.security.auth import (  # noqa: E402,F401  (re-exported for backward-compat)
    _RATE_LIMIT_MAX_FAILURES,
    _RATE_LIMIT_WINDOW,
    _auth_failures,
    _check_rate_limit,
    _record_auth_failure,
    verify_admin,
)


def get_judge():
    """Get RealityJudge singleton."""
    from scp.runtime.judge import RealityJudge
    return RealityJudge()


def _extract_v98_context(request) -> dict[str, Any]:
    """Extract V98 context from request."""
    ctx = {}
    ctx["ip"] = request.client.host if request.client else "unknown"
    ctx["user_agent"] = request.headers.get("user-agent", "")
    return ctx


# [FIX] Stubs for route imports — delegate to api_server at runtime

from typing import Any

from pydantic import BaseModel, Field


class SessionAnalyzeRequest(BaseModel):
    session_logs: list[dict[str, Any]] = []
    model_responses: list[dict[str, Any]] = []


class SimulationRequest(BaseModel):
    count: int = Field(50, ge=1, le=500)

# [SCP-DNA-FIX] Replace 12 broken shim functions with a PEP 562 module __getattr__.
#
# TẠI SAO (Reality > Model): the previous shims were defined as
#     def _multi_turn_tracker(*args, **kwargs):
#         from scp.api_server import _multi_turn_tracker as _impl
#         return _impl(*args, **kwargs)
# but `api_server._multi_turn_tracker` is a SINGLETON INSTANCE
# (`_multi_turn_tracker = MultiTurnTracker()`), NOT a callable. So:
#   - calling the shim `_multi_turn_tracker()` -> TypeError (instance not callable)
#   - not calling it `_multi_turn_tracker.stats()` -> AttributeError
#     ('function' object has no attribute 'stats')
# Every /v104/* and /v103/* route that touched these names was broken at
# runtime. pylint E1102/E1101 flagged 27 sites; ruff is blind to it
# (no cross-module type inference). The bug passed 3 audit rounds because
# the routes were never exercised in the test transcript (PowerShell.txt
# has zero HTTP request logs) and SCP's defensive try/except swallowed
# the errors. PASS != TRUE.
#
# Fix: __getattr__ returns the api_server attribute DIRECTLY (the instance /
# bool / function — whatever it actually is). Routes' `_multi_turn_tracker.stats()`
# then resolves `.stats` on the real instance. Zero callers invoked the old
# shims with parens (verified by grep across scp/), so this is backward-
# compatible. Circular-import safety: api_server.py defines these singletons
# at module level (lines 176-188) BEFORE it imports the route modules inside
# create_app() (lines 494+). By the time any route does
# `from scp.api._shared import X`, api_server is fully loaded.
_DELEGATED_NAMES = frozenset({
    "_SCP_VERSION", "_V1042_AVAILABLE", "_attack_crawler",
    "_cross_language_learner", "_fact_checker", "_fast_learning",
    "_image_detector", "_multi_turn_tracker", "_real_learning",
    "_safe_fetch_url", "_simple_explainer", "_voice_detector",
})


def __getattr__(name: str):
    """PEP 562 — lazily delegate singleton/function lookups to api_server.

    Fires only for names NOT already defined on this module (i.e. the
    delegated singletons listed in _DELEGATED_NAMES). Returns the real
    attribute from scp.api_server (instance, bool, or function — as-is),
    so route code like `_multi_turn_tracker.stats()` resolves `.stats` on
    the real singleton instance rather than on a broken shim function.
    """
    if name in _DELEGATED_NAMES:
        from scp import api_server
        try:
            return getattr(api_server, name)
        except AttributeError:
            # api_server mid-import or attr genuinely missing — surface a
            # clear error rather than a silent None (DNA: never silent).
            raise AttributeError(
                f"module 'scp.api._shared' cannot resolve '{name}' — "
                f"'scp.api_server' has no such attribute (circular import or missing singleton)"
            ) from None
    raise AttributeError(f"module 'scp.api._shared' has no attribute {name!r}")


def __dir__():
    """Make the delegated names discoverable (tab-completion, pydoc)."""
    return sorted(set(globals().keys()) | set(_DELEGATED_NAMES))