Add a service-token bypass for code-forge, gated on auth being enabled
code-forge (the stdio MCP server) calls the REST API directly and has no login flow of its own. With auth disabled it already passes through as guest, same as before - but the moment CLAUDE_OS_EMAIL/ PASSWORD get set, it would start getting 401'd with no way to authenticate. get_current_user now accepts a CLAUDE_OS_SERVICE_TOKEN as a bearer token, checked with a constant-time compare, alongside normal user JWTs. claude_code_mcp.py sends it (via a shared _auth_headers() used by all 5 of its httpx.AsyncClient() call sites) whenever CLAUDE_OS_SERVICE_TOKEN is set, and sends nothing when it isn't - a no-op change for anyone not using auth. Since this process can be launched directly (e.g. over SSH as a stdio server) without claude-os's own .env sourced into its shell or its cwd set to the project root, it now loads .env explicitly by path relative to the script's own location rather than relying on load_dotenv()'s cwd-relative default search.
This commit is contained in:
parent
9298de1a49
commit
e374440c67
2 changed files with 29 additions and 5 deletions
|
|
@ -125,6 +125,15 @@ def get_current_user(credentials: Optional[HTTPAuthorizationCredentials] = Secur
|
|||
)
|
||||
|
||||
token = credentials.credentials
|
||||
|
||||
# Service token: lets trusted local integrations (e.g. the code-forge
|
||||
# stdio MCP server) call the API without a user login. Only relevant
|
||||
# once auth is actually enabled - with no CLAUDE_OS_EMAIL set, everyone
|
||||
# already gets the guest bypass above regardless of this.
|
||||
service_token = os.getenv("CLAUDE_OS_SERVICE_TOKEN")
|
||||
if service_token and secrets.compare_digest(token, service_token):
|
||||
return {"email": "service", "service_account": True}
|
||||
|
||||
payload = decode_access_token(token)
|
||||
|
||||
if payload is None:
|
||||
|
|
|
|||
|
|
@ -32,12 +32,27 @@ from pathlib import Path
|
|||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import Tool, TextContent
|
||||
|
||||
# This process is launched directly (e.g. over SSH as a stdio MCP server),
|
||||
# not necessarily with claude-os's own .env sourced into its environment or
|
||||
# its working directory set to the project root - so load it explicitly by
|
||||
# path rather than relying on load_dotenv()'s cwd-relative default search.
|
||||
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
|
||||
|
||||
# Configuration
|
||||
API_BASE = os.environ.get("CLAUDE_OS_API", "http://localhost:8051")
|
||||
SERVICE_TOKEN = os.environ.get("CLAUDE_OS_SERVICE_TOKEN")
|
||||
|
||||
|
||||
def _auth_headers() -> dict:
|
||||
"""Auth header for calling claude-os's own API. No-op unless a service
|
||||
token is configured, which only matters once CLAUDE_OS_EMAIL/PASSWORD
|
||||
are set - the API allows all requests through as a guest otherwise."""
|
||||
return {"Authorization": f"Bearer {SERVICE_TOKEN}"} if SERVICE_TOKEN else {}
|
||||
|
||||
# Health cache — tracks last health check time per KB and any warnings
|
||||
HEALTH_CACHE: dict[str, dict] = {}
|
||||
|
|
@ -78,7 +93,7 @@ async def _maybe_check_health(kb_name: str) -> list[str]:
|
|||
|
||||
# Run health check
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(headers=_auth_headers()) as client:
|
||||
resp = await client.get(api_url(f"/api/kb/{kb_name}/lifecycle/health"), timeout=15.0)
|
||||
resp.raise_for_status()
|
||||
health = resp.json()
|
||||
|
|
@ -109,7 +124,7 @@ def api_url(path: str) -> str:
|
|||
|
||||
async def api_get(path: str) -> dict:
|
||||
"""Make GET request to API."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(headers=_auth_headers()) as client:
|
||||
response = await client.get(api_url(path), timeout=30.0)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
|
@ -117,7 +132,7 @@ async def api_get(path: str) -> dict:
|
|||
|
||||
async def api_post(path: str, data: dict = None) -> dict:
|
||||
"""Make POST request to API."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(headers=_auth_headers()) as client:
|
||||
response = await client.post(api_url(path), json=data or {}, timeout=60.0)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
|
@ -125,7 +140,7 @@ async def api_post(path: str, data: dict = None) -> dict:
|
|||
|
||||
async def api_delete(path: str) -> dict:
|
||||
"""Make DELETE request to API."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(headers=_auth_headers()) as client:
|
||||
response = await client.delete(api_url(path), timeout=30.0)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
|
@ -133,7 +148,7 @@ async def api_delete(path: str) -> dict:
|
|||
|
||||
async def api_put(path: str, data: dict = None) -> dict:
|
||||
"""Make PUT request to API."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(headers=_auth_headers()) as client:
|
||||
response = await client.put(api_url(path), json=data or {}, timeout=30.0)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue