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,114 @@
|
|
|
1
|
+
"""ACP session manager — tracks ACP-side session state.
|
|
2
|
+
|
|
3
|
+
Each ACP session wraps an internal CrabCode session (created via the
|
|
4
|
+
Gateway REST API). The manager holds the mapping between ACP session
|
|
5
|
+
IDs and their associated state (cwd, model, mode, MCP servers).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from acp.exceptions import RequestError
|
|
14
|
+
from acp.schema import HttpMcpServer, McpServerStdio, SseMcpServer
|
|
15
|
+
|
|
16
|
+
from crabcode_core.logging_utils import get_logger
|
|
17
|
+
from crabcode_gateway.acp.types import ACPConfig, ACPSessionState, ModelSelection
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ACPSessionManager:
|
|
23
|
+
"""Manages ACP session lifecycle backed by the Gateway HTTP API."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, config: ACPConfig) -> None:
|
|
26
|
+
self._config = config
|
|
27
|
+
self._sessions: dict[str, ACPSessionState] = {}
|
|
28
|
+
self._client = httpx.AsyncClient(base_url=config.base_url, timeout=60.0)
|
|
29
|
+
|
|
30
|
+
# ── CRUD ────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
async def create(
|
|
33
|
+
self,
|
|
34
|
+
cwd: str,
|
|
35
|
+
mcp_servers: list[McpServerStdio | HttpMcpServer | SseMcpServer],
|
|
36
|
+
model: ModelSelection | None = None,
|
|
37
|
+
) -> ACPSessionState:
|
|
38
|
+
"""Create a new session via the Gateway API and store ACP state."""
|
|
39
|
+
resp = await self._client.post("/session/new", json={"cwd": cwd})
|
|
40
|
+
resp.raise_for_status()
|
|
41
|
+
data = resp.json()
|
|
42
|
+
session_id = data["session_id"]
|
|
43
|
+
|
|
44
|
+
state = ACPSessionState(
|
|
45
|
+
id=session_id,
|
|
46
|
+
cwd=cwd,
|
|
47
|
+
mcp_servers=mcp_servers,
|
|
48
|
+
created_at=time.time(),
|
|
49
|
+
model=model,
|
|
50
|
+
)
|
|
51
|
+
self._sessions[session_id] = state
|
|
52
|
+
logger.info("acp_session_created", extra={"session_id": session_id})
|
|
53
|
+
return state
|
|
54
|
+
|
|
55
|
+
async def load(
|
|
56
|
+
self,
|
|
57
|
+
session_id: str,
|
|
58
|
+
cwd: str,
|
|
59
|
+
mcp_servers: list[McpServerStdio | HttpMcpServer | SseMcpServer],
|
|
60
|
+
model: ModelSelection | None = None,
|
|
61
|
+
) -> ACPSessionState:
|
|
62
|
+
"""Load an existing session via the Gateway API."""
|
|
63
|
+
resp = await self._client.post("/session/resume", json={"session_id": session_id})
|
|
64
|
+
resp.raise_for_status()
|
|
65
|
+
|
|
66
|
+
state = ACPSessionState(
|
|
67
|
+
id=session_id,
|
|
68
|
+
cwd=cwd,
|
|
69
|
+
mcp_servers=mcp_servers,
|
|
70
|
+
created_at=time.time(),
|
|
71
|
+
model=model,
|
|
72
|
+
)
|
|
73
|
+
self._sessions[session_id] = state
|
|
74
|
+
logger.info("acp_session_loaded", extra={"session_id": session_id})
|
|
75
|
+
return state
|
|
76
|
+
|
|
77
|
+
def try_get(self, session_id: str) -> ACPSessionState | None:
|
|
78
|
+
"""Return session state if it exists, else None."""
|
|
79
|
+
return self._sessions.get(session_id)
|
|
80
|
+
|
|
81
|
+
def get(self, session_id: str) -> ACPSessionState:
|
|
82
|
+
"""Return session state or raise ACP RequestError."""
|
|
83
|
+
state = self._sessions.get(session_id)
|
|
84
|
+
if not state:
|
|
85
|
+
raise RequestError(code=-32602, message=f"Session not found: {session_id}")
|
|
86
|
+
return state
|
|
87
|
+
|
|
88
|
+
def set_model(self, session_id: str, model: ModelSelection) -> None:
|
|
89
|
+
state = self.get(session_id)
|
|
90
|
+
state.model = model
|
|
91
|
+
|
|
92
|
+
def get_model(self, session_id: str) -> ModelSelection | None:
|
|
93
|
+
return self.get(session_id).model
|
|
94
|
+
|
|
95
|
+
def set_variant(self, session_id: str, variant: str | None) -> None:
|
|
96
|
+
self.get(session_id).variant = variant
|
|
97
|
+
|
|
98
|
+
def get_variant(self, session_id: str) -> str | None:
|
|
99
|
+
return self.get(session_id).variant
|
|
100
|
+
|
|
101
|
+
def set_mode(self, session_id: str, mode_id: str) -> None:
|
|
102
|
+
self.get(session_id).mode_id = mode_id
|
|
103
|
+
|
|
104
|
+
def get_mode(self, session_id: str) -> str | None:
|
|
105
|
+
return self.get(session_id).mode_id
|
|
106
|
+
|
|
107
|
+
# ── HTTP helpers exposed to Agent ───────────────────────────
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def client(self) -> httpx.AsyncClient:
|
|
111
|
+
return self._client
|
|
112
|
+
|
|
113
|
+
async def aclose(self) -> None:
|
|
114
|
+
await self._client.aclose()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""ACP stdio transport — wires CrabCodeACPAgent to stdin/stdout via ACP SDK.
|
|
2
|
+
|
|
3
|
+
Uses the official `acp.run_agent` convenience function which handles:
|
|
4
|
+
- JSON-RPC framing over ndjson stdio streams
|
|
5
|
+
- Request/response routing
|
|
6
|
+
- Agent-side connection lifecycle
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import acp
|
|
12
|
+
|
|
13
|
+
from crabcode_core.logging_utils import get_logger
|
|
14
|
+
from crabcode_gateway.acp.agent import CrabCodeACPAgent
|
|
15
|
+
from crabcode_gateway.acp.types import ACPConfig
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def run_acp_server(config: ACPConfig) -> None:
|
|
21
|
+
"""Start the ACP agent server on stdio.
|
|
22
|
+
|
|
23
|
+
This function blocks until stdin is closed or a fatal error occurs.
|
|
24
|
+
|
|
25
|
+
Usage::
|
|
26
|
+
|
|
27
|
+
config = ACPConfig(base_url="http://127.0.0.1:4096")
|
|
28
|
+
await run_acp_server(config)
|
|
29
|
+
"""
|
|
30
|
+
agent = CrabCodeACPAgent(config)
|
|
31
|
+
try:
|
|
32
|
+
logger.info("acp_server_starting", extra={"base_url": config.base_url})
|
|
33
|
+
await acp.run_agent(agent)
|
|
34
|
+
except Exception:
|
|
35
|
+
logger.exception("acp_server_error")
|
|
36
|
+
raise
|
|
37
|
+
finally:
|
|
38
|
+
await agent.close()
|
|
39
|
+
logger.info("acp_server_stopped")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""ACP internal types — session state and configuration.
|
|
2
|
+
|
|
3
|
+
Maps CrabCode session/model concepts to ACP's expectations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from acp.schema import McpServerStdio, HttpMcpServer, SseMcpServer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ACPSessionState:
|
|
16
|
+
"""Tracks ACP-specific state for a single session.
|
|
17
|
+
|
|
18
|
+
Stored in ACPSessionManager; not persisted to disk.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
id: str
|
|
22
|
+
cwd: str
|
|
23
|
+
mcp_servers: list[McpServerStdio | HttpMcpServer | SseMcpServer] = field(default_factory=list)
|
|
24
|
+
created_at: float = 0.0
|
|
25
|
+
model: ModelSelection | None = None
|
|
26
|
+
mode_id: str | None = None
|
|
27
|
+
variant: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ModelSelection:
|
|
32
|
+
"""Identifies a provider + model pair."""
|
|
33
|
+
|
|
34
|
+
provider_id: str
|
|
35
|
+
model_id: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ACPConfig:
|
|
40
|
+
"""Top-level configuration passed to the ACP Agent on construction."""
|
|
41
|
+
|
|
42
|
+
base_url: str # e.g. "http://127.0.0.1:4096"
|
|
43
|
+
default_model: ModelSelection | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── Tool kind mapping ──────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def to_tool_kind(tool_name: str) -> str:
|
|
50
|
+
"""Map CrabCode tool names to ACP ToolKind literals.
|
|
51
|
+
|
|
52
|
+
ACP ToolKind: read | edit | delete | move | search | execute |
|
|
53
|
+
think | fetch | switch_mode | other
|
|
54
|
+
"""
|
|
55
|
+
tool = tool_name.lower()
|
|
56
|
+
if tool == "bash":
|
|
57
|
+
return "execute"
|
|
58
|
+
if tool == "web_search":
|
|
59
|
+
return "fetch"
|
|
60
|
+
if tool in ("applypatch", "apply_patch", "edit", "patch", "file_edit", "file_write", "write"):
|
|
61
|
+
return "edit"
|
|
62
|
+
if tool in ("grep", "glob", "toolsearch"):
|
|
63
|
+
return "search"
|
|
64
|
+
if tool == "read" or tool == "file_read":
|
|
65
|
+
return "read"
|
|
66
|
+
if tool == "delete":
|
|
67
|
+
return "delete"
|
|
68
|
+
if tool == "switch_mode":
|
|
69
|
+
return "switch_mode"
|
|
70
|
+
return "other"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def to_locations(tool_name: str, tool_input: dict[str, Any]) -> list[dict[str, str]]:
|
|
74
|
+
"""Extract file paths from tool input for ACP ToolCallLocation."""
|
|
75
|
+
tool = tool_name.lower()
|
|
76
|
+
if tool in ("read", "file_read", "edit", "file_edit", "write", "file_write"):
|
|
77
|
+
path = tool_input.get("filePath") or tool_input.get("file_path") or ""
|
|
78
|
+
return [{"path": path}] if path else []
|
|
79
|
+
if tool in ("applypatch", "apply_patch", "patch"):
|
|
80
|
+
try:
|
|
81
|
+
from crabcode_core.tools.apply_patch import patch_paths
|
|
82
|
+
|
|
83
|
+
paths = tool_input.get("affected_paths") or patch_paths(str(tool_input.get("patch") or ""))
|
|
84
|
+
except (TypeError, ValueError):
|
|
85
|
+
paths = []
|
|
86
|
+
return [{"path": path} for path in paths if isinstance(path, str) and path]
|
|
87
|
+
if tool in ("glob", "grep"):
|
|
88
|
+
path = tool_input.get("path") or ""
|
|
89
|
+
return [{"path": path}] if path else []
|
|
90
|
+
return []
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Protocol adapter abstraction — inspired by OpenCode's adapter.ts.
|
|
2
|
+
|
|
3
|
+
Each concrete adapter (HTTP, gRPC) implements the ProtocolAdapter ABC,
|
|
4
|
+
allowing the server to start/stop multiple protocols uniformly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProtocolAdapter(ABC):
|
|
13
|
+
"""Abstract base class for protocol adapters.
|
|
14
|
+
|
|
15
|
+
Mirrors OpenCode's Adapter interface which abstracts away the
|
|
16
|
+
runtime-specific server creation (Bun vs Node).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def start(self, host: str, port: int) -> None:
|
|
21
|
+
"""Start serving on the given host:port."""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def stop(self) -> None:
|
|
25
|
+
"""Gracefully stop the server."""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def is_running(self) -> bool:
|
|
30
|
+
"""Whether the server is currently running."""
|
crabcode_gateway/auth.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Gateway authentication: password/public-key login and short-lived JWTs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import hmac
|
|
8
|
+
import os
|
|
9
|
+
import secrets
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import jwt
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
18
|
+
from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa
|
|
19
|
+
except ImportError: # pragma: no cover - dependency is declared by gateway
|
|
20
|
+
serialization = None # type: ignore[assignment]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _b64(data: bytes) -> str:
|
|
24
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _unb64(value: str) -> bytes:
|
|
28
|
+
return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def make_jwt(payload: dict[str, Any], secret: str) -> str:
|
|
32
|
+
return jwt.encode(payload, secret, algorithm="HS256")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def decode_jwt(token: str, secret: str) -> dict[str, Any] | None:
|
|
36
|
+
try:
|
|
37
|
+
return jwt.decode(
|
|
38
|
+
token,
|
|
39
|
+
secret,
|
|
40
|
+
algorithms=["HS256"],
|
|
41
|
+
issuer="crabcode-gateway",
|
|
42
|
+
audience="crabcode-api",
|
|
43
|
+
options={"require": ["exp", "iat", "nbf", "iss", "aud", "sub", "jti"]},
|
|
44
|
+
)
|
|
45
|
+
except jwt.PyJWTError:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def hash_password(password: str, *, iterations: int = 310_000) -> str:
|
|
50
|
+
salt = os.urandom(16)
|
|
51
|
+
digest = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations)
|
|
52
|
+
return f"pbkdf2_sha256${iterations}${_b64(salt)}${_b64(digest)}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def verify_password(password: str, encoded: str) -> bool:
|
|
56
|
+
try:
|
|
57
|
+
algorithm, raw_iterations, salt, expected = encoded.split("$", 3)
|
|
58
|
+
if algorithm != "pbkdf2_sha256":
|
|
59
|
+
return False
|
|
60
|
+
iterations = int(raw_iterations)
|
|
61
|
+
if iterations < 100_000 or iterations > 2_000_000:
|
|
62
|
+
return False
|
|
63
|
+
actual = hashlib.pbkdf2_hmac("sha256", password.encode(), _unb64(salt), iterations)
|
|
64
|
+
return hmac.compare_digest(_b64(actual), expected)
|
|
65
|
+
except (ValueError, TypeError):
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _authorized_keys(path: str) -> dict[str, Any]:
|
|
70
|
+
"""Read an OpenSSH authorized_keys file and return id -> public key."""
|
|
71
|
+
keys: dict[str, Any] = {}
|
|
72
|
+
expanded = Path(path).expanduser()
|
|
73
|
+
if serialization is None or not expanded.is_file():
|
|
74
|
+
return keys
|
|
75
|
+
try:
|
|
76
|
+
lines = expanded.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
77
|
+
except OSError:
|
|
78
|
+
return keys
|
|
79
|
+
for line in lines:
|
|
80
|
+
line = line.strip()
|
|
81
|
+
if not line or line.startswith("#"):
|
|
82
|
+
continue
|
|
83
|
+
fields = line.split()
|
|
84
|
+
try:
|
|
85
|
+
idx = next(
|
|
86
|
+
i
|
|
87
|
+
for i, value in enumerate(fields)
|
|
88
|
+
if value.startswith(("ssh-", "ecdsa-", "sk-"))
|
|
89
|
+
)
|
|
90
|
+
key_type, encoded = fields[idx], fields[idx + 1]
|
|
91
|
+
raw = base64.b64decode(encoded, validate=True)
|
|
92
|
+
public_key = serialization.load_ssh_public_key(
|
|
93
|
+
f"{key_type} {encoded}".encode()
|
|
94
|
+
)
|
|
95
|
+
fingerprint = "SHA256:" + _b64(hashlib.sha256(raw).digest())
|
|
96
|
+
comment = " ".join(fields[idx + 2:]).strip()
|
|
97
|
+
key_id = comment or fingerprint
|
|
98
|
+
keys[key_id] = public_key
|
|
99
|
+
keys.setdefault(fingerprint, public_key)
|
|
100
|
+
except (StopIteration, ValueError, IndexError, TypeError):
|
|
101
|
+
continue
|
|
102
|
+
return keys
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def verify_public_signature(path: str, key_id: str, message: bytes, signature_b64: str) -> bool:
|
|
106
|
+
keys = _authorized_keys(path)
|
|
107
|
+
public_key = keys.get(key_id)
|
|
108
|
+
if public_key is None:
|
|
109
|
+
return False
|
|
110
|
+
try:
|
|
111
|
+
signature = base64.b64decode(signature_b64, validate=True)
|
|
112
|
+
if isinstance(public_key, ed25519.Ed25519PublicKey):
|
|
113
|
+
public_key.verify(signature, message)
|
|
114
|
+
elif isinstance(public_key, rsa.RSAPublicKey):
|
|
115
|
+
public_key.verify(signature, message, padding.PKCS1v15(), hashes.SHA256())
|
|
116
|
+
elif isinstance(public_key, ec.EllipticCurvePublicKey):
|
|
117
|
+
public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
|
|
118
|
+
else:
|
|
119
|
+
return False
|
|
120
|
+
return True
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def new_token(*, subject: str, mode: str, secret: str, ttl: int) -> str:
|
|
126
|
+
now = int(time.time())
|
|
127
|
+
return make_jwt(
|
|
128
|
+
{
|
|
129
|
+
"iss": "crabcode-gateway",
|
|
130
|
+
"aud": "crabcode-api",
|
|
131
|
+
"sub": subject,
|
|
132
|
+
"auth_mode": mode,
|
|
133
|
+
"iat": now,
|
|
134
|
+
"nbf": now,
|
|
135
|
+
"exp": now + ttl,
|
|
136
|
+
"jti": secrets.token_hex(16),
|
|
137
|
+
},
|
|
138
|
+
secret,
|
|
139
|
+
)
|