claude-smart 0.2.43 → 0.2.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +1 -1
- package/bin/claude-smart.js +2 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +21 -1
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/backend-service.sh +50 -4
- package/plugin/scripts/cli.sh +2 -1
- package/plugin/scripts/ensure-plugin-root.sh +1 -0
- package/plugin/scripts/hook_entry.sh +5 -3
- package/plugin/scripts/smart-install.sh +2 -2
- package/plugin/src/README.md +57 -0
- package/plugin/uv.lock +126 -5
- package/plugin/vendor/reflexio/.env.example +9 -0
- package/plugin/vendor/reflexio/pyproject.toml +5 -2
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +0 -1
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +7 -4
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +4 -4
- package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +2 -2
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +13 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +3 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +59 -6
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +7 -1
- package/plugin/vendor/reflexio/reflexio/server/api.py +188 -34
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +33 -11
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +278 -29
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +457 -181
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +22 -12
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +137 -9
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +36 -3
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +13 -3
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.1.prompt.md +69 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +71 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +27 -23
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.2.prompt.md +234 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +244 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +19 -9
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +1 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +44 -2
- package/plugin/vendor/reflexio/reflexio/server/services/embedding_text.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +91 -24
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +18 -5
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +2 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +88 -3
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +36 -5
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +32 -22
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +89 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +46 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +8 -4
|
@@ -26,7 +26,11 @@ app = typer.Typer(
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
_PROVIDERS: dict[str, dict[str, str]] = {
|
|
29
|
-
"openai": {
|
|
29
|
+
"openai": {
|
|
30
|
+
"env_var": "OPENAI_API_KEY",
|
|
31
|
+
"model": "gpt-5.4-mini",
|
|
32
|
+
"display": "OpenAI",
|
|
33
|
+
},
|
|
30
34
|
"anthropic": {
|
|
31
35
|
"env_var": "ANTHROPIC_API_KEY",
|
|
32
36
|
"model": "claude-sonnet-4-6",
|
|
@@ -885,9 +889,8 @@ def openai_codex_setup(
|
|
|
885
889
|
reads from this file directly — no dependency on OpenClaw or any other
|
|
886
890
|
CLI. The proxy auto-refreshes the access token when it nears expiry.
|
|
887
891
|
|
|
888
|
-
Run this once
|
|
889
|
-
|
|
890
|
-
./reflexio_ext/scripts/start_with_codex_proxy.sh
|
|
892
|
+
Run this once; the codex proxy (``codex_proxy.py``) then picks up the
|
|
893
|
+
stored tokens automatically on start.
|
|
891
894
|
|
|
892
895
|
Re-run this command if your subscription tier changes or the
|
|
893
896
|
refresh_token gets revoked (rare).
|
|
@@ -107,8 +107,8 @@ def load_reflexio_env(
|
|
|
107
107
|
|
|
108
108
|
Args:
|
|
109
109
|
package_data_module: Module containing bundled .env.example
|
|
110
|
-
(for importlib.resources). OS package
|
|
111
|
-
|
|
110
|
+
(for importlib.resources). The OS package passes "reflexio.data";
|
|
111
|
+
a downstream build may pass its own data module.
|
|
112
112
|
auto_generate_keys: Env var names to auto-generate as hex tokens
|
|
113
113
|
(e.g., ["JWT_SECRET_KEY"]).
|
|
114
114
|
|
|
@@ -150,8 +150,8 @@ def ensure_user_env_for_setup(
|
|
|
150
150
|
|
|
151
151
|
Args:
|
|
152
152
|
package_data_module: Module containing the bundled ``.env.example``
|
|
153
|
-
template (for ``importlib.resources``). OS
|
|
154
|
-
|
|
153
|
+
template (for ``importlib.resources``). The OS package passes
|
|
154
|
+
``reflexio.data``; a downstream build may pass its own data module.
|
|
155
155
|
auto_generate_keys: Env var names to auto-fill with random hex
|
|
156
156
|
tokens when creating from the template.
|
|
157
157
|
|
|
@@ -24,8 +24,8 @@ def describe_storage(
|
|
|
24
24
|
to print anywhere.
|
|
25
25
|
|
|
26
26
|
Enterprise-only storage types (Supabase, Postgres, local directory)
|
|
27
|
-
are matched by class name so
|
|
28
|
-
|
|
27
|
+
are matched by class name so the open-source package takes no hard
|
|
28
|
+
import dependency on its enterprise extension.
|
|
29
29
|
|
|
30
30
|
Args:
|
|
31
31
|
storage_config: The storage configuration to describe.
|
|
@@ -233,7 +233,9 @@ class UserProfile(BaseModel):
|
|
|
233
233
|
custom_features: dict | None = None
|
|
234
234
|
source: str | None = None
|
|
235
235
|
status: Status | None = None # indicates the status of the profile
|
|
236
|
-
extractor_names: list[str] | None =
|
|
236
|
+
extractor_names: list[str] | None = (
|
|
237
|
+
None # Retained provenance data column (merged on dedup); new profiles write None.
|
|
238
|
+
)
|
|
237
239
|
expanded_terms: str | None = None
|
|
238
240
|
embedding: EmbeddingVector = []
|
|
239
241
|
source_span: str | None = None
|
|
@@ -554,6 +556,7 @@ class InteractionData(BaseModel):
|
|
|
554
556
|
|
|
555
557
|
# publish user interaction request
|
|
556
558
|
class PublishUserInteractionRequest(BaseModel):
|
|
559
|
+
request_id: NonEmptyStr | None = None
|
|
557
560
|
user_id: NonEmptyStr
|
|
558
561
|
interaction_data_list: list[InteractionData] = Field(min_length=1)
|
|
559
562
|
source: str = ""
|
|
@@ -891,7 +894,9 @@ class RerunProfileGenerationRequest(BaseModel):
|
|
|
891
894
|
start_time: datetime | None = None
|
|
892
895
|
end_time: datetime | None = None
|
|
893
896
|
source: str | None = None
|
|
894
|
-
extractor_names: list[str] | None =
|
|
897
|
+
extractor_names: list[str] | None = (
|
|
898
|
+
None # Deprecated compatibility field; ignored for selection.
|
|
899
|
+
)
|
|
895
900
|
|
|
896
901
|
@model_validator(mode="after")
|
|
897
902
|
def check_time_range(self) -> Self:
|
|
@@ -936,7 +941,9 @@ class ManualPlaybookGenerationRequest(BaseModel):
|
|
|
936
941
|
|
|
937
942
|
agent_version: str = DEFAULT_AGENT_VERSION
|
|
938
943
|
source: str | None = None
|
|
939
|
-
playbook_name: str | None =
|
|
944
|
+
playbook_name: str | None = (
|
|
945
|
+
None # Deprecated compatibility field; ignored for selection.
|
|
946
|
+
)
|
|
940
947
|
|
|
941
948
|
@field_validator("agent_version")
|
|
942
949
|
@classmethod
|
|
@@ -956,7 +963,9 @@ class RerunPlaybookGenerationRequest(BaseModel):
|
|
|
956
963
|
agent_version: str = DEFAULT_AGENT_VERSION
|
|
957
964
|
start_time: datetime | None = None
|
|
958
965
|
end_time: datetime | None = None
|
|
959
|
-
playbook_name: str | None =
|
|
966
|
+
playbook_name: str | None = (
|
|
967
|
+
None # Deprecated compatibility field; ignored for selection.
|
|
968
|
+
)
|
|
960
969
|
source: str | None = None
|
|
961
970
|
|
|
962
971
|
@field_validator("agent_version")
|
|
@@ -56,7 +56,9 @@ class SearchUserProfileRequest(BaseModel):
|
|
|
56
56
|
top_k: int | None = Field(default=10, gt=0)
|
|
57
57
|
source: str | None = None
|
|
58
58
|
custom_feature: str | None = None
|
|
59
|
-
extractor_name: str | None =
|
|
59
|
+
extractor_name: str | None = (
|
|
60
|
+
None # Deprecated compatibility field; accepted but ignored.
|
|
61
|
+
)
|
|
60
62
|
threshold: float | None = Field(default=0.4, ge=0.0, le=1.0)
|
|
61
63
|
enable_reformulation: bool | None = False
|
|
62
64
|
search_mode: SearchMode = SearchMode.HYBRID
|
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
import ipaddress
|
|
18
18
|
import os
|
|
19
19
|
import re
|
|
20
|
+
import socket
|
|
20
21
|
from typing import Annotated, Any
|
|
21
22
|
from urllib.parse import urlparse
|
|
22
23
|
|
|
@@ -125,6 +126,33 @@ def _is_strict_mode() -> bool:
|
|
|
125
126
|
)
|
|
126
127
|
|
|
127
128
|
|
|
129
|
+
def _is_metadata_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
|
|
130
|
+
"""Return whether an IP address is a known cloud metadata endpoint."""
|
|
131
|
+
return str(ip) in METADATA_IPS
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _is_forbidden_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
|
|
135
|
+
"""Return whether an IP address is unsafe for server-side HTTP callbacks."""
|
|
136
|
+
return (
|
|
137
|
+
_is_metadata_ip(ip)
|
|
138
|
+
or ip.is_private
|
|
139
|
+
or ip.is_reserved
|
|
140
|
+
or ip.is_loopback
|
|
141
|
+
or ip.is_link_local
|
|
142
|
+
or ip.is_multicast
|
|
143
|
+
or ip.is_unspecified
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _resolved_host_ips(host: str, port: int | None) -> set[str]:
|
|
148
|
+
"""Resolve a hostname into IP address strings for SSRF validation."""
|
|
149
|
+
try:
|
|
150
|
+
addr_info = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
|
|
151
|
+
except socket.gaierror as exc:
|
|
152
|
+
raise ValueError(f"URL hostname could not be resolved: {host}") from exc
|
|
153
|
+
return {str(entry[4][0]) for entry in addr_info}
|
|
154
|
+
|
|
155
|
+
|
|
128
156
|
def _check_safe_url(v: Any) -> Any:
|
|
129
157
|
"""Validate URL is not targeting dangerous resources.
|
|
130
158
|
|
|
@@ -150,13 +178,11 @@ def _check_safe_url(v: Any) -> Any:
|
|
|
150
178
|
|
|
151
179
|
try:
|
|
152
180
|
ip = ipaddress.ip_address(host)
|
|
153
|
-
if
|
|
181
|
+
if _is_metadata_ip(ip):
|
|
154
182
|
raise ValueError(f"URL must not target cloud metadata endpoint: {host}")
|
|
155
183
|
|
|
156
184
|
# In strict mode, also block private/localhost
|
|
157
|
-
if _is_strict_mode() and (
|
|
158
|
-
ip.is_private or ip.is_reserved or ip.is_loopback or ip.is_link_local
|
|
159
|
-
):
|
|
185
|
+
if _is_strict_mode() and _is_forbidden_ip(ip):
|
|
160
186
|
raise ValueError(
|
|
161
187
|
f"URL targets private/reserved IP '{host}'. "
|
|
162
188
|
f"If running locally, unset REFLEXIO_BLOCK_PRIVATE_URLS."
|
|
@@ -164,12 +190,39 @@ def _check_safe_url(v: Any) -> Any:
|
|
|
164
190
|
except ValueError as e:
|
|
165
191
|
if "must not target" in str(e) or "targets private" in str(e):
|
|
166
192
|
raise
|
|
167
|
-
# Not an IP (hostname)
|
|
168
|
-
|
|
193
|
+
# Not an IP literal (hostname). Resolve and inspect each IP: cloud
|
|
194
|
+
# metadata endpoints are ALWAYS blocked (no legitimate use case),
|
|
195
|
+
# while private/reserved ranges are blocked only in strict mode.
|
|
196
|
+
strict = _is_strict_mode()
|
|
197
|
+
if strict and host in ("localhost", "0.0.0.0"): # noqa: S104
|
|
169
198
|
raise ValueError(
|
|
170
199
|
f"URL targets '{host}'. "
|
|
171
200
|
f"If running locally, unset REFLEXIO_BLOCK_PRIVATE_URLS."
|
|
172
201
|
) from None
|
|
202
|
+
try:
|
|
203
|
+
resolved_ips = _resolved_host_ips(host, parsed.port)
|
|
204
|
+
except ValueError:
|
|
205
|
+
# A resolution failure is only fatal in strict mode; otherwise we
|
|
206
|
+
# cannot confirm a metadata target and must not break local dev.
|
|
207
|
+
if strict:
|
|
208
|
+
raise
|
|
209
|
+
resolved_ips = set()
|
|
210
|
+
for resolved_ip in resolved_ips:
|
|
211
|
+
try:
|
|
212
|
+
ip = ipaddress.ip_address(resolved_ip)
|
|
213
|
+
except ValueError:
|
|
214
|
+
continue
|
|
215
|
+
if _is_metadata_ip(ip):
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"URL hostname '{host}' resolves to cloud metadata "
|
|
218
|
+
f"endpoint '{resolved_ip}'."
|
|
219
|
+
) from None
|
|
220
|
+
if strict and _is_forbidden_ip(ip):
|
|
221
|
+
raise ValueError(
|
|
222
|
+
f"URL hostname '{host}' resolves to private/reserved IP "
|
|
223
|
+
f"'{resolved_ip}'. If running locally, unset "
|
|
224
|
+
f"REFLEXIO_BLOCK_PRIVATE_URLS."
|
|
225
|
+
) from None
|
|
173
226
|
|
|
174
227
|
return v
|
|
175
228
|
|
|
@@ -18,7 +18,7 @@ EMBEDDING_DIMENSIONS = 512
|
|
|
18
18
|
|
|
19
19
|
# Default sliding window parameters for extraction
|
|
20
20
|
DEFAULT_WINDOW_SIZE = 10
|
|
21
|
-
DEFAULT_STRIDE_SIZE =
|
|
21
|
+
DEFAULT_STRIDE_SIZE = 8
|
|
22
22
|
|
|
23
23
|
# Deprecated aliases kept for older imports.
|
|
24
24
|
DEFAULT_BATCH_SIZE = DEFAULT_WINDOW_SIZE
|
|
@@ -54,6 +54,8 @@ Description: FastAPI backend server that processes user interactions to generate
|
|
|
54
54
|
|
|
55
55
|
**Directory**: `api_endpoints/`
|
|
56
56
|
|
|
57
|
+
**Detailed Documentation**: See [`api_endpoints/README.md`](api_endpoints/README.md) for the `RequestContext` contract and per-file handler map.
|
|
58
|
+
|
|
57
59
|
| File | Purpose |
|
|
58
60
|
|------|---------|
|
|
59
61
|
| `request_context.py` | RequestContext (bundles org_id, storage, configurator, prompt_manager) |
|
|
@@ -77,7 +79,7 @@ Description: FastAPI backend server that processes user interactions to generate
|
|
|
77
79
|
- **Operations/admin**: `GET /api/get_operation_status`, `POST /api/cancel_operation`, `POST /api/admin/cache/invalidate`, `DELETE /api/delete_interaction`, `DELETE /api/delete_request`, `DELETE /api/delete_session`, `DELETE /api/delete_requests_by_ids`, `DELETE /api/delete_all_interactions`, `POST /api/clear_user_data`
|
|
78
80
|
- **Human clarification/stall state**: `GET /api/pending_tool_calls`, `GET /api/pending_tool_calls/{pending_tool_call_id}`, `POST /api/pending_tool_calls/{pending_tool_call_id}/resolve`, `PATCH /api/pending_tool_calls/{pending_tool_call_id}/answer`, `POST /api/pending_tool_calls/{pending_tool_call_id}/not_applicable`, `POST /api/pending_tool_calls/{pending_tool_call_id}/cancel`, `GET /api/stall_state`, `POST /api/stall_state/notified`
|
|
79
81
|
|
|
80
|
-
**Authentication Pattern**: The open-source app uses `default_get_org_id` and `DEFAULT_ORG_ID` for local/no-auth starts.
|
|
82
|
+
**Authentication Pattern**: The open-source app uses `default_get_org_id` and `DEFAULT_ORG_ID` for local/no-auth starts. The enterprise extension wraps `create_app()` with authenticated org resolution, login/OAuth/account/share/waitlist routers, admin checks, Sentry tracing, and usage metrics.
|
|
81
83
|
|
|
82
84
|
**Pattern**: Core route handlers call `Reflexio` through `get_reflexio(org_id)`; endpoint helper files should not instantiate `Reflexio` directly.
|
|
83
85
|
|
|
@@ -172,6 +174,8 @@ python -m reflexio.server.scripts.manage_invitation_codes list --show-used
|
|
|
172
174
|
|
|
173
175
|
**Directory**: `services/`
|
|
174
176
|
|
|
177
|
+
**Detailed Documentation**: See [`services/README.md`](services/README.md) for the per-directory file index across generation, evaluation, async extraction, search, and persistence.
|
|
178
|
+
|
|
175
179
|
**Service Boundary**: The service layer owns LLM orchestration, extraction, evaluation, optimization, search preparation, storage access, and long-running operation state. API endpoints should validate/authenticate requests, build `RequestContext`, and delegate into `Reflexio` or focused service helpers rather than embedding business logic.
|
|
176
180
|
|
|
177
181
|
**Encapsulated Components**:
|
|
@@ -611,6 +615,8 @@ All services follow BaseGenerationService:
|
|
|
611
615
|
## See Also
|
|
612
616
|
|
|
613
617
|
- [Code Map (root README)](../README.md) -- high-level overview of all Reflexio components
|
|
618
|
+
- [API Endpoints README](api_endpoints/README.md) -- RequestContext contract and handler/helper map
|
|
619
|
+
- [Services README](services/README.md) -- per-directory index of the business-logic layer
|
|
614
620
|
- [Prompt Bank README](prompt/prompt_bank/README.md) -- versioned prompt template system
|
|
615
621
|
- [Playbook Service README](services/playbook/README.md) -- playbook extraction, aggregation, and deduplication pipeline
|
|
616
622
|
- [Site Variables README](site_var/README.md) -- global configuration and feature flags
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import inspect
|
|
3
3
|
import logging
|
|
4
|
-
import
|
|
4
|
+
import os
|
|
5
5
|
from collections.abc import Callable
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
7
|
from datetime import UTC, datetime
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
@@ -21,6 +22,7 @@ from slowapi.errors import RateLimitExceeded
|
|
|
21
22
|
from slowapi.util import get_remote_address
|
|
22
23
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
23
24
|
from starlette.responses import Response
|
|
25
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
24
26
|
|
|
25
27
|
from reflexio.models.api_schema.braintrust_schema import (
|
|
26
28
|
BraintrustStatusResponse,
|
|
@@ -194,6 +196,51 @@ SYNC_REQUEST_TIMEOUT_SECONDS = (
|
|
|
194
196
|
)
|
|
195
197
|
SUSPICIOUS_USER_AGENTS = ["bot", "crawler", "spider", "scraper", "curl", "wget"]
|
|
196
198
|
ALLOWED_EMPTY_UA_PATHS = ["/health", "/"] # Paths that allow empty user agents
|
|
199
|
+
DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024
|
|
200
|
+
REGENERATE_MAX_WORKERS = 2
|
|
201
|
+
_regen_executor = ThreadPoolExecutor(
|
|
202
|
+
max_workers=REGENERATE_MAX_WORKERS,
|
|
203
|
+
thread_name_prefix="reflexio-regen",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _resolve_cors_origins() -> list[str]:
|
|
208
|
+
"""Resolve browser origins allowed to make credentialed CORS requests."""
|
|
209
|
+
configured_origins = os.getenv("REFLEXIO_ALLOWED_ORIGINS", "").strip()
|
|
210
|
+
if configured_origins:
|
|
211
|
+
origins = [
|
|
212
|
+
origin.strip().rstrip("/")
|
|
213
|
+
for origin in configured_origins.split(",")
|
|
214
|
+
if origin.strip()
|
|
215
|
+
]
|
|
216
|
+
return origins or ["http://localhost:8080"]
|
|
217
|
+
|
|
218
|
+
frontend_url = os.getenv("FRONTEND_URL", "").strip()
|
|
219
|
+
if frontend_url:
|
|
220
|
+
return [frontend_url.rstrip("/")]
|
|
221
|
+
|
|
222
|
+
return ["http://localhost:8080"]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _max_body_bytes_from_env() -> int:
|
|
226
|
+
raw_value = os.getenv("REFLEXIO_MAX_BODY_BYTES", str(DEFAULT_MAX_BODY_BYTES))
|
|
227
|
+
try:
|
|
228
|
+
max_bytes = int(raw_value)
|
|
229
|
+
except ValueError:
|
|
230
|
+
logger.warning(
|
|
231
|
+
"Ignoring invalid REFLEXIO_MAX_BODY_BYTES=%r; using %s",
|
|
232
|
+
raw_value,
|
|
233
|
+
DEFAULT_MAX_BODY_BYTES,
|
|
234
|
+
)
|
|
235
|
+
return DEFAULT_MAX_BODY_BYTES
|
|
236
|
+
if max_bytes <= 0:
|
|
237
|
+
logger.warning(
|
|
238
|
+
"Ignoring non-positive REFLEXIO_MAX_BODY_BYTES=%r; using %s",
|
|
239
|
+
raw_value,
|
|
240
|
+
DEFAULT_MAX_BODY_BYTES,
|
|
241
|
+
)
|
|
242
|
+
return DEFAULT_MAX_BODY_BYTES
|
|
243
|
+
return max_bytes
|
|
197
244
|
|
|
198
245
|
|
|
199
246
|
def get_rate_limit_key(request: Request) -> str:
|
|
@@ -312,6 +359,82 @@ class TimeoutMiddleware(BaseHTTPMiddleware):
|
|
|
312
359
|
)
|
|
313
360
|
|
|
314
361
|
|
|
362
|
+
class _RequestBodyTooLargeError(Exception):
|
|
363
|
+
"""Raised when the streamed request body exceeds the configured limit."""
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class BodySizeLimitMiddleware:
|
|
367
|
+
"""Reject requests whose declared or streamed body size exceeds the limit."""
|
|
368
|
+
|
|
369
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
370
|
+
self.app = app
|
|
371
|
+
|
|
372
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
373
|
+
from starlette.responses import JSONResponse
|
|
374
|
+
|
|
375
|
+
if scope["type"] != "http":
|
|
376
|
+
await self.app(scope, receive, send)
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
max_body_bytes = _max_body_bytes_from_env()
|
|
380
|
+
content_length = None
|
|
381
|
+
for name, value in scope.get("headers", []):
|
|
382
|
+
if name.lower() == b"content-length":
|
|
383
|
+
content_length = value.decode("latin-1")
|
|
384
|
+
break
|
|
385
|
+
|
|
386
|
+
if content_length is not None:
|
|
387
|
+
try:
|
|
388
|
+
body_bytes = int(content_length)
|
|
389
|
+
except ValueError:
|
|
390
|
+
body_bytes = 0
|
|
391
|
+
if body_bytes > max_body_bytes:
|
|
392
|
+
await JSONResponse(
|
|
393
|
+
status_code=status.HTTP_413_CONTENT_TOO_LARGE,
|
|
394
|
+
content={"detail": "Request body too large"},
|
|
395
|
+
)(scope, receive, send)
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
consumed_bytes = 0
|
|
399
|
+
|
|
400
|
+
async def limited_receive() -> Message:
|
|
401
|
+
nonlocal consumed_bytes
|
|
402
|
+
message = await receive()
|
|
403
|
+
if message["type"] == "http.request":
|
|
404
|
+
consumed_bytes += len(message.get("body", b""))
|
|
405
|
+
if consumed_bytes > max_body_bytes:
|
|
406
|
+
raise _RequestBodyTooLargeError
|
|
407
|
+
return message
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
await self.app(scope, limited_receive, send)
|
|
411
|
+
except _RequestBodyTooLargeError:
|
|
412
|
+
await JSONResponse(
|
|
413
|
+
status_code=status.HTTP_413_CONTENT_TOO_LARGE,
|
|
414
|
+
content={"detail": "Request body too large"},
|
|
415
|
+
)(scope, receive, send)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
419
|
+
"""Attach conservative browser security headers to every response."""
|
|
420
|
+
|
|
421
|
+
async def dispatch(
|
|
422
|
+
self, request: Request, call_next: RequestResponseEndpoint
|
|
423
|
+
) -> Response:
|
|
424
|
+
response = await call_next(request)
|
|
425
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
426
|
+
response.headers["X-Frame-Options"] = "DENY"
|
|
427
|
+
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
428
|
+
if (
|
|
429
|
+
request.url.scheme == "https"
|
|
430
|
+
or request.headers.get("x-forwarded-proto", "").lower() == "https"
|
|
431
|
+
):
|
|
432
|
+
response.headers["Strict-Transport-Security"] = (
|
|
433
|
+
"max-age=31536000; includeSubDomains"
|
|
434
|
+
)
|
|
435
|
+
return response
|
|
436
|
+
|
|
437
|
+
|
|
315
438
|
class CorrelationIdMiddleware(BaseHTTPMiddleware):
|
|
316
439
|
"""Middleware that assigns a unique correlation ID to each request.
|
|
317
440
|
|
|
@@ -931,7 +1054,9 @@ def delete_user_playbooks_by_ids(
|
|
|
931
1054
|
response_model=BulkDeleteResponse,
|
|
932
1055
|
response_model_exclude_none=True,
|
|
933
1056
|
)
|
|
1057
|
+
@limiter.limit("10/minute")
|
|
934
1058
|
def delete_all_interactions(
|
|
1059
|
+
request: Request,
|
|
935
1060
|
org_id: str = Depends(default_get_org_id),
|
|
936
1061
|
) -> BulkDeleteResponse:
|
|
937
1062
|
"""Delete all requests and their associated interactions.
|
|
@@ -950,7 +1075,9 @@ def delete_all_interactions(
|
|
|
950
1075
|
response_model=BulkDeleteResponse,
|
|
951
1076
|
response_model_exclude_none=True,
|
|
952
1077
|
)
|
|
1078
|
+
@limiter.limit("10/minute")
|
|
953
1079
|
def delete_all_profiles(
|
|
1080
|
+
request: Request,
|
|
954
1081
|
org_id: str = Depends(default_get_org_id),
|
|
955
1082
|
) -> BulkDeleteResponse:
|
|
956
1083
|
"""Delete all profiles.
|
|
@@ -969,7 +1096,9 @@ def delete_all_profiles(
|
|
|
969
1096
|
response_model=BulkDeleteResponse,
|
|
970
1097
|
response_model_exclude_none=True,
|
|
971
1098
|
)
|
|
1099
|
+
@limiter.limit("10/minute")
|
|
972
1100
|
def delete_all_playbooks(
|
|
1101
|
+
request: Request,
|
|
973
1102
|
org_id: str = Depends(default_get_org_id),
|
|
974
1103
|
) -> BulkDeleteResponse:
|
|
975
1104
|
"""Delete all playbooks (both user and agent).
|
|
@@ -988,7 +1117,9 @@ def delete_all_playbooks(
|
|
|
988
1117
|
response_model=BulkDeleteResponse,
|
|
989
1118
|
response_model_exclude_none=True,
|
|
990
1119
|
)
|
|
1120
|
+
@limiter.limit("10/minute")
|
|
991
1121
|
def delete_all_user_playbooks(
|
|
1122
|
+
request: Request,
|
|
992
1123
|
org_id: str = Depends(default_get_org_id),
|
|
993
1124
|
) -> BulkDeleteResponse:
|
|
994
1125
|
"""Delete all user playbooks (user only, not agent).
|
|
@@ -1007,7 +1138,9 @@ def delete_all_user_playbooks(
|
|
|
1007
1138
|
response_model=BulkDeleteResponse,
|
|
1008
1139
|
response_model_exclude_none=True,
|
|
1009
1140
|
)
|
|
1141
|
+
@limiter.limit("10/minute")
|
|
1010
1142
|
def delete_all_agent_playbooks(
|
|
1143
|
+
request: Request,
|
|
1011
1144
|
org_id: str = Depends(default_get_org_id),
|
|
1012
1145
|
) -> BulkDeleteResponse:
|
|
1013
1146
|
"""Delete all agent playbooks (agent only, not user).
|
|
@@ -1026,8 +1159,10 @@ def delete_all_agent_playbooks(
|
|
|
1026
1159
|
response_model=ClearUserDataResponse,
|
|
1027
1160
|
response_model_exclude_none=True,
|
|
1028
1161
|
)
|
|
1162
|
+
@limiter.limit("10/minute")
|
|
1029
1163
|
def clear_user_data(
|
|
1030
|
-
request:
|
|
1164
|
+
request: Request,
|
|
1165
|
+
payload: ClearUserDataRequest,
|
|
1031
1166
|
org_id: str = Depends(default_get_org_id),
|
|
1032
1167
|
) -> ClearUserDataResponse:
|
|
1033
1168
|
"""Delete all rows scoped to a single ``user_id``.
|
|
@@ -1046,7 +1181,7 @@ def clear_user_data(
|
|
|
1046
1181
|
Returns:
|
|
1047
1182
|
ClearUserDataResponse: Response with per-entity deletion counts
|
|
1048
1183
|
"""
|
|
1049
|
-
return publisher_api.clear_user_data(org_id=org_id, request=
|
|
1184
|
+
return publisher_api.clear_user_data(org_id=org_id, request=payload)
|
|
1050
1185
|
|
|
1051
1186
|
|
|
1052
1187
|
@core_router.post(
|
|
@@ -1071,7 +1206,9 @@ def get_interactions(
|
|
|
1071
1206
|
response_model=GetInteractionsViewResponse,
|
|
1072
1207
|
response_model_exclude_none=True,
|
|
1073
1208
|
)
|
|
1209
|
+
@limiter.limit("30/minute")
|
|
1074
1210
|
def get_all_interactions(
|
|
1211
|
+
request: Request,
|
|
1075
1212
|
limit: int = 100,
|
|
1076
1213
|
org_id: str = Depends(default_get_org_id),
|
|
1077
1214
|
) -> GetInteractionsViewResponse:
|
|
@@ -1154,7 +1291,9 @@ def get_profiles(
|
|
|
1154
1291
|
response_model=GetProfilesViewResponse,
|
|
1155
1292
|
response_model_exclude_none=True,
|
|
1156
1293
|
)
|
|
1294
|
+
@limiter.limit("30/minute")
|
|
1157
1295
|
def get_all_profiles(
|
|
1296
|
+
request: Request,
|
|
1158
1297
|
limit: int = 100,
|
|
1159
1298
|
status_filter: str | None = None,
|
|
1160
1299
|
org_id: str = Depends(default_get_org_id),
|
|
@@ -1230,7 +1369,9 @@ def run_playbook_aggregation(
|
|
|
1230
1369
|
|
|
1231
1370
|
|
|
1232
1371
|
@core_router.post("/api/set_config")
|
|
1372
|
+
@limiter.limit("10/minute")
|
|
1233
1373
|
def set_config(
|
|
1374
|
+
request: Request,
|
|
1234
1375
|
config: dict[str, Any],
|
|
1235
1376
|
org_id: str = Depends(default_get_org_id),
|
|
1236
1377
|
) -> SetConfigResponse:
|
|
@@ -1257,7 +1398,9 @@ def set_config(
|
|
|
1257
1398
|
|
|
1258
1399
|
|
|
1259
1400
|
@core_router.post("/api/update_config")
|
|
1401
|
+
@limiter.limit("10/minute")
|
|
1260
1402
|
def update_config(
|
|
1403
|
+
request: Request,
|
|
1261
1404
|
partial: dict[str, Any],
|
|
1262
1405
|
org_id: str = Depends(default_get_org_id),
|
|
1263
1406
|
) -> SetConfigResponse:
|
|
@@ -1566,8 +1709,10 @@ def update_user_profile_endpoint(
|
|
|
1566
1709
|
response_model=GetDashboardStatsResponse,
|
|
1567
1710
|
response_model_exclude_none=True,
|
|
1568
1711
|
)
|
|
1712
|
+
@limiter.limit("30/minute")
|
|
1569
1713
|
def get_dashboard_stats(
|
|
1570
|
-
request:
|
|
1714
|
+
request: Request,
|
|
1715
|
+
payload: GetDashboardStatsRequest,
|
|
1571
1716
|
org_id: str = Depends(default_get_org_id),
|
|
1572
1717
|
) -> GetDashboardStatsResponse:
|
|
1573
1718
|
"""Get comprehensive dashboard statistics including counts and time-series data.
|
|
@@ -1583,7 +1728,7 @@ def get_dashboard_stats(
|
|
|
1583
1728
|
reflexio = get_reflexio(org_id=org_id)
|
|
1584
1729
|
|
|
1585
1730
|
# Get dashboard stats using Reflexio's method
|
|
1586
|
-
return reflexio.get_dashboard_stats(
|
|
1731
|
+
return reflexio.get_dashboard_stats(payload)
|
|
1587
1732
|
|
|
1588
1733
|
|
|
1589
1734
|
@core_router.post(
|
|
@@ -1755,7 +1900,9 @@ def get_evaluation_overview(
|
|
|
1755
1900
|
response_model=RegenerateStartResponse,
|
|
1756
1901
|
response_model_exclude_none=True,
|
|
1757
1902
|
)
|
|
1903
|
+
@limiter.limit("5/minute")
|
|
1758
1904
|
def start_regenerate(
|
|
1905
|
+
request: Request,
|
|
1759
1906
|
payload: RegenerateRequest,
|
|
1760
1907
|
org_id: str = Depends(default_get_org_id),
|
|
1761
1908
|
) -> RegenerateStartResponse:
|
|
@@ -1789,15 +1936,12 @@ def start_regenerate(
|
|
|
1789
1936
|
)
|
|
1790
1937
|
except RuntimeError as e:
|
|
1791
1938
|
raise HTTPException(status_code=409, detail=str(e)) from e
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
},
|
|
1799
|
-
daemon=True,
|
|
1800
|
-
).start()
|
|
1939
|
+
_regen_executor.submit(
|
|
1940
|
+
run_regen,
|
|
1941
|
+
job=job,
|
|
1942
|
+
request_context=reflexio.request_context,
|
|
1943
|
+
llm_client=reflexio.llm_client,
|
|
1944
|
+
)
|
|
1801
1945
|
return RegenerateStartResponse(job_id=job.job_id, total=job.total)
|
|
1802
1946
|
|
|
1803
1947
|
|
|
@@ -2612,24 +2756,16 @@ def create_app(
|
|
|
2612
2756
|
@asynccontextmanager
|
|
2613
2757
|
async def lifespan(app: FastAPI) -> AsyncIterator[None]: # noqa: ARG001
|
|
2614
2758
|
validate_llm_availability()
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
if retrieval_floor.enabled:
|
|
2619
|
-
from reflexio.server.llm.rerank import prewarm as _prewarm_cross_encoder
|
|
2620
|
-
|
|
2621
|
-
_prewarm_cross_encoder()
|
|
2622
|
-
else:
|
|
2623
|
-
logger.info(
|
|
2624
|
-
"Cross-encoder pre-warm skipped because retrieval_floor.enabled=false"
|
|
2625
|
-
)
|
|
2759
|
+
from reflexio.server.llm.rerank import prewarm as _prewarm_cross_encoder
|
|
2760
|
+
|
|
2761
|
+
_prewarm_cross_encoder()
|
|
2626
2762
|
# The scheduler discovers every org with resumable work each tick and
|
|
2627
2763
|
# drives a per-org worker with org-scoped claims, so it is not limited
|
|
2628
2764
|
# to the bootstrap org. The bootstrap org is only used to read config
|
|
2629
2765
|
# and to seed cross-org discovery.
|
|
2630
2766
|
scheduler = maybe_start_resume_scheduler(
|
|
2631
2767
|
lambda org_id: RequestContext(org_id=org_id),
|
|
2632
|
-
bootstrap_org_id=
|
|
2768
|
+
bootstrap_org_id=_lifespan_org_id(),
|
|
2633
2769
|
)
|
|
2634
2770
|
try:
|
|
2635
2771
|
yield
|
|
@@ -2661,14 +2797,32 @@ def create_app(
|
|
|
2661
2797
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore[reportArgumentType]
|
|
2662
2798
|
|
|
2663
2799
|
# CORS
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2800
|
+
# The locked-down, credentialed allowlist is an enterprise concern: only
|
|
2801
|
+
# hosts that wire in auth (``require_auth=True``) restrict browser origins.
|
|
2802
|
+
# OSS/local runs have no auth and bundle their own docs playground on a
|
|
2803
|
+
# separate port, so they allow any origin (no credentials needed).
|
|
2804
|
+
if require_auth:
|
|
2805
|
+
app.add_middleware(
|
|
2806
|
+
CORSMiddleware,
|
|
2807
|
+
allow_origins=_resolve_cors_origins(),
|
|
2808
|
+
allow_credentials=True,
|
|
2809
|
+
allow_methods=["*"],
|
|
2810
|
+
allow_headers=["*"],
|
|
2811
|
+
)
|
|
2812
|
+
else:
|
|
2813
|
+
app.add_middleware(
|
|
2814
|
+
CORSMiddleware,
|
|
2815
|
+
allow_origins=["*"],
|
|
2816
|
+
allow_credentials=False,
|
|
2817
|
+
allow_methods=["*"],
|
|
2818
|
+
allow_headers=["*"],
|
|
2819
|
+
)
|
|
2820
|
+
|
|
2821
|
+
# Reject oversized requests before they reach endpoint handlers.
|
|
2822
|
+
app.add_middleware(BodySizeLimitMiddleware)
|
|
2823
|
+
|
|
2824
|
+
# Security headers
|
|
2825
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
2672
2826
|
|
|
2673
2827
|
# Timeout middleware
|
|
2674
2828
|
app.add_middleware(TimeoutMiddleware)
|