Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ AUTOCONTEXT_ALLOW_PRIMEINTELLECT_FALLBACK=true
# Local sandbox behavior
AUTOCONTEXT_LOCAL_SANDBOX_HARDENED=true

# API server (autoctx serve / autoctx tui)
# Comma-separated origins allowed to call the HTTP API cross-origin.
# Defaults cover local GUI clients (cowork desktop and dev servers);
# set this for remote or custom GUI deployments.
AUTOCONTEXT_CORS_ORIGINS=http://localhost:1420,http://localhost:4173,http://localhost:3000,tauri://localhost

# Optional browser exploration (secure defaults)
AUTOCONTEXT_BROWSER_ENABLED=false
AUTOCONTEXT_BROWSER_BACKEND=chrome-cdp
Expand Down
2 changes: 1 addition & 1 deletion autocontext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ Start the API server:
uv run autoctx serve --host 127.0.0.1 --port 8000
```

Inspect `http://127.0.0.1:8000/` for the API index after the server starts. For an interactive terminal UI, use the TypeScript package: `npx autoctx tui`.
Inspect `http://127.0.0.1:8000/` for the API index after the server starts. Browser-based GUI clients calling the HTTP API cross-origin are allowed via CORS for local app origins by default (the cowork desktop webview and dev servers); set `AUTOCONTEXT_CORS_ORIGINS` to a comma-separated origin list for remote or custom GUI deployments. For an interactive terminal UI, use the TypeScript package: `npx autoctx tui`.

Run a persistent queue worker beside the API server:

Expand Down
19 changes: 19 additions & 0 deletions autocontext/src/autocontext/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import asyncio
import logging
import os
from pathlib import Path
from typing import Any

from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from pydantic import ValidationError

from autocontext.config import load_settings
Expand Down Expand Up @@ -101,6 +103,23 @@ def create_app(
) -> FastAPI:
"""Factory that creates the FastAPI app, optionally wired to a LoopController."""
application = FastAPI(title="autocontext API", version="0.1.0")
# Local GUI clients (cowork desktop webview, browser dev servers) call the
# HTTP API cross-origin. The engine binds to localhost, so allowing
# explicit local-app origins is safe; override via AUTOCONTEXT_CORS_ORIGINS.
cors_origins = [
origin.strip()
for origin in os.environ.get(
"AUTOCONTEXT_CORS_ORIGINS",
"http://localhost:1420,http://localhost:4173,http://localhost:3000,tauri://localhost",
).split(",")
if origin.strip()
]
application.add_middleware(
CORSMiddleware,
allow_origins=cors_origins,
allow_methods=["GET", "PUT", "POST"],
allow_headers=["content-type"],
)
application.include_router(cockpit_router)
application.include_router(hub_router)
application.include_router(knowledge_router)
Expand Down
54 changes: 54 additions & 0 deletions autocontext/tests/test_server_cors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""CORS for local GUI clients (cowork desktop webview, browser dev servers)."""

from pathlib import Path

import pytest
from fastapi.testclient import TestClient

from autocontext.server.app import create_app


@pytest.fixture()
def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient:
monkeypatch.setenv("AUTOCONTEXT_KNOWLEDGE_ROOT", str(tmp_path / "knowledge"))
monkeypatch.setenv("AUTOCONTEXT_DB_PATH", str(tmp_path / "runs.sqlite3"))
monkeypatch.setenv("AUTOCONTEXT_AGENT_PROVIDER", "deterministic")
return TestClient(create_app())


def test_get_allows_local_gui_origin(client: TestClient) -> None:
response = client.get("/api/runs", headers={"Origin": "http://localhost:1420"})
assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == "http://localhost:1420"


def test_put_preflight_allows_tauri_origin(client: TestClient) -> None:
response = client.options(
"/api/knowledge/grid_ctf",
headers={
"Origin": "tauri://localhost",
"Access-Control-Request-Method": "PUT",
"Access-Control-Request-Headers": "content-type",
},
)
assert response.status_code == 200
assert response.headers["access-control-allow-origin"] == "tauri://localhost"
assert "PUT" in response.headers["access-control-allow-methods"]


def test_unknown_origin_gets_no_cors_headers(client: TestClient) -> None:
response = client.get("/api/runs", headers={"Origin": "https://evil.example"})
assert response.status_code == 200
assert "access-control-allow-origin" not in response.headers


def test_origins_overridable_via_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("AUTOCONTEXT_KNOWLEDGE_ROOT", str(tmp_path / "knowledge"))
monkeypatch.setenv("AUTOCONTEXT_DB_PATH", str(tmp_path / "runs.sqlite3"))
monkeypatch.setenv("AUTOCONTEXT_AGENT_PROVIDER", "deterministic")
monkeypatch.setenv("AUTOCONTEXT_CORS_ORIGINS", "http://localhost:9999")
client = TestClient(create_app())
allowed = client.get("/api/runs", headers={"Origin": "http://localhost:9999"})
assert allowed.headers["access-control-allow-origin"] == "http://localhost:9999"
default_gone = client.get("/api/runs", headers={"Origin": "http://localhost:1420"})
assert "access-control-allow-origin" not in default_gone.headers