Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 2 additions & 3 deletions deploy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOST=0.0.0.0 PORT=8000

# An editable install keeps the package source at /app/packages/agami-core/src, so the package's relative
# migrations path (store.py resolves parents[3]/migrations/core) points at /app/migrations.
# The migrations + static assets now live INSIDE the package (src/migrations, src/static) and resolve
# next to store.py / mcp_http.py, so copying the package source is enough — no separate COPY needed.
WORKDIR /app
COPY packages/agami-core /app/packages/agami-core
COPY migrations /app/migrations
COPY deploy/entrypoint.sh /app/entrypoint.sh
# Install as root, then run as a dedicated unprivileged user (smaller blast radius if the app is compromised).
RUN pip install --no-cache-dir -e "/app/packages/agami-core[server,model]" \
Expand Down
10 changes: 9 additions & 1 deletion packages/agami-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,15 @@ package-dir = { "" = "src" }
# Flat top-level modules (not under a parent package) — listed explicitly so the import
# names stay flat regardless of disk layout.
py-modules = ["agami_paths", "async_offload", "execute_sql", "sql_guard", "mcp_harness", "mcp_http", "tools", "ports", "contracts", "oss_adapters", "store", "model_store", "model_deploy", "deploy_preflight", "oauth_server", "oidc", "passwords", "user_store", "admin", "ui", "onboarding"]
packages = ["semantic_model"]
# `static` (brand assets served at /static) and `migrations` (the core schema) are DATA, not import
# targets — but both must land NEXT TO the flat modules in site-packages, because mcp_http and store
# resolve them as `Path(__file__).parent / …`. Shipping them as packages with their files as
# package-data is what puts them there. Omit either and the installed wheel breaks: a missing `static`
# raises "Directory does not exist" when building the app; a missing `migrations` is worse — it globs
# to nothing, so the server would boot on an empty schema (now guarded in store.run_migrations).
packages = ["semantic_model", "static", "migrations"]

[tool.setuptools.package-data]
semantic_model = ["requirements.txt"]
static = ["**/*"]
migrations = ["**/*"]
11 changes: 8 additions & 3 deletions packages/agami-core/src/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
from pathlib import Path
from typing import Any

# The repo's migration home; resolved relative to this file so it works from an installed package
# or a checkout.
MIGRATIONS_DIR = Path(__file__).resolve().parents[3] / "migrations" / "core"
# The migrations ship INSIDE the package (see pyproject's packages/package-data), so this resolves
# next to this module — identically from an installed wheel and from a checkout. Resolving it out of
# the tree (e.g. parents[3]) silently yields nothing once installed: glob on a missing dir returns [].
MIGRATIONS_DIR = Path(__file__).resolve().parent / "migrations" / "core"

# A fixed (non-secret) key for the Postgres session advisory lock that serializes concurrent
# migration runs — see run_migrations. The digits spell "AGAMI" in hex; any stable bigint works.
Expand Down Expand Up @@ -147,6 +148,10 @@ def run_migrations(
collide on the `schema_migrations` primary key. SQLite is single-writer, so the lock is a no-op.
A failing migration propagates (fail-closed: a half-migrated schema must not serve)."""
migrations_dir = migrations_dir or MIGRATIONS_DIR
# Same failure mode the overlays guard against, but for CORE — and far worse: a missing core root
# globs to nothing, so the server would boot on an EMPTY schema with no error at all. Fail loudly.
if not migrations_dir.is_dir():
raise ValueError(f"core migration root is not a directory: {migrations_dir}")
Comment on lines 150 to +154
overlays = overlay_dirs if overlay_dirs is not None else list(_MIGRATION_OVERLAYS)
# A registered overlay that doesn't exist (or isn't a directory) would silently apply nothing —
# `glob` on a bad path yields an empty iterator — so a misconfigured overlay would be skipped
Expand Down
Loading