Update app/main.py
This commit is contained in:
+55
-12
@@ -1,14 +1,17 @@
|
||||
"""coreci-executor v0.1.1 — governed write rail (Design v1.0, approved 2026-10-03).
|
||||
"""coreci-executor v0.1.3 — governed write rail (Design v1.0, approved 2026-10-03).
|
||||
|
||||
Tool endpoints (X-Executor-Key auth, for Open WebUI Global Tool Server):
|
||||
Tool endpoints (Authorization: Bearer <EXECUTOR_API_KEY> or X-Executor-Key):
|
||||
POST /submit_request {agent, kind, targets[], content, justification}
|
||||
GET /request_status?request_id=...
|
||||
GET /health
|
||||
Web UI (passcode session): / (queue), /ui/request/{id}, /ui/audit,
|
||||
/ui/login, POST /ui/request/{id}/approve|deny.
|
||||
Web UI (passcode session — EXECUTOR_PASSCODE): / (queue), /ui/request/{id},
|
||||
/ui/audit, /ui/login, POST /ui/request/{id}/approve|deny.
|
||||
Policy: allowlist.yaml (fail-closed NEEDS_APPROVAL; AUTO_RUN for agent's own
|
||||
home-repo packs). Packs must be credential-free; auth is injected at run time.
|
||||
Audit: JSONL to logs/ (+ optional Discord webhook). Secrets stay in env.
|
||||
|
||||
v0.1.2: CORS for browser-origin schema fetch; Bearer auth support (Open WebUI).
|
||||
v0.1.3: never-500 login; OpenAPI securitySchemes (Swagger Authorize button).
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
@@ -26,6 +29,8 @@ from pathlib import Path
|
||||
import httpx
|
||||
import yaml
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
APP_DIR = Path(__file__).resolve().parent
|
||||
@@ -45,7 +50,14 @@ TIMEOUT_S = int(ENV.get("REQUEST_TIMEOUT_S", "900"))
|
||||
COOKIE_NAME = "exec_session"
|
||||
CRED_RE = re.compile(r"https?://[^/\s@]+:[^@\s]+@")
|
||||
|
||||
app = FastAPI(title="coreci-executor", version="0.1.1")
|
||||
app = FastAPI(title="coreci-executor", version="0.1.3")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["https://chat.coreci.dev"], # add other origins you use for Open WebUI here
|
||||
allow_methods=["GET", "POST", "OPTIONS"],
|
||||
allow_headers=["Authorization", "Content-Type", "X-Executor-Key"],
|
||||
)
|
||||
|
||||
REQS: dict[str, dict] = {}
|
||||
LOCK = threading.Lock()
|
||||
|
||||
@@ -107,6 +119,12 @@ def run_pack(rid: str) -> None:
|
||||
pass
|
||||
shutil.rmtree(home, ignore_errors=True)
|
||||
|
||||
def key_ok(request: Request) -> bool:
|
||||
"""Accept Authorization: Bearer <key> (Open WebUI) OR X-Executor-Key: <key>."""
|
||||
auth = request.headers.get("authorization", "")
|
||||
return (request.headers.get("X-Executor-Key") == API_KEY
|
||||
or (auth.startswith("Bearer ") and auth[7:] == API_KEY))
|
||||
|
||||
def session_ok(request: Request) -> bool:
|
||||
return request.cookies.get(COOKIE_NAME) == hashlib.sha256(PASSCODE.encode()).hexdigest()
|
||||
|
||||
@@ -119,12 +137,12 @@ def page(title: str, body: str) -> str:
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "version": "0.1.1", "requests": len(REQS)}
|
||||
return {"status": "ok", "version": "0.1.3", "requests": len(REQS)}
|
||||
|
||||
@app.post("/submit_request")
|
||||
async def submit(request: Request):
|
||||
if request.headers.get("X-Executor-Key") != API_KEY:
|
||||
raise HTTPException(401, "bad or missing X-Executor-Key")
|
||||
if not key_ok(request):
|
||||
raise HTTPException(401, "bad or missing API key")
|
||||
d = await request.json()
|
||||
agent, kind = d.get("agent", ""), d.get("kind", "bash_pack")
|
||||
targets = d.get("targets") or []
|
||||
@@ -151,7 +169,7 @@ async def submit(request: Request):
|
||||
|
||||
@app.get("/request_status")
|
||||
def status(request_id: str, request: Request):
|
||||
if request.headers.get("X-Executor-Key") != API_KEY and not session_ok(request):
|
||||
if not key_ok(request) and not session_ok(request):
|
||||
raise HTTPException(401, "unauthorized")
|
||||
r = get_req(request_id)
|
||||
return {k: r.get(k) for k in ("id", "agent", "kind", "targets", "decision",
|
||||
@@ -177,9 +195,16 @@ def login():
|
||||
|
||||
@app.post("/ui/login")
|
||||
async def login_post(request: Request):
|
||||
form = await request.form()
|
||||
if form.get("p") != PASSCODE:
|
||||
raise HTTPException(401, "wrong passcode")
|
||||
pw = None
|
||||
try:
|
||||
pw = (await request.form()).get("p")
|
||||
except Exception:
|
||||
try:
|
||||
pw = (json.loads(await request.body()) or {}).get("p")
|
||||
except Exception:
|
||||
pw = None
|
||||
if pw != PASSCODE:
|
||||
raise HTTPException(401, "wrong passcode — the web UI takes EXECUTOR_PASSCODE from .env, not the API key")
|
||||
resp = RedirectResponse("/", status_code=303)
|
||||
resp.set_cookie(COOKIE_NAME, hashlib.sha256(PASSCODE.encode()).hexdigest(),
|
||||
httponly=True, samesite="strict")
|
||||
@@ -225,3 +250,21 @@ def audit_view(request: Request):
|
||||
f = AUDIT_DIR / f"audit-{datetime.now(timezone.utc):%Y-%m-%d}.jsonl"
|
||||
txt = f.read_text()[-20000:] if f.exists() else "(empty)"
|
||||
return HTMLResponse(page("audit", f"<h1>audit — today</h1><pre>{esc(txt)}</pre>"))
|
||||
|
||||
def custom_openapi():
|
||||
if app.openapi_schema:
|
||||
return app.openapi_schema
|
||||
schema = get_openapi(title=app.title, version=app.version, routes=app.routes)
|
||||
schema.setdefault("components", {})["securitySchemes"] = {
|
||||
"bearerAuth": {"type": "http", "scheme": "bearer",
|
||||
"description": "EXECUTOR_API_KEY as Authorization: Bearer <key>"},
|
||||
"executorKey": {"type": "apiKey", "in": "header", "name": "X-Executor-Key",
|
||||
"description": "EXECUTOR_API_KEY via X-Executor-Key header"},
|
||||
}
|
||||
for _p in ("/submit_request", "/request_status"):
|
||||
for _m in schema.get("paths", {}).get(_p, {}).values():
|
||||
_m.setdefault("security", [{"bearerAuth": []}, {"executorKey": []}])
|
||||
app.openapi_schema = schema
|
||||
return schema
|
||||
|
||||
app.openapi = custom_openapi
|
||||
Reference in New Issue
Block a user