crabcode 0.1.6__py3-none-win_amd64.whl
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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- crabcode_search/tool.py +268 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Gateway middleware — auth, logging, CORS, error handling.
|
|
2
|
+
|
|
3
|
+
Mirrors OpenCode's middleware.ts pattern.
|
|
4
|
+
|
|
5
|
+
NOTE: All custom middleware below are pure ASGI middleware (not
|
|
6
|
+
BaseHTTPMiddleware) because BaseHTTPMiddleware rejects WebSocket
|
|
7
|
+
upgrade requests with 403 by design.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import base64
|
|
14
|
+
import binascii
|
|
15
|
+
import hmac
|
|
16
|
+
import time
|
|
17
|
+
from urllib.parse import parse_qs
|
|
18
|
+
|
|
19
|
+
from fastapi import FastAPI, Request, Response
|
|
20
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
21
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
22
|
+
|
|
23
|
+
from crabcode_core.logging_utils import get_logger
|
|
24
|
+
from crabcode_gateway.auth import decode_jwt, verify_password
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── Helper: check if scope is a WebSocket request ────────────────
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_websocket(scope: Scope) -> bool:
|
|
33
|
+
return scope.get("type") == "websocket"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _scope_header(scope: Scope, name: str) -> str:
|
|
37
|
+
"""Return the first value for an HTTP/WS header in an ASGI scope."""
|
|
38
|
+
wanted = name.lower().encode("latin-1")
|
|
39
|
+
for key, value in scope.get("headers", []):
|
|
40
|
+
if key.lower() == wanted:
|
|
41
|
+
try:
|
|
42
|
+
return value.decode("latin-1")
|
|
43
|
+
except (AttributeError, UnicodeDecodeError):
|
|
44
|
+
return ""
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _scope_query(scope: Scope, name: str) -> str | None:
|
|
49
|
+
"""Read one query parameter without constructing a Request/WebSocket."""
|
|
50
|
+
raw = scope.get("query_string", b"")
|
|
51
|
+
if isinstance(raw, bytes):
|
|
52
|
+
raw = raw.decode("latin-1")
|
|
53
|
+
values = parse_qs(raw, keep_blank_values=True).get(name)
|
|
54
|
+
return values[0] if values else None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_authorized(
|
|
58
|
+
scope: Scope,
|
|
59
|
+
*,
|
|
60
|
+
username: str = "crabcode",
|
|
61
|
+
password: str | None = None,
|
|
62
|
+
password_hash: str | None = None,
|
|
63
|
+
mode: str = "password",
|
|
64
|
+
jwt_secret: str | None = None,
|
|
65
|
+
) -> bool:
|
|
66
|
+
"""Validate gateway credentials for an HTTP or WebSocket ASGI scope.
|
|
67
|
+
|
|
68
|
+
The gateway historically accepted a Basic password, a Bearer token, and
|
|
69
|
+
the ``auth_token`` query parameter. Keep all three forms so existing
|
|
70
|
+
clients continue to work, while applying the same policy to WebSockets.
|
|
71
|
+
"""
|
|
72
|
+
if mode == "none":
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
query_token = _scope_query(scope, "auth_token")
|
|
76
|
+
if query_token is not None:
|
|
77
|
+
if jwt_secret and decode_jwt(query_token, jwt_secret):
|
|
78
|
+
return True
|
|
79
|
+
if mode in ("password", "mixed"):
|
|
80
|
+
if password and hmac.compare_digest(query_token, password):
|
|
81
|
+
return True
|
|
82
|
+
if password_hash and verify_password(query_token, password_hash):
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
auth_header = _scope_header(scope, "authorization")
|
|
86
|
+
scheme, separator, credentials = auth_header.partition(" ")
|
|
87
|
+
if not separator:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
if scheme.lower() == "bearer":
|
|
91
|
+
if jwt_secret and decode_jwt(credentials, jwt_secret):
|
|
92
|
+
return True
|
|
93
|
+
# Keep the existing static bearer password working during migration.
|
|
94
|
+
if mode in ("password", "mixed"):
|
|
95
|
+
if password and hmac.compare_digest(credentials, password):
|
|
96
|
+
return True
|
|
97
|
+
if password_hash and verify_password(credentials, password_hash):
|
|
98
|
+
return True
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
if scheme.lower() != "basic":
|
|
102
|
+
return False
|
|
103
|
+
if mode not in ("password", "mixed"):
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
decoded = base64.b64decode(credentials, validate=True).decode("utf-8")
|
|
108
|
+
user, supplied_password = decoded.split(":", 1)
|
|
109
|
+
except (ValueError, UnicodeDecodeError, binascii.Error):
|
|
110
|
+
return False
|
|
111
|
+
return hmac.compare_digest(user, username) and (
|
|
112
|
+
(password is not None and hmac.compare_digest(supplied_password, password))
|
|
113
|
+
or (password_hash is not None and verify_password(supplied_password, password_hash))
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ── Auth middleware ──────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class AuthMiddleware:
|
|
121
|
+
"""JWT, public-key bootstrap, and compatible password authentication.
|
|
122
|
+
|
|
123
|
+
Skipped when gateway security mode is ``none``.
|
|
124
|
+
The same credentials are required for WebSocket upgrades. Rejecting
|
|
125
|
+
before ``websocket.accept`` avoids exposing an unauthenticated stream.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
app: ASGIApp,
|
|
131
|
+
username: str = "crabcode",
|
|
132
|
+
password: str | None = None,
|
|
133
|
+
password_hash: str | None = None,
|
|
134
|
+
mode: str = "password",
|
|
135
|
+
jwt_secret: str | None = None,
|
|
136
|
+
) -> None:
|
|
137
|
+
self.app = app
|
|
138
|
+
self.username = username
|
|
139
|
+
self.password = password
|
|
140
|
+
self.password_hash = password_hash
|
|
141
|
+
self.mode = mode
|
|
142
|
+
self.jwt_secret = jwt_secret
|
|
143
|
+
|
|
144
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
145
|
+
if _is_websocket(scope):
|
|
146
|
+
if self.mode != "none" and not is_authorized(
|
|
147
|
+
scope,
|
|
148
|
+
username=self.username,
|
|
149
|
+
password=self.password,
|
|
150
|
+
password_hash=self.password_hash,
|
|
151
|
+
mode=self.mode,
|
|
152
|
+
jwt_secret=self.jwt_secret,
|
|
153
|
+
):
|
|
154
|
+
await send({
|
|
155
|
+
"type": "websocket.close",
|
|
156
|
+
"code": 1008,
|
|
157
|
+
"reason": "Unauthorized",
|
|
158
|
+
})
|
|
159
|
+
return
|
|
160
|
+
await self.app(scope, receive, send)
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
if scope["type"] != "http":
|
|
164
|
+
await self.app(scope, receive, send)
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Build a Request to inspect headers conveniently
|
|
168
|
+
request = Request(scope, receive, send)
|
|
169
|
+
|
|
170
|
+
# Skip auth for OPTIONS (CORS preflight)
|
|
171
|
+
if request.method == "OPTIONS":
|
|
172
|
+
await self.app(scope, receive, send)
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
if self.mode == "none":
|
|
176
|
+
await self.app(scope, receive, send)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
if request.url.path.startswith("/auth/"):
|
|
180
|
+
await self.app(scope, receive, send)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
if is_authorized(
|
|
184
|
+
scope,
|
|
185
|
+
username=self.username,
|
|
186
|
+
password=self.password,
|
|
187
|
+
password_hash=self.password_hash,
|
|
188
|
+
mode=self.mode,
|
|
189
|
+
jwt_secret=self.jwt_secret,
|
|
190
|
+
):
|
|
191
|
+
await self.app(scope, receive, send)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# Not authenticated
|
|
195
|
+
response = Response(
|
|
196
|
+
content='{"detail":"Unauthorized"}',
|
|
197
|
+
status_code=401,
|
|
198
|
+
media_type="application/json",
|
|
199
|
+
headers={"WWW-Authenticate": 'Basic realm="crabcode"'},
|
|
200
|
+
)
|
|
201
|
+
await response(scope, receive, send)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ── Logging middleware ──────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class LoggerMiddleware:
|
|
208
|
+
"""Log incoming requests and their duration."""
|
|
209
|
+
|
|
210
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
211
|
+
self.app = app
|
|
212
|
+
|
|
213
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
214
|
+
if _is_websocket(scope):
|
|
215
|
+
# Log WebSocket connections briefly
|
|
216
|
+
path = scope.get("path", "/")
|
|
217
|
+
logger.info("WebSocket %s", path)
|
|
218
|
+
await self.app(scope, receive, send)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
if scope["type"] != "http":
|
|
222
|
+
await self.app(scope, receive, send)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
request = Request(scope, receive, send)
|
|
226
|
+
|
|
227
|
+
# Skip noisy endpoints
|
|
228
|
+
if request.url.path in ("/health", "/event"):
|
|
229
|
+
await self.app(scope, receive, send)
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
start = time.monotonic()
|
|
233
|
+
logger.info("request %s %s", request.method, request.url.path)
|
|
234
|
+
|
|
235
|
+
# Capture status code from the inner app's response
|
|
236
|
+
status_code: int = 0
|
|
237
|
+
|
|
238
|
+
async def send_wrapper(message: dict) -> None:
|
|
239
|
+
nonlocal status_code
|
|
240
|
+
if message["type"] == "http.response.start":
|
|
241
|
+
status_code = message.get("status", 0)
|
|
242
|
+
await send(message)
|
|
243
|
+
|
|
244
|
+
await self.app(scope, receive, send_wrapper)
|
|
245
|
+
|
|
246
|
+
elapsed = time.monotonic() - start
|
|
247
|
+
logger.info(
|
|
248
|
+
"request %s %s → %d (%.3fs)",
|
|
249
|
+
request.method,
|
|
250
|
+
request.url.path,
|
|
251
|
+
status_code,
|
|
252
|
+
elapsed,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ── Error middleware ─────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class ErrorMiddleware:
|
|
260
|
+
"""Catch unhandled exceptions and return structured JSON errors."""
|
|
261
|
+
|
|
262
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
263
|
+
self.app = app
|
|
264
|
+
|
|
265
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
266
|
+
if _is_websocket(scope):
|
|
267
|
+
await self.app(scope, receive, send)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
if scope["type"] != "http":
|
|
271
|
+
await self.app(scope, receive, send)
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
await self.app(scope, receive, send)
|
|
276
|
+
except Exception as exc:
|
|
277
|
+
logger.exception("Unhandled error on %s", scope.get("path", "unknown"))
|
|
278
|
+
response = Response(
|
|
279
|
+
content=json.dumps({
|
|
280
|
+
"type": "error",
|
|
281
|
+
"message": str(exc),
|
|
282
|
+
"recoverable": False,
|
|
283
|
+
"error_type": "internal",
|
|
284
|
+
}),
|
|
285
|
+
status_code=500,
|
|
286
|
+
media_type="application/json",
|
|
287
|
+
)
|
|
288
|
+
await response(scope, receive, send)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ── CORS setup ──────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def setup_cors(app: FastAPI, extra_origins: list[str] | None = None) -> None:
|
|
295
|
+
"""Add CORS middleware matching OpenCode's policy.
|
|
296
|
+
|
|
297
|
+
Allows localhost, tauri, vscode-webview, and configurable origins.
|
|
298
|
+
"""
|
|
299
|
+
app.add_middleware(
|
|
300
|
+
CORSMiddleware,
|
|
301
|
+
allow_origin_regex=(
|
|
302
|
+
r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$"
|
|
303
|
+
r"|^https?://tauri\.localhost$"
|
|
304
|
+
r"|^tauri://localhost$"
|
|
305
|
+
r"|^vscode-webview://"
|
|
306
|
+
),
|
|
307
|
+
allow_origins=extra_origins or [],
|
|
308
|
+
allow_credentials=True,
|
|
309
|
+
allow_methods=["*"],
|
|
310
|
+
allow_headers=["*"],
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ── Helper: register all middleware ─────────────────────────────
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def register_middleware(
|
|
318
|
+
app: FastAPI,
|
|
319
|
+
*,
|
|
320
|
+
password: str | None = None,
|
|
321
|
+
password_hash: str | None = None,
|
|
322
|
+
security_mode: str | None = None,
|
|
323
|
+
jwt_secret: str | None = None,
|
|
324
|
+
cors_origins: list[str] | None = None,
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Register the full middleware stack on the FastAPI app."""
|
|
327
|
+
# Order matters: outermost first
|
|
328
|
+
app.add_middleware(ErrorMiddleware)
|
|
329
|
+
|
|
330
|
+
resolved_mode = security_mode or (
|
|
331
|
+
"password" if password is not None or password_hash is not None else "none"
|
|
332
|
+
)
|
|
333
|
+
if resolved_mode != "none":
|
|
334
|
+
app.add_middleware(
|
|
335
|
+
AuthMiddleware,
|
|
336
|
+
password=password,
|
|
337
|
+
password_hash=password_hash,
|
|
338
|
+
mode=resolved_mode,
|
|
339
|
+
jwt_secret=jwt_secret,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
app.add_middleware(LoggerMiddleware)
|
|
343
|
+
|
|
344
|
+
setup_cors(app, extra_origins=cors_origins)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Wire protocol metadata shared by CrabCode gateway transports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Increment this value when clients and gateways need an explicit compatibility
|
|
7
|
+
# boundary. Additive changes may keep the same protocol version.
|
|
8
|
+
GATEWAY_PROTOCOL_VERSION = 1
|
|
9
|
+
GATEWAY_MIN_PROTOCOL_VERSION = 1
|
|
10
|
+
GATEWAY_MAX_PROTOCOL_VERSION = 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"GATEWAY_MAX_PROTOCOL_VERSION",
|
|
15
|
+
"GATEWAY_MIN_PROTOCOL_VERSION",
|
|
16
|
+
"GATEWAY_PROTOCOL_VERSION",
|
|
17
|
+
]
|
|
File without changes
|