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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
core/engine/atrium/static/assets/*.js whitespace=-trailing-space
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ push-check:
# proxies the extension's data routes; a production build has no vite, so a canvas that has
# only ever been driven under `npm run dev` has never been driven at all.
canvas-build:
cd core/ui/canvas && npx vite build
cd core/ui/canvas && npm run build:package

canvas-host: canvas-build
uv run uvicorn core.engine.api.canvas_host:app --host 127.0.0.1 --port 5173
Expand Down
69 changes: 61 additions & 8 deletions core/engine/api/canvas_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,36 @@
from fastapi.responses import FileResponse, Response
from fastapi.staticfiles import StaticFiles

from core.engine.atrium import static_dir as atrium_static_dir

REPO = Path(__file__).resolve().parents[3]
CANVAS_DIST = REPO / "core" / "ui" / "canvas" / "dist"
CANVAS_DIST = atrium_static_dir()
EXTENSIONS = REPO / "extensions"

#: Prefixes the HOST itself routes. An extension may not shadow one.
#: Deliberately tiny — this server exists to serve the canvas and forward, nothing else.
KERNEL_PREFIXES: tuple[str, ...] = ("/__host_health",)
#: Public Core API prefixes used by the production Atrium bundle. The host
#: forwards these server-side to the configured Core API so every browser call
#: remains same-origin. ``/v1`` is the Intelligence OS boundary; the remaining
#: paths preserve the broader Canvas surface during the migration to Atrium.
CORE_API_PREFIXES: tuple[str, ...] = (
"/v1",
"/auth",
"/health",
"/canvas",
"/proactive",
"/briefings",
"/portal",
"/product",
"/recommendations",
"/decisions",
"/foresight",
"/atc",
"/sentinels",
"/tasks",
"/extension-invocations",
)

#: Prefixes the host or Core itself routes. An extension may not shadow one.
KERNEL_PREFIXES: tuple[str, ...] = ("/__host_health", *CORE_API_PREFIXES)


class ProxyCollisionError(Exception):
Expand Down Expand Up @@ -178,6 +201,9 @@ def create_app(
dist: Path = CANVAS_DIST,
extensions_root: Path = EXTENSIONS,
env: dict[str, str] | None = None,
core_api_url: str | None = None,
access_token: str | None = None,
transport: httpx.AsyncBaseTransport | None = None,
) -> FastAPI:
env = dict(os.environ) if env is None else env

Expand All @@ -188,6 +214,7 @@ def create_app(
app = FastAPI(title="ACE Canvas Host", docs_url=None, redoc_url=None)
app.state.proxies = proxies
app.state.dist = dist
app.state.core_api_url = core_api_url

@app.get("/__host_health")
async def host_health() -> dict[str, Any]:
Expand All @@ -197,22 +224,33 @@ async def host_health() -> dict[str, Any]:
return {
"ok": True,
"canvas_built": dist.is_dir(),
"core_api_configured": core_api_url is not None,
"proxies": proxies,
}

client = httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=5.0), follow_redirects=False)
client = httpx.AsyncClient(
timeout=httpx.Timeout(30.0, connect=5.0),
follow_redirects=False,
transport=transport,
)

@app.on_event("shutdown")
async def _close() -> None:
await client.aclose()

def _install(prefix: str, target: str) -> None:
def _install(prefix: str, target: str, *, unavailable: str = "data plane unreachable") -> None:
@app.api_route(
f"{prefix}/{{path:path}}",
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
include_in_schema=False,
)
async def _proxy(request: Request, path: str, _t: str = target, _p: str = prefix) -> Response:
async def _proxy(
request: Request,
path: str,
_t: str = target,
_p: str = prefix,
_unavailable: str = unavailable,
) -> Response:
url = httpx.URL(f"{_t}{_p}/{path}").copy_with(query=request.url.query.encode())
headers = {k: v for k, v in request.headers.items() if k.lower() not in _DROP}
try:
Expand All @@ -222,7 +260,7 @@ async def _proxy(request: Request, path: str, _t: str = target, _p: str = prefix
# 500 — is how "the data plane is down" gets misdiagnosed as "the canvas is
# broken", which is the entire reason the proxy exists rather than CORS.
return Response(
content=json.dumps({"error": "data plane unreachable", "target": _t, "detail": str(exc)}),
content=json.dumps({"error": _unavailable, "target": _t, "detail": str(exc)}),
status_code=502,
media_type="application/json",
)
Expand All @@ -232,6 +270,21 @@ async def _proxy(request: Request, path: str, _t: str = target, _p: str = prefix
headers={k: v for k, v in upstream.headers.items() if k.lower() not in _DROP},
)

# A packaged browser bundle cannot embed a user's API key. The local host
# instead returns the already-issued CLI bearer token to this same-origin
# page. It is kept in memory by the page and never written to browser
# storage. With no token configured, /auth is forwarded normally.
if access_token is not None:

@app.post("/auth/token", include_in_schema=False)
async def atrium_token() -> dict[str, str]:
return {"token": access_token}

if core_api_url is not None:
target = core_api_url.rstrip("/")
for prefix in CORE_API_PREFIXES:
_install(prefix, target, unavailable="Core API unreachable")

for prefix, target in proxies.items():
_install(prefix, target)

Expand Down
27 changes: 27 additions & 0 deletions core/engine/atrium/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Packaged Atrium application assets.

Atrium is the domain-neutral command center for an ACE installation. Its
production bundle ships with ``ace-core`` so a user can open the application
without a JavaScript toolchain or a source checkout.
"""

from __future__ import annotations

from importlib.resources import files
from pathlib import Path


def static_dir() -> Path:
"""Return the installed filesystem directory containing the Atrium bundle."""

resource = files(__package__).joinpath("static")
# Wheels are installed as ordinary files by supported Python installers.
# Serving an SPA requires a filesystem path, so deliberately fail clearly
# for exotic zip-import loaders instead of extracting mutable assets.
try:
return Path(resource)
except TypeError as exc: # pragma: no cover - standard wheel installs are filesystem-backed
raise RuntimeError("Atrium assets require a filesystem-backed ace-core installation") from exc


__all__ = ["static_dir"]
787 changes: 787 additions & 0 deletions core/engine/atrium/static/assets/index-BaLqTrAb.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions core/engine/atrium/static/assets/index-CZdqexKr.css

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Empty file.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions core/engine/atrium/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ACE</title>
<link rel="icon" type="image/png" sizes="32x32" href="/brand/ace_logo_fixed_32.png" />
<link rel="icon" type="image/png" sizes="64x64" href="/brand/ace_logo_fixed_64.png" />
<link rel="icon" type="image/png" sizes="128x128" href="/brand/ace_logo_fixed_128.png" />
<link rel="icon" type="image/png" sizes="256x256" href="/brand/ace_logo_fixed_256.png" />
<link rel="apple-touch-icon" sizes="512x512" href="/brand/ace_logo_fixed_512.png" />
<script type="module" crossorigin src="/assets/index-BaLqTrAb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CZdqexKr.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
28 changes: 28 additions & 0 deletions core/engine/atrium/static/live-brain.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"beliefs": [
{
"belief": "Pricing is the #1 objection for enterprise buyers",
"prior_confidence": 0.82,
"proposed_confidence": 0.15,
"still_supported": false,
"rationale": "The evidence directly contradicts the belief by showing pricing was never raised in fresh buyer interviews, while security compliance and integration risk were identified as the actual blockers.",
"ground": "Fresh buyer interviews: pricing was never raised; security compliance and integration risk were the real blockers."
},
{
"belief": "The homepage hero should lead with outcomes, not price",
"prior_confidence": 0.58,
"proposed_confidence": 0.82,
"still_supported": true,
"rationale": "Q2 A/B test data directly shows outcomes-led hero outperformed price-led hero by 34% on qualified-demo conversion, providing strong empirical validation of the belief.",
"ground": "Q2 A/B test: the outcomes-led hero beat the price-led hero on qualified-demo conversion by 34%."
},
{
"belief": "A Q3 launch window is optimal for the repositioning",
"prior_confidence": 0.7,
"proposed_confidence": 0.25,
"still_supported": false,
"rationale": "Launching simultaneously with a primary competitor's directly-overlapping repositioning significantly undermines optimality by creating market saturation and reducing differentiation.",
"ground": "Competitive intel: the primary competitor is announcing a directly-overlapping repositioning the same week in Q3."
}
]
}
47 changes: 47 additions & 0 deletions core/engine/cli/commands/atrium.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Launch the installed Atrium command center."""

from __future__ import annotations

import click
import uvicorn

from core.engine.api.canvas_host import create_app
from core.engine.atrium import static_dir


@click.command("atrium")
@click.option("--port", type=click.IntRange(1, 65535), default=5173, show_default=True)
@click.option("--open/--no-open", "open_browser", default=True, show_default=True)
@click.pass_context
def atrium(ctx: click.Context, port: int, open_browser: bool) -> None:
"""Open Atrium, the personal ACE Intelligence command center.

The ACE API must already be running (``ace service start``). Atrium is
served from this installed package and forwards API calls same-origin, so
neither a source checkout nor a JavaScript development server is required.
"""

api_url = str(ctx.obj.get("url", "")).rstrip("/")
token = ctx.obj.get("token")
if not api_url:
raise click.ClickException("No ACE API URL is configured. Run `ace setup` first.")
if not token:
raise click.ClickException("No ACE login is available. Run `ace setup` or `ace login`, then retry.")

assets = static_dir()
if not (assets / "index.html").is_file():
raise click.ClickException(
"This ace-core installation does not contain Atrium assets. Reinstall the release package and retry."
)

app = create_app(
dist=assets,
core_api_url=api_url,
access_token=str(token),
)
address = f"http://127.0.0.1:{port}/atrium"
click.echo(f"Atrium is available at {address}")
click.echo(f"Forwarding its ACE requests to {api_url}")
if open_browser:
click.launch(address)
uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")
2 changes: 2 additions & 0 deletions core/engine/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def cli(ctx, url):

# Import and register command groups
from core.engine.cli.commands.assertions import assertion
from core.engine.cli.commands.atrium import atrium
from core.engine.cli.commands.briefing import briefing
from core.engine.cli.commands.cognition import cognition
from core.engine.cli.commands.conflicts import conflicts
Expand All @@ -43,6 +44,7 @@ def cli(ctx, url):
from core.engine.cli.commands.templates import templates

cli.add_command(login)
cli.add_command(atrium)
cli.add_command(assertion)
cli.add_command(run)
cli.add_command(quick)
Expand Down
1 change: 1 addition & 0 deletions core/ui/canvas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:package": "tsc && vite build --outDir ../../engine/atrium/static --emptyOutDir",
"build:naked": "node scripts/build-naked.mjs",
"preview": "vite preview",
"test": "vitest run",
Expand Down
5 changes: 5 additions & 0 deletions docs/design/core-engine-compatibility-disposition-v0.8.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
"treatment": "explicitly enabled executable compatibility surface; never Domain Pack data or implicit authority",
"packages": ["extensions", "generation", "github", "playbooks", "runner", "templates"]
},
"installed_product_application": {
"owner": "application",
"treatment": "installed optional product surface over public ACE services; never a source of truth or authority",
"packages": ["atrium"]
},
"legacy_product_application": {
"owner": "application",
"treatment": "frozen product-era compatibility surface; no new canonical callers or category language",
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ include = ["ace", "ace.*", "ace_mcp_client*", "core*", "extensions*", "scripts*"
"core.engine.cognition.recipes" = ["*.yaml"]
"core.engine.generation" = ["templates/*.j2"]
"core.engine.reports" = ["templates/*.html", "templates/*.css", "static/*.js"]
"core.engine.atrium" = ["static/index.html", "static/*.json", "static/assets/*", "static/brand/*"]
"evaluations" = ["*.md", "fixtures/*.json", "results/*.json", "results/*.md"]

[tool.setuptools.data-files]
Expand Down
55 changes: 55 additions & 0 deletions tests/test_atrium_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Checkout-free Atrium package and CLI contract."""

from pathlib import Path

from click.testing import CliRunner

from core.engine.atrium import static_dir
from core.engine.cli.commands import atrium as atrium_module


def test_packaged_atrium_has_an_entrypoint_and_hashed_assets() -> None:
assets = static_dir()

assert (assets / "index.html").is_file()
assert any((assets / "assets").glob("*.js"))
assert any((assets / "assets").glob("*.css"))


def test_atrium_command_serves_packaged_assets_and_the_configured_api(monkeypatch) -> None:
launched: dict[str, object] = {}

def run(app, **kwargs):
launched.update({"app": app, **kwargs})

monkeypatch.setattr(atrium_module.uvicorn, "run", run)
result = CliRunner().invoke(
atrium_module.atrium,
["--no-open", "--port", "6123"],
obj={"url": "http://127.0.0.1:3000", "token": "test-token"},
)

assert result.exit_code == 0, result.output
assert "http://127.0.0.1:6123/atrium" in result.output
assert launched["host"] == "127.0.0.1"
assert launched["port"] == 6123
assert launched["app"].state.dist == static_dir()
assert launched["app"].state.core_api_url == "http://127.0.0.1:3000"


def test_atrium_command_refuses_to_serve_without_a_login() -> None:
result = CliRunner().invoke(
atrium_module.atrium,
["--no-open"],
obj={"url": "http://127.0.0.1:3000", "token": None},
)

assert result.exit_code != 0
assert "ace setup" in result.output


def test_pyproject_declares_atrium_package_data() -> None:
pyproject = (Path(__file__).resolve().parents[1] / "pyproject.toml").read_text()

assert '"core.engine.atrium"' in pyproject
assert '"static/index.html"' in pyproject
Loading