Skip to content

Commit 9959cd5

Browse files
�[38;5;243m 1�[0m �[38;5;249mfix(deploy): make scaffolded main.py app fully-featured for uvicorn parity with agentomatic run�[0m
�[38;5;243m 2�[0m �[38;5;243m 3�[0m �[38;5;249m- main.py now builds an env-driven, fully-featured module-level `app` so a�[0m �[38;5;243m 4�[0m �[38;5;249m deployed container (`uvicorn main:app`) serves the same surface as�[0m �[38;5;243m 5�[0m �[38;5;249m `agentomatic run`: discovers all component dirs and enables Studio, docs,�[0m �[38;5;243m 6�[0m �[38;5;249m health, and metrics by default�[0m �[38;5;243m 7�[0m �[38;5;249m- feature flags read from AGENTOMATIC_* env vars (ENABLE_STUDIO, ENABLE_METRICS,�[0m �[38;5;243m 8�[0m �[38;5;249m ENABLE_AUTH, ENABLE_JWT, REQUIRE_AUTH -> implies JWT + zero-trust,�[0m �[38;5;243m 9�[0m �[38;5;249m ENABLE_CONTROL_PLANE, ENABLE_RATE_LIMIT, TITLE, LOG_LEVEL) so the same file�[0m �[38;5;243m 10�[0m �[38;5;249m works in dev and in the container without code edits�[0m �[38;5;243m 11�[0m �[38;5;249m- .env.example documents the deploy feature flags�[0m �[38;5;243m 12�[0m �[38;5;249m- deployment guide + 1.2.1 changelog (docs/changelog.md + CHANGELOG.md) note�[0m �[38;5;243m 13�[0m �[38;5;249m the uvicorn main:app parity�[0m �[38;5;243m 14�[0m �[38;5;249m- new parity tests: build the scaffolded app and assert run-equivalent routes�[0m �[38;5;243m 15�[0m �[38;5;249m (/health, /readiness, /docs, /openapi.json, /, /studio); env flags drive�[0m �[38;5;243m 16�[0m �[38;5;249m features (studio toggle + title)�[0m �[38;5;243m 17�[0m �[38;5;243m 18�[0m �[38;5;249mruff/format/mypy/pytest(1187)/mkdocs --strict/uv build all green.�[0m Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent aef34ef commit 9959cd5

5 files changed

Lines changed: 140 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ Full detail lives in [`docs/changelog.md`](docs/changelog.md); highlights:
4040
skipped run is observable
4141
- CORS: wildcard origins auto-disable `allow_credentials` (safer default);
4242
configure explicit `cors_origins=[...]` to enable credentialed CORS
43+
- Deploy parity: scaffolded `main.py` builds a fully-featured, env-driven `app`
44+
so `uvicorn main:app` in a container serves the same surface as
45+
`agentomatic run` (Studio/docs/health/metrics on by default; auth, control
46+
plane, and rate limiting toggle via `AGENTOMATIC_*` env vars)
4347

4448

4549
## v1.2.0 (2026-07-14)

docs/changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ breaking API changes.
5858
- **CORS hardening (P2)**: wildcard origins (`cors_origins=["*"]`) no longer
5959
send `Access-Control-Allow-Credentials: true` — credentials are auto-disabled
6060
(with a one-time warning) unless explicit origins are configured
61+
- **Deploy parity (`uvicorn main:app` == `agentomatic run`)**: the scaffolded
62+
`main.py` now builds a fully-featured, env-driven `app` (Studio, docs, health,
63+
metrics on by default; all component dirs discovered) so a deployed container
64+
serves the exact same surface as `agentomatic run`. Feature flags read from
65+
`AGENTOMATIC_*` env vars (`ENABLE_STUDIO`, `ENABLE_METRICS`, `ENABLE_AUTH`,
66+
`ENABLE_JWT`, `REQUIRE_AUTH`, `ENABLE_CONTROL_PLANE`, `ENABLE_RATE_LIMIT`,
67+
`TITLE`, `LOG_LEVEL`) so the same file works in dev and in the container
6168

6269
---
6370

docs/guide/deployment.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,17 @@ Run it:
437437
agentomatic run agents/ --host 0.0.0.0 --port 8000
438438
```
439439

440+
!!! tip "Scaffolded `main.py` matches `agentomatic run`"
441+
Projects created with `agentomatic init --project` (and containers built by
442+
`agentomatic deploy`) ship a `main.py` whose module-level `app` is **feature-
443+
identical to `agentomatic run`**: it discovers every component directory and
444+
turns Studio, docs, health, and metrics on by default. Auth, control plane,
445+
and rate limiting are driven by `AGENTOMATIC_*` env vars
446+
(`AGENTOMATIC_ENABLE_AUTH`, `AGENTOMATIC_ENABLE_JWT`, `AGENTOMATIC_REQUIRE_AUTH`,
447+
`AGENTOMATIC_ENABLE_CONTROL_PLANE`, `AGENTOMATIC_ENABLE_RATE_LIMIT`,
448+
`AGENTOMATIC_TITLE`, `AGENTOMATIC_LOG_LEVEL`), so `uvicorn main:app` in the
449+
generated Dockerfile drops no functionality versus running the CLI.
450+
440451
!!! warning "Workers and in-memory state"
441452
Connection pools and per-process caches live **per worker**. Keep shared
442453
state (threads, memory, cache) in external services (Postgres, redis) so it

src/agentomatic/cli/project.py

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,69 @@ def _requirements() -> str:
2424

2525

2626
def _main_py(name: str) -> str:
27-
"""Return a starter ``main.py`` with an explicit AgentPlatform config."""
27+
"""Return a starter ``main.py`` whose module-level ``app`` matches ``run``.
28+
29+
The generated module exposes ``app = platform.build()`` so a deployed
30+
container (``uvicorn main:app``) serves the exact same feature set as
31+
``agentomatic run`` (Studio, docs, health, metrics, all component dirs).
32+
Every feature is driven from ``AGENTOMATIC_*`` env vars, so the same file
33+
works unchanged in dev and in production without code edits.
34+
35+
Args:
36+
name: Project directory / display name.
37+
38+
Returns:
39+
Rendered ``main.py`` source.
40+
"""
2841
display = name.replace("_", " ").title()
2942
return f'''"""{name} — Agentomatic platform entrypoint.
3043
31-
Edit the AgentPlatform kwargs below to enable auth, metrics, stores, etc.
32-
Stacks in ``stacks/`` supply LLM / embedding / DB defaults — switch with::
44+
Serves an identical feature set whether launched with ``agentomatic run``
45+
(dev) or ``uvicorn main:app`` (container). Toggle features with
46+
``AGENTOMATIC_*`` env vars — no code edits required. Stacks in ``stacks/``
47+
supply LLM / embedding / DB defaults — switch with::
3348
3449
agentomatic stack use local # or remote
3550
"""
3651
from __future__ import annotations
3752
53+
import os
54+
3855
from agentomatic import AgentPlatform
3956
4057
# Optional: SQLAlchemyStore, MemoryStore, connection configs, JWTConfig, …
4158
4259
60+
def _env_bool(var: str, default: bool) -> bool:
61+
"""Parse a boolean feature flag from the environment.
62+
63+
Args:
64+
var: Environment variable name.
65+
default: Value used when the variable is unset or empty.
66+
67+
Returns:
68+
``True`` for ``1/true/yes/on`` (case-insensitive), else ``False``.
69+
"""
70+
raw = os.getenv(var)
71+
if raw is None or raw == "":
72+
return default
73+
return raw.strip().lower() in {{"1", "true", "yes", "on"}}
74+
75+
4376
def create_platform() -> AgentPlatform:
44-
"""Build the platform from discovered agents/plugins/endpoints/ingestion."""
77+
"""Build a fully-featured platform matching ``agentomatic run`` defaults.
78+
79+
Discovers every component directory (agents, plugins, endpoints, ingestion,
80+
pipelines, stacks) and enables Studio, docs, health, and metrics by default.
81+
Auth, control plane, and rate limiting stay opt-in via ``AGENTOMATIC_*``
82+
env vars so the deployed container is configurable without code changes.
83+
84+
Returns:
85+
A configured :class:`AgentPlatform` ready to :meth:`build`.
86+
"""
87+
# ``require_auth`` mirrors ``agentomatic run --require-auth-globally``:
88+
# it implies zero-trust + JWT unless those are overridden individually.
89+
require_auth = _env_bool("AGENTOMATIC_REQUIRE_AUTH", False)
4590
return AgentPlatform.from_folder(
4691
"agents/",
4792
plugins_dir="plugins/",
@@ -50,18 +95,21 @@ def create_platform() -> AgentPlatform:
5095
stacks_dir="stacks/",
5196
# Pipelines are auto-discovered from ../pipelines/ (sibling of agents/).
5297
# stack="local", # or set via .agentomatic-stack / STACK env
53-
title="{display} Platform",
98+
title=os.getenv("AGENTOMATIC_TITLE", "{display} Platform"),
5499
description="Agentomatic multi-agent platform for {name}",
55-
enable_studio=True,
56-
enable_metrics=True,
57-
# enable_auth=True,
58-
# auth_api_key="${{API_KEY}}", # prefer env / stack auth
59-
# enable_jwt_auth=True, # set jwt_config / JWKS via stack
60-
# enable_zero_trust=True,
61-
# require_auth_globally=False, # needs JWT or API-key auth wired
62-
# enable_control_plane=True,
63-
# control_token="${{CONTROL_TOKEN}}",
64-
# enable_rate_limit=True,
100+
log_level=os.getenv("AGENTOMATIC_LOG_LEVEL", "INFO"),
101+
# On by default — matches `agentomatic run` (Studio) + prod observability.
102+
enable_studio=_env_bool("AGENTOMATIC_ENABLE_STUDIO", True),
103+
enable_metrics=_env_bool("AGENTOMATIC_ENABLE_METRICS", True),
104+
# Opt-in hardening — drive from env / stack; no code edits needed.
105+
enable_auth=_env_bool("AGENTOMATIC_ENABLE_AUTH", False),
106+
auth_api_key=os.getenv("AGENTOMATIC_API_KEY", ""),
107+
enable_jwt_auth=_env_bool("AGENTOMATIC_ENABLE_JWT", require_auth),
108+
enable_zero_trust=_env_bool("AGENTOMATIC_ENABLE_ZERO_TRUST", require_auth),
109+
require_auth_globally=require_auth,
110+
enable_control_plane=_env_bool("AGENTOMATIC_ENABLE_CONTROL_PLANE", False),
111+
control_token=os.getenv("AGENTOMATIC_CONTROL_TOKEN", ""),
112+
enable_rate_limit=_env_bool("AGENTOMATIC_ENABLE_RATE_LIMIT", False),
65113
)
66114
67115
@@ -70,7 +118,10 @@ def create_platform() -> AgentPlatform:
70118
71119
72120
if __name__ == "__main__":
73-
_platform.run(host="0.0.0.0", port=8000)
121+
_platform.run(
122+
host=os.getenv("AGENTOMATIC_HOST", "0.0.0.0"),
123+
port=int(os.getenv("AGENTOMATIC_PORT", "8000")),
124+
)
74125
'''
75126

76127

@@ -130,6 +181,20 @@ def _env_example() -> str:
130181
# Active stack (optional; prefer: agentomatic stack use local)
131182
# AGENTOMATIC_STACK=local
132183
184+
# --- Deploy feature flags (main.py reads these; parity with `agentomatic run`) ---
185+
# Studio + metrics are ON by default; auth/control-plane/rate-limit are opt-in.
186+
# AGENTOMATIC_TITLE=My Platform
187+
# AGENTOMATIC_LOG_LEVEL=INFO
188+
# AGENTOMATIC_ENABLE_STUDIO=1
189+
# AGENTOMATIC_ENABLE_METRICS=1
190+
# AGENTOMATIC_ENABLE_AUTH=0 # API-key auth (needs AGENTOMATIC_API_KEY)
191+
# AGENTOMATIC_ENABLE_JWT=0 # JWT auth (set JWKS via stack)
192+
# AGENTOMATIC_REQUIRE_AUTH=0 # implies JWT + zero-trust; needs auth wired
193+
# AGENTOMATIC_ENABLE_CONTROL_PLANE=0 # needs AGENTOMATIC_CONTROL_TOKEN
194+
# AGENTOMATIC_ENABLE_RATE_LIMIT=0
195+
# AGENTOMATIC_API_KEY=
196+
# AGENTOMATIC_CONTROL_TOKEN=
197+
133198
# --- LLM providers ---
134199
OPENAI_API_KEY=
135200
AZURE_OPENAI_API_KEY=

tests/test_project_scaffold.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,43 @@ def test_project_includes_package_inits(self) -> None:
5858
assert "plugins/__init__.py" in files
5959

6060

61+
class TestDeployRunParity:
62+
"""The deployed ``uvicorn main:app`` must expose the same surface as run."""
63+
64+
@staticmethod
65+
def _build_scaffold_app(
66+
tmp_path: Path,
67+
monkeypatch,
68+
env: dict[str, str] | None = None,
69+
):
70+
"""Scaffold a project, exec its ``main.py``, and return the built app."""
71+
scaffold_project(tmp_path / "proj", "demo_app", force=True)
72+
monkeypatch.chdir(tmp_path / "proj")
73+
for key, value in (env or {}).items():
74+
monkeypatch.setenv(key, value)
75+
namespace: dict[str, object] = {}
76+
source = (tmp_path / "proj" / "main.py").read_text()
77+
exec(compile(source, "main.py", "exec"), namespace) # noqa: S102
78+
return namespace["app"]
79+
80+
def test_app_exposes_run_parity_routes(self, tmp_path: Path, monkeypatch) -> None:
81+
app = self._build_scaffold_app(tmp_path, monkeypatch)
82+
paths = {getattr(route, "path", None) for route in app.routes}
83+
# Platform surface `agentomatic run` always mounts.
84+
for expected in ("/health", "/readiness", "/docs", "/openapi.json", "/", "/studio"):
85+
assert expected in paths, f"missing route: {expected}"
86+
87+
def test_env_flags_drive_features(self, tmp_path: Path, monkeypatch) -> None:
88+
app = self._build_scaffold_app(
89+
tmp_path,
90+
monkeypatch,
91+
env={"AGENTOMATIC_ENABLE_STUDIO": "0", "AGENTOMATIC_TITLE": "Env Title"},
92+
)
93+
paths = {getattr(route, "path", None) for route in app.routes}
94+
assert "/studio" not in paths # studio redirect only added when enabled
95+
assert app.title == "Env Title"
96+
97+
6198
class TestClassTemplateAlias:
6299
def test_class_alias_matches_basic(self) -> None:
63100
basic = get_template_files("basic", "hello")

0 commit comments

Comments
 (0)