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.
Files changed (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. 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