import asyncio from contextlib import asynccontextmanager from typing import Annotated import uvicorn from fastapi import Depends, FastAPI, Header, HTTPException from agent_platform.auth import UserIdentity, decode_openwebui_identity, verify_service_bearer from agent_platform.config import Settings, get_settings from agent_platform.gateway.provider import ExecutionProvider, create_execution_provider from agent_platform.gateway.schemas import ( ApplyPatchRequest, ExecRequest, GitDiffRequest, GitRequest, ListFilesRequest, ProcessRequest, ReadFileRequest, SearchFilesRequest, StartProcessRequest, ToolResult, WorkspaceStatus, WriteFileRequest, ) def create_app( settings: Settings | None = None, provider: ExecutionProvider | None = None, ) -> FastAPI: settings = settings or get_settings() @asynccontextmanager async def lifespan(app: FastAPI): settings.validate_gateway() app.state.provider = provider or create_execution_provider(settings) yield app = FastAPI( title="K1412 Workspace Gateway", description="Authenticated tools for one isolated workspace per user.", version="0.1.0", lifespan=lifespan, ) mutation_locks: dict[str, asyncio.Lock] = {} mutation_locks_guard = asyncio.Lock() def require_identity( authorization: Annotated[ str | None, Header(alias="Authorization", include_in_schema=False), ] = None, user_jwt: Annotated[ str | None, Header(alias="X-OpenWebUI-User-Jwt", include_in_schema=False), ] = None, ) -> UserIdentity: verify_service_bearer(authorization, settings.internal_gateway_key) return decode_openwebui_identity(user_jwt, settings.openwebui_forward_jwt_secret) Identity = Annotated[UserIdentity, Depends(require_identity)] def executor() -> ExecutionProvider: return app.state.provider def translate_value_error(exc: ValueError) -> HTTPException: return HTTPException(status_code=400, detail=str(exc)) async def mutation_lock(user_id: str) -> asyncio.Lock: async with mutation_locks_guard: return mutation_locks.setdefault(user_id, asyncio.Lock()) @app.get("/health", include_in_schema=False) async def health() -> dict: return {"status": "ok", "provider": settings.execution_provider} @app.post("/v1/tools/workspace_status", response_model=WorkspaceStatus, operation_id="workspace_status") async def workspace_status(identity: Identity) -> WorkspaceStatus: return await executor().status(identity.user_id) @app.post("/v1/tools/list_files", response_model=ToolResult, operation_id="list_files") async def list_files(body: ListFilesRequest, identity: Identity) -> ToolResult: try: return await executor().list_files(identity.user_id, body.path, body.max_depth, body.limit) except ValueError as exc: raise translate_value_error(exc) from exc @app.post("/v1/tools/read_file", response_model=ToolResult, operation_id="read_file") async def read_file(body: ReadFileRequest, identity: Identity) -> ToolResult: try: return await executor().read_file(identity.user_id, body.path, body.start_line, body.max_lines) except ValueError as exc: raise translate_value_error(exc) from exc @app.post("/v1/tools/write_file", response_model=ToolResult, operation_id="write_file") async def write_file(body: WriteFileRequest, identity: Identity) -> ToolResult: try: lock = await mutation_lock(identity.user_id) async with lock: return await executor().write_file(identity.user_id, body.path, body.content) except ValueError as exc: raise translate_value_error(exc) from exc @app.post("/v1/tools/search_files", response_model=ToolResult, operation_id="search_files") async def search_files(body: SearchFilesRequest, identity: Identity) -> ToolResult: try: return await executor().search_files( identity.user_id, body.query, body.path, body.glob, body.limit, ) except ValueError as exc: raise translate_value_error(exc) from exc @app.post("/v1/tools/exec", response_model=ToolResult, operation_id="exec") async def exec_command(body: ExecRequest, identity: Identity) -> ToolResult: try: lock = await mutation_lock(identity.user_id) async with lock: return await executor().exec( identity.user_id, body.command, body.cwd, min(body.timeout_seconds, settings.tool_timeout_seconds), ) except ValueError as exc: raise translate_value_error(exc) from exc @app.post("/v1/tools/apply_patch", response_model=ToolResult, operation_id="apply_patch") async def apply_patch(body: ApplyPatchRequest, identity: Identity) -> ToolResult: try: lock = await mutation_lock(identity.user_id) async with lock: return await executor().apply_patch(identity.user_id, body.patch, body.cwd) except ValueError as exc: raise translate_value_error(exc) from exc @app.post("/v1/tools/git_status", response_model=ToolResult, operation_id="git_status") async def git_status(body: GitRequest, identity: Identity) -> ToolResult: try: return await executor().exec(identity.user_id, "git status --short --branch", body.cwd, 30) except ValueError as exc: raise translate_value_error(exc) from exc @app.post("/v1/tools/git_diff", response_model=ToolResult, operation_id="git_diff") async def git_diff(body: GitDiffRequest, identity: Identity) -> ToolResult: command = "git diff --cached" if body.staged else "git diff" try: return await executor().exec(identity.user_id, command, body.cwd, 30) except ValueError as exc: raise translate_value_error(exc) from exc @app.post("/v1/tools/start_process", response_model=ToolResult, operation_id="start_process") async def start_process(body: StartProcessRequest, identity: Identity) -> ToolResult: try: lock = await mutation_lock(identity.user_id) async with lock: return await executor().start_process(identity.user_id, body.command, body.cwd) except ValueError as exc: raise translate_value_error(exc) from exc @app.post("/v1/tools/poll_process", response_model=ToolResult, operation_id="poll_process") async def poll_process(body: ProcessRequest, identity: Identity) -> ToolResult: return await executor().poll_process(identity.user_id, body.process_id) @app.post("/v1/tools/cancel_process", response_model=ToolResult, operation_id="cancel_process") async def cancel_process(body: ProcessRequest, identity: Identity) -> ToolResult: lock = await mutation_lock(identity.user_id) async with lock: return await executor().cancel_process(identity.user_id, body.process_id) return app app = create_app() def run() -> None: uvicorn.run("agent_platform.gateway.app:app", host="0.0.0.0", port=8001) # noqa: S104 if __name__ == "__main__": run()