Initial public ModelForge release

This commit is contained in:
Jens
2026-09-01 21:30:16 +02:00
commit 7082ab955a
490 changed files with 104252 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
.git
.env
**/__pycache__
**/.pytest_cache
**/.mypy_cache
**/.ruff_cache
**/*.pyc
frontend/node_modules
frontend/dist
frontend/tsconfig.tsbuildinfo
backend/tests
node-agent/tests
docs
+342
View File
@@ -0,0 +1,342 @@
# ITWorx ModelForge 1.2.1 — configuration example
#
# Generated by scripts/generate_configuration_docs.py. Do not edit by hand.
# Copy to .env and fill in the values marked REQUIRED. See docs/CONFIGURATION.md.
#
# Secrets are intentionally empty here. This file is committed to the repository, so a
# real value placed in it would be published with the release.
# Deployment profile. 'production' turns on every fail-closed startup rule; 'development' and 'test' report the same problems without refusing to start.
MODELFORGE_ENV=development
# Interface the API binds inside its container. Leave at 0.0.0.0.
MODELFORGE_API_HOST=0.0.0.0
# Port the API listens on inside its container.
MODELFORGE_API_PORT=8000
# Pre-parser request-body limit for public and operator control-plane routes.
MODELFORGE_CONTROL_PLANE_MAX_PAYLOAD_BYTES=1048576
# Name this process reports in logs and audit events.
MODELFORGE_SERVICE_NAME=modelforge-api
# Structured log level: DEBUG, INFO, WARNING or ERROR.
MODELFORGE_LOG_LEVEL=INFO
# SQLAlchemy URL for the API's non-owner modelforge_runtime role. It may read the audit trail and execute the canonical append function, but cannot mutate audit tables directly. (REQUIRED in production)
MODELFORGE_DATABASE_URL=
# SQLAlchemy URL for the non-superuser modelforge schema-owner role. Set only in the one-shot migration process; production API startup refuses when this secret is present.
MODELFORGE_MIGRATION_DATABASE_URL=
# Redis URL for transient request payloads and queues. (REQUIRED in production)
MODELFORGE_REDIS_URL=redis://localhost:6379/0
# Comma-separated exact origins allowed to call the API from a browser. A wildcard is refused in production because requests are credentialed.
MODELFORGE_CORS_ORIGINS=http://localhost:3000
# Operator API key guarding every admin route. Generate at least 32 random characters; ModelForge never mints one for you. (REQUIRED in production)
MODELFORGE_OPERATOR_API_KEY=
# Optional Hugging Face token, used only for acquiring gated repositories. It is never passed to a runtime and never leaves the control plane.
MODELFORGE_HF_TOKEN=
# Base64 AES-256 key for backup encryption. Without it no backup can be produced, and without the same key no backup can be restored — store it outside this deployment. (REQUIRED in production)
MODELFORGE_BACKUP_ENCRYPTION_KEY=
# Identifier recorded in each backup manifest so a restore can name the key it needs.
MODELFORGE_BACKUP_ENCRYPTION_KEY_ID=modelforge-backup-key-1
# Hugging Face cache root inside the container.
MODELFORGE_HF_HOME=/data/hf-cache
# Verified model artifact root. Must exist and be writable.
MODELFORGE_ARTIFACT_ROOT=/data/artifacts
# Where acquired artifacts are held until their checks pass.
MODELFORGE_QUARANTINE_ROOT=/data/quarantine
# Artifact root as a runtime worker sees it on a compute node.
MODELFORGE_RUNTIME_ARTIFACT_ROOT=/models/model-registry
# Backup destination. Must exist and be writable, or backups fail closed.
MODELFORGE_BACKUP_ROOT=/data/backups
# Working directory a restore stages into before it commits.
MODELFORGE_BACKUP_RESTORE_ROOT=/data/restore
# Per-request timeout for Hugging Face metadata calls.
MODELFORGE_HF_TIMEOUT_SECONDS=30
# How long a resolved upstream snapshot stays cached.
MODELFORGE_HF_SNAPSHOT_TTL_SECONDS=3600
# Whether model repositories may execute their own Python. Always false in production; startup refuses any other value there.
MODELFORGE_ALLOW_REMOTE_CODE=false
# Collect GPU telemetry on this host.
MODELFORGE_ENABLE_GPU_TELEMETRY=true
# Run a hardware inventory pass when the process starts.
MODELFORGE_HARDWARE_REFRESH_ON_STARTUP=false
# Interval between hardware inventory passes.
MODELFORGE_HARDWARE_POLL_INTERVAL_SECONDS=30
# Explicit node identity. Leave empty to use the persisted file.
MODELFORGE_NODE_IDENTITY=
# 'persisted' keeps a node's identity across restarts; 'auto' derives it.
MODELFORGE_NODE_IDENTITY_MODE=auto
# Where a persisted node identity is stored.
MODELFORGE_NODE_IDENTITY_FILE=/data/state/node-id
# Silence after which a node is considered stale.
MODELFORGE_NODE_STALE_AFTER_SECONDS=30
# Silence after which a node is considered offline. Must exceed the stale threshold.
MODELFORGE_NODE_OFFLINE_AFTER_SECONDS=90
# How often node liveness is re-evaluated.
MODELFORGE_LIVENESS_POLL_INTERVAL_SECONDS=5
# Clock skew tolerated on an agent report before refusal.
MODELFORGE_AGENT_MAX_CLOCK_SKEW_SECONDS=300
# Pre-parser request-body limit for enrollment and authenticated Node Agent reports.
MODELFORGE_NODE_AGENT_MAX_PAYLOAD_BYTES=4194304
# Run the node liveness monitor in this process.
MODELFORGE_NODE_LIVENESS_MONITOR_ENABLED=false
# Maximum inputs accepted in a single capability invocation.
MODELFORGE_GATEWAY_MAX_BATCH_SIZE=8
# Maximum characters per input item.
MODELFORGE_GATEWAY_MAX_INPUT_CHARACTERS=8192
# Maximum accepted request body size.
MODELFORGE_GATEWAY_MAX_PAYLOAD_BYTES=65536
# Total time a capability invocation may take. Must exceed the queue timeout.
MODELFORGE_GATEWAY_REQUEST_TIMEOUT_SECONDS=45
# How long a request may wait for capacity before rejection.
MODELFORGE_GATEWAY_QUEUE_TIMEOUT_SECONDS=30
# Lease held by a serving job before it is reclaimed.
MODELFORGE_SERVING_JOB_LEASE_SECONDS=120
# How long a request payload survives in Redis.
MODELFORGE_SERVING_PAYLOAD_TTL_SECONDS=120
# VRAM never offered to a placement, as an absolute floor.
MODELFORGE_SCHEDULER_SAFETY_RESERVE_BYTES=1073741824
# VRAM never offered to a placement, as a fraction. Half a device leaves nothing schedulable.
MODELFORGE_SCHEDULER_SAFETY_RESERVE_PERCENTAGE=0.05
# Headroom reserved for runtime overhead per node.
MODELFORGE_SCHEDULER_RUNTIME_MARGIN_BYTES=268435456
# Absolute headroom added to each deployment estimate.
MODELFORGE_SCHEDULER_DEPLOYMENT_MARGIN_BYTES=134217728
# Proportional headroom added to each estimate.
MODELFORGE_SCHEDULER_DEPLOYMENT_MARGIN_PERCENTAGE=0.1
# Queued requests accepted before capacity rejection begins.
MODELFORGE_SCHEDULER_GLOBAL_QUEUE_LIMIT=128
# Telemetry age past which admission is blocked rather than extrapolated.
MODELFORGE_SCHEDULER_TELEMETRY_STALE_SECONDS=90
# How long pressure must hold before the state changes.
MODELFORGE_SCHEDULER_PRESSURE_STABLE_SECONDS=30
# Minimum interval between evictions on a node.
MODELFORGE_SCHEDULER_EVICTION_COOLDOWN_SECONDS=60
# Placement decisions retained for inspection.
MODELFORGE_SCHEDULER_PLACEMENT_HISTORY_LIMIT=500
# Seed the candidate and project registries from manifests.
MODELFORGE_REGISTRY_SEED_ON_STARTUP=false
# Reconcile abandoned serving work in this process.
MODELFORGE_SERVING_RECONCILIATION_ENABLED=false
# Interval between serving reconciliation passes.
MODELFORGE_SERVING_RECONCILIATION_INTERVAL_SECONDS=5
# Roll back incomplete lifecycle operations at startup.
MODELFORGE_LIFECYCLE_RECONCILIATION_ENABLED=false
# Report interrupted migration cutovers at startup. They are never auto-resolved: external alias truth cannot be inferred after a crash.
MODELFORGE_MIGRATION_RECONCILIATION_ENABLED=false
# Run SLO and alert evaluation in this process.
MODELFORGE_OBSERVABILITY_MONITOR_ENABLED=false
# Interval between observability evaluation passes.
MODELFORGE_OBSERVABILITY_POLL_INTERVAL_SECONDS=60
# Reconcile interrupted backups and restores at startup.
MODELFORGE_RECOVERY_RECONCILIATION_ENABLED=false
# pg_dump executable. Must match the server major version.
MODELFORGE_BACKUP_PG_DUMP_PATH=pg_dump
# pg_restore executable.
MODELFORGE_BACKUP_PG_RESTORE_PATH=pg_restore
# psql executable.
MODELFORGE_BACKUP_PSQL_PATH=psql
# Timeout for a dump or restore command.
MODELFORGE_BACKUP_COMMAND_TIMEOUT_SECONDS=1800
# Age past which the newest verified backup raises BACKUP_STALE.
MODELFORGE_BACKUP_STALE_AFTER_SECONDS=93600
# Free space below which a backup refuses to start.
MODELFORGE_BACKUP_MINIMUM_FREE_BYTES=1073741824
# Required free space as a multiple of the estimated size.
MODELFORGE_BACKUP_CAPACITY_HEADROOM_RATIO=3.0
# Whether a restore may overwrite the live database. Keep false outside a rehearsal.
MODELFORGE_RESTORE_ALLOW_PRODUCTION_TARGET=false
# --------------------------------------------------------------------------
# Deployment variables. Read by Compose, the Node Agent and the Runtime Worker
# rather than by the control-plane process - an operator still has to set them.
# --------------------------------------------------------------------------
# Host address the control-plane database is published on. Defaults to 127.0.0.1; publishing it more widely exposes provenance, credential hashes and the audit trail.
MODELFORGE_POSTGRES_BIND=
# Host address Redis is published on. Defaults to 127.0.0.1.
MODELFORGE_REDIS_BIND=
# Host address the API is published on. Defaults to 0.0.0.0 deliberately: the console and compute nodes need it, and every admin route is operator-authenticated.
MODELFORGE_API_BIND=
# Host address the console is published on. Defaults to 127.0.0.1.
MODELFORGE_WEB_BIND=
# Host port the database is published on. Defaults to 5432.
MODELFORGE_POSTGRES_PORT=
# Host port Redis is published on. Defaults to 6379.
MODELFORGE_REDIS_PORT=
# Host port the API is published on. Defaults to 8000.
MODELFORGE_API_PUBLISHED_PORT=
# Host port the console is published on. Defaults to 3000.
MODELFORGE_WEB_PORT=
# Host address for the DR rehearsal database. Loopback only.
MODELFORGE_DR_POSTGRES_BIND=
# Host address for the DR rehearsal API. Loopback only.
MODELFORGE_DR_API_BIND=
# API base URL compiled into the console. Vite inlines it at build time, so changing it requires rebuilding the console image, not restarting it.
VITE_API_BASE_URL=
# Production database name. Required by the production overlay.
MODELFORGE_POSTGRES_DB=
# Bootstrap/admin role used only by PostgreSQL provisioning; defaults to postgres.
MODELFORGE_POSTGRES_ADMIN_USER=
# Bootstrap/admin password; never passed to the migration or API container. (REQUIRED in production)
MODELFORGE_POSTGRES_ADMIN_PASSWORD=
# Raw password supplied to provisioning for the non-superuser modelforge owner role. (REQUIRED in production)
MODELFORGE_MIGRATION_DB_PASSWORD=
# Raw password supplied to provisioning for the non-owner modelforge_runtime role. (REQUIRED in production)
MODELFORGE_RUNTIME_DB_PASSWORD=
# Non-owner runtime-role URL passed only to the API container. (REQUIRED in production)
MODELFORGE_RUNTIME_DATABASE_URL=
# Exact version tag applied to built images and required when the production overlay is not given explicit API and web image references.
MODELFORGE_VERSION=
# Source commit stamped into images at build time.
MODELFORGE_COMMIT=
# Build timestamp stamped into images.
MODELFORGE_BUILT_AT=
# Exact tag or digest the production overlay runs for the API; never use latest.
MODELFORGE_API_IMAGE=
# Exact tag or digest the production overlay runs for the console; never use latest.
MODELFORGE_WEB_IMAGE=
# Exact release tag or digest for the standalone Node Agent. The local-build fallback is named local and never resolves to latest.
MODELFORGE_NODE_AGENT_IMAGE=
# Digest recorded as the running API build identity.
MODELFORGE_API_IMAGE_DIGEST=
# Volume or bind path backing the backup root.
MODELFORGE_BACKUP_VOLUME=
# Volume or bind path backing the restore staging root.
MODELFORGE_RESTORE_VOLUME=
# Volume or bind path holding the agent's persisted identity.
MODELFORGE_AGENT_STATE_VOLUME=
# Volume or bind path for the agent's Hugging Face cache.
MODELFORGE_AGENT_HF_CACHE_VOLUME=
# Volume or bind path for verified artifacts on a node.
MODELFORGE_AGENT_ARTIFACT_VOLUME=
# Volume or bind path for the node's quarantine area.
MODELFORGE_AGENT_QUARANTINE_VOLUME=
# URL the agent reports to. Outbound only; the control plane never dials a node.
MODELFORGE_AGENT_CONTROL_PLANE_URL=
# Single-use enrolment token. Consumed atomically: a storm against one token produces exactly one identity.
MODELFORGE_AGENT_ENROLLMENT_TOKEN=
# Hostname the agent enrols under.
MODELFORGE_AGENT_HOSTNAME=
# Accelerator contract: nvidia fails closed unless NVML inventory and telemetry are valid; cpu permits a legitimate CPU-only node; auto requires NVIDIA when injected devices are observed. Canonical GPU Compose deployments set nvidia explicitly.
MODELFORGE_AGENT_ACCELERATOR_MODE=
# Whether the agent verifies the control plane's certificate. True wherever TLS is real.
MODELFORGE_AGENT_TLS_VERIFY=
# Host address mapped for a private-CA deployment.
MODELFORGE_AGENT_CONTROL_PLANE_HOST_ADDRESS=
# Path to the private CA certificate the agent trusts.
MODELFORGE_AGENT_CA_CERT_PATH=
# Artifact root as the runtime worker sees it.
MODELFORGE_RUNTIME_WORKER_ARTIFACT_ROOT=
# Worker poll interval, in seconds.
MODELFORGE_RUNTIME_WORKER_POLL_INTERVAL_SECONDS=
# Source commit reported by the deployment.
MODELFORGE_SOURCE_COMMIT=
# Git reference reported by the deployment.
MODELFORGE_SOURCE_REFERENCE=
# Repository URL reported by the deployment.
MODELFORGE_SOURCE_REPOSITORY=
+12
View File
@@ -0,0 +1,12 @@
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.woff2 binary
@@ -0,0 +1,215 @@
"""Fail-closed checks for the production delivery workflow.
This test intentionally uses only the Python standard library so it can run before
project dependencies are installed. The small helpers parse indentation-delimited
YAML blocks that are relevant to the policy; they are not a general YAML parser.
"""
from __future__ import annotations
import re
import subprocess
import unittest
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
DEPLOY_WORKFLOW = REPOSITORY_ROOT / ".gitea" / "workflows" / "unraid-deploy.yml"
VALIDATION_WORKFLOW = REPOSITORY_ROOT / ".gitea" / "workflows" / "managed-validation.yml"
def _indented_block(text: str, header: str) -> str:
"""Return a YAML block beginning at an exact header line."""
lines = text.splitlines()
try:
start = lines.index(header)
except ValueError as exc:
raise AssertionError(f"Missing YAML header: {header!r}") from exc
indent = len(header) - len(header.lstrip())
end = len(lines)
for index in range(start + 1, len(lines)):
line = lines[index]
if not line.strip() or line.lstrip().startswith("#"):
continue
current_indent = len(line) - len(line.lstrip())
if current_indent <= indent:
end = index
break
return "\n".join(lines[start:end])
def _named_step(text: str, name: str) -> str:
return _indented_block(text, f" - name: {name}")
def _run_script(step: str) -> str:
"""Extract one inline or literal run body without normalizing shell content."""
lines = step.splitlines()
if " run: |" not in lines:
inline = next((line for line in lines if line.startswith(" run: ")), None)
if inline is None:
raise AssertionError("Named step does not contain a run command")
return inline.removeprefix(" run: ") + "\n"
start = lines.index(" run: |") + 1
script_lines = lines[start:]
if any(line and not line.startswith(" ") for line in script_lines):
raise AssertionError("Unexpected indentation in literal run block")
return "\n".join(line[10:] if line else "" for line in script_lines) + "\n"
class ProductionDeployPolicyTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.deploy = DEPLOY_WORKFLOW.read_text(encoding="utf-8")
cls.validation = VALIDATION_WORKFLOW.read_text(encoding="utf-8")
def test_deployment_has_no_automatic_trigger(self) -> None:
trigger = _indented_block(self.deploy, "on:")
trigger_keys = re.findall(r"^ ([a-z_]+):", trigger, flags=re.MULTILINE)
self.assertEqual(trigger_keys, ["workflow_dispatch"])
self.assertNotRegex(trigger, r"(?m)^\s+push:")
def test_dispatch_inputs_are_required_and_fail_safe(self) -> None:
inputs = _indented_block(self.deploy, " inputs:")
input_names = re.findall(r"^ ([a-z_]+):", inputs, flags=re.MULTILINE)
self.assertEqual(input_names, ["release_tag", "release_commit", "action"])
for input_name in ("release_tag", "release_commit"):
block = _indented_block(self.deploy, f" {input_name}:")
self.assertRegex(block, r"(?m)^ required: true$")
self.assertRegex(block, r"(?m)^ type: string$")
action = _indented_block(self.deploy, " action:")
self.assertRegex(action, r"(?m)^ required: true$")
self.assertRegex(action, r"(?m)^ default: VERIFY_ONLY$")
self.assertRegex(action, r"(?m)^ type: choice$")
options = re.findall(r"^ - (\S+)$", action, flags=re.MULTILINE)
self.assertEqual(options, ["VERIFY_ONLY", "DEPLOY_STABLE_TO_PRODUCTION"])
def test_inputs_are_validated_before_checkout(self) -> None:
validation = _named_step(self.deploy, "Validate immutable dispatch inputs")
checkout = _named_step(self.deploy, "Check out the exact release commit with full history")
self.assertLess(self.deploy.index(validation), self.deploy.index(checkout))
self.assertIn('"refs/heads/master"', validation)
self.assertIn(
r"^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$",
validation,
)
self.assertIn(r"^[0-9a-f]{40}$", validation)
self.assertIn("VERIFY_ONLY|DEPLOY_STABLE_TO_PRODUCTION", validation)
tag_pattern = re.search(r'RELEASE_TAG\}" =~ (\^.*\$) \]\]', validation)
commit_pattern = re.search(r'RELEASE_COMMIT\}" =~ (\^.*\$) \]\]', validation)
self.assertIsNotNone(tag_pattern)
self.assertIsNotNone(commit_pattern)
assert tag_pattern is not None and commit_pattern is not None
for valid_tag in ("v0.0.0", "v1.2.1", "v10.20.30"):
self.assertRegex(valid_tag, tag_pattern.group(1))
for invalid_tag in ("1.2.1", "v01.2.1", "v1.2.1-rc.1", "v1.2.1+build", "master"):
self.assertNotRegex(invalid_tag, tag_pattern.group(1))
self.assertRegex("0" * 40, commit_pattern.group(1))
for invalid_commit in ("0" * 39, "A" * 40, "g" * 40, "master"):
self.assertNotRegex(invalid_commit, commit_pattern.group(1))
def test_checkout_is_pinned_to_the_validated_commit_with_full_history(self) -> None:
checkout = _named_step(self.deploy, "Check out the exact release commit with full history")
self.assertRegex(checkout, r"actions/checkout@[0-9a-f]{40}")
self.assertIn("ref: ${{ inputs.release_commit }}", checkout)
self.assertIn("fetch-depth: 0", checkout)
self.assertIn("persist-credentials: true", checkout)
def test_release_provenance_is_verified_and_exported(self) -> None:
provenance = _named_step(self.deploy, "Verify stable release provenance")
required_fragments = (
"git fetch --force --no-recurse-submodules origin",
'git cat-file -t "${tag_ref}"',
'git rev-parse "${tag_ref}^{tag}"',
'git rev-parse "${tag_ref}^{commit}"',
"git rev-parse HEAD",
"< VERSION",
"git merge-base --is-ancestor",
"refs/remotes/origin/master",
"DEPLOY_COMMIT=%s",
'>> "${GITHUB_ENV}"',
)
for fragment in required_fragments:
with self.subTest(fragment=fragment):
self.assertIn(fragment, provenance)
self.assertNotIn("GITHUB_SHA", self.deploy)
def test_all_production_shell_blocks_parse(self) -> None:
names = (
"Validate immutable dispatch inputs",
"Verify stable release provenance",
"Verification-only result",
"Deploy verified stable release to production",
)
for name in names:
script = _run_script(_named_step(self.deploy, name))
result = subprocess.run(
["bash", "-n"],
input=script.encode("utf-8"),
capture_output=True,
check=False,
)
with self.subTest(step=name):
self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", "replace"))
def test_deployment_requires_the_explicit_action_and_verified_commit(self) -> None:
deploy = _named_step(self.deploy, "Deploy verified stable release to production")
self.assertIn("inputs.action == 'DEPLOY_STABLE_TO_PRODUCTION'", deploy)
self.assertIn('test -n "${DEPLOY_COMMIT:-}"', deploy)
self.assertIn('"${GITHUB_REPOSITORY}" "${DEPLOY_COMMIT}"', deploy)
self.assertEqual(self.deploy.count("/opt/gitea-deploy/deploy.py deploy"), 1)
verify_only = _named_step(self.deploy, "Verification-only result")
self.assertIn("inputs.action == 'VERIFY_ONLY'", verify_only)
def test_managed_validation_runs_policy_before_profile_work(self) -> None:
policy_step = _named_step(self.validation, "Production delivery policy")
self.assertIn("python3 -m unittest discover -s .gitea/tests", policy_step)
self.assertNotRegex(policy_step, r"(?m)^ if:")
self.assertLess(
self.validation.index(policy_step),
self.validation.index(" - name: Validate the requested profile"),
)
def test_security_profile_runs_fail_closed_scanners(self) -> None:
step = _named_step(self.validation, "Security — secrets and vulnerable dependencies")
self.assertIn("env.PROFILE == 'security'", step)
self.assertIn("env.PROFILE == 'full'", step)
script = _run_script(step)
self.assertIn("gitleaks git .", script)
self.assertIn("pip_audit --strict --skip-editable", script)
self.assertIn("npm audit --audit-level=high --omit=dev", script)
self.assertNotRegex(script, r"(?m)(?:\|\|\s*true|continue-on-error)")
toolchain = _named_step(self.validation, "Toolchain")
toolchain_script = _run_script(toolchain)
self.assertIn("command -v gitleaks", toolchain_script)
self.assertIn("exit 1", toolchain_script)
def test_compose_validation_renders_the_production_projection(self) -> None:
step = _named_step(self.validation, "Compose projections")
script = _run_script(step)
self.assertIn(
"for overlay in backup dr gpu node-agent node-recovery production runtime-worker",
script,
)
for variable in (
"MODELFORGE_POSTGRES_ADMIN_PASSWORD",
"MODELFORGE_MIGRATION_DATABASE_URL",
"MODELFORGE_RUNTIME_DATABASE_URL",
"MODELFORGE_OPERATOR_API_KEY",
"MODELFORGE_BACKUP_ENCRYPTION_KEY",
"MODELFORGE_CORS_ORIGINS",
"VITE_API_BASE_URL",
):
with self.subTest(variable=variable):
self.assertIn(f"export {variable}=", script)
if __name__ == "__main__":
unittest.main()
+199
View File
@@ -0,0 +1,199 @@
"""Fail-closed integration tests for the parentless public source boundary."""
from __future__ import annotations
import subprocess
import tempfile
import unittest
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
EXPORTER = REPOSITORY_ROOT / "scripts" / "export-public-source.mjs"
VALIDATOR = REPOSITORY_ROOT / "scripts" / "validate-public-source.mjs"
class PublicSourceExportTests(unittest.TestCase):
def _repository(
self,
root: Path,
*,
license_present: bool = True,
managed_validation_workflow: str | None = None,
) -> Path:
repository = root / "private-source"
repository.mkdir()
files: dict[str, str | bytes] = {
"README.md": "# Product\n",
"SECURITY.md": "# Security\n",
"CONTRIBUTING.md": "# Contributing\n",
"docker-compose.yml": "services: {}\n",
"VERSION": "1.2.1\n",
"backend/pyproject.toml": "[project]\nname = 'fixture-api'\n",
"frontend/package.json": '{"name":"fixture-web"}\n',
"node-agent/pyproject.toml": "[project]\nname = 'fixture-agent'\n",
"runtime-worker/pyproject.toml": "[project]\nname = 'fixture-worker'\n",
}
if license_present:
files["LICENSE"] = (REPOSITORY_ROOT / "LICENSE").read_bytes()
if managed_validation_workflow is not None:
files[".gitea/workflows/managed-validation.yml"] = managed_validation_workflow
for relative_path, contents in files.items():
destination = repository / relative_path
destination.parent.mkdir(parents=True, exist_ok=True)
if isinstance(contents, bytes):
destination.write_bytes(contents)
else:
destination.write_text(contents, encoding="utf-8")
(repository / "public-source.allowlist").write_text(
"\n".join(sorted(files)) + "\n", encoding="utf-8"
)
subprocess.run(["git", "init", "-q"], cwd=repository, check=True)
subprocess.run(["git", "add", "."], cwd=repository, check=True)
subprocess.run(
[
"git",
"-c",
"user.name=ModelForge test",
"-c",
"user.email=test@example.invalid",
"commit",
"-qm",
"fixture",
],
cwd=repository,
check=True,
)
return repository
def _export(self, repository: Path, output: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["node", str(EXPORTER), "--repository", str(repository), "--output", str(output)],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
)
def _validate(self, output: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["node", str(VALIDATOR)],
cwd=output,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
)
def test_export_requires_an_explicit_tracked_license(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
repository = self._repository(root, license_present=False)
result = self._export(repository, root / "public")
self.assertNotEqual(result.returncode, 0)
self.assertIn("canonical LICENSE is missing", result.stderr)
def test_manifest_detects_content_tampering_and_unexpected_files(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
output = root / "public"
result = self._export(self._repository(root), output)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
validation = self._validate(output)
self.assertEqual(validation.returncode, 0, validation.stdout + validation.stderr)
original_readme = (output / "README.md").read_bytes()
(output / "README.md").write_text("tampered\n", encoding="utf-8")
tampered = self._validate(output)
self.assertNotEqual(tampered.returncode, 0)
self.assertIn("Manifest", tampered.stderr)
(output / "README.md").write_bytes(original_readme)
(output / "not-reviewed.txt").write_text("extra\n", encoding="utf-8")
unexpected = self._validate(output)
self.assertNotEqual(unexpected.returncode, 0)
self.assertIn("Unexpected public source files", unexpected.stderr)
def test_dirty_source_tree_cannot_claim_the_head_revision(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
repository = self._repository(root)
(repository / "README.md").write_text("changed after commit\n", encoding="utf-8")
result = self._export(repository, root / "public")
self.assertNotEqual(result.returncode, 0)
self.assertIn("commit the exact source tree", result.stderr)
def test_sanitized_destination_collisions_fail_before_writing(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
repository = self._repository(root)
private_name = "Tow" + "er.txt"
public_name = "GPU " + "Node.txt"
(repository / private_name).write_text("one\n", encoding="utf-8")
(repository / public_name).write_text("two\n", encoding="utf-8")
with (repository / "public-source.allowlist").open("a", encoding="utf-8") as allowlist:
allowlist.write(f"{private_name}\n{public_name}\n")
subprocess.run(["git", "add", "."], cwd=repository, check=True)
subprocess.run(
[
"git",
"-c",
"user.name=ModelForge test",
"-c",
"user.email=test@example.invalid",
"commit",
"-qm",
"collision fixture",
],
cwd=repository,
check=True,
)
result = self._export(repository, root / "public")
self.assertNotEqual(result.returncode, 0)
self.assertIn("finding", result.stderr)
def test_public_managed_validation_requires_explicit_owner_dispatch(self) -> None:
workflow = "name: Managed validation\n\non:\n pull_request:\n workflow_dispatch:\n"
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
repository = self._repository(
root,
managed_validation_workflow=workflow,
)
output = root / "public"
result = self._export(repository, output)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
canonical = (
repository / ".gitea/workflows/managed-validation.yml"
).read_text(encoding="utf-8")
exported = (
output / ".gitea/workflows/managed-validation.yml"
).read_text(encoding="utf-8")
self.assertIn(" pull_request:\n", canonical)
self.assertNotIn(" pull_request:\n", exported)
self.assertIn(" workflow_dispatch:\n", exported)
self.assertIn("fork PRs never reach private runners", exported)
def test_changed_managed_validation_trigger_fails_closed(self) -> None:
workflow = "name: Managed validation\n\non:\n workflow_dispatch:\n"
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
repository = self._repository(
root,
managed_validation_workflow=workflow,
)
result = self._export(repository, root / "public")
self.assertNotEqual(result.returncode, 0)
self.assertIn("has no expected pull_request trigger", result.stderr)
if __name__ == "__main__":
unittest.main()
+280
View File
@@ -0,0 +1,280 @@
"""Structural safety checks for public-candidate server acceptance."""
from __future__ import annotations
import subprocess
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
WORKFLOW = ROOT / ".gitea" / "workflows" / "public-candidate-acceptance.yml"
MANAGED_WORKFLOW = ROOT / ".gitea" / "workflows" / "managed-validation.yml"
ACCEPTANCE = ROOT / "scripts" / "rc_server_acceptance.py"
TRIVY_VALIDATOR = ROOT / "scripts" / "validate_trivy_report.py"
RUNTIME_DOCKERFILE = ROOT / "runtime-worker" / "Dockerfile"
RUNTIME_PYPROJECT = ROOT / "runtime-worker" / "pyproject.toml"
class RcAcceptancePolicyTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.workflow = WORKFLOW.read_text(encoding="utf-8")
cls.managed = MANAGED_WORKFLOW.read_text(encoding="utf-8")
cls.acceptance = ACCEPTANCE.read_text(encoding="utf-8")
cls.trivy = TRIVY_VALIDATOR.read_text(encoding="utf-8")
cls.runtime_dockerfile = RUNTIME_DOCKERFILE.read_text(encoding="utf-8")
cls.runtime_pyproject = RUNTIME_PYPROJECT.read_text(encoding="utf-8")
def test_workflow_is_manual_and_never_calls_production_deployment(self) -> None:
self.assertIn("workflow_dispatch:", self.workflow)
self.assertNotIn("\n push:", self.workflow)
self.assertNotIn("pull_request:", self.workflow)
self.assertNotIn("deploy.py", self.workflow)
self.assertNotIn("DEPLOY_STABLE_TO_PRODUCTION", self.workflow)
def test_exact_source_and_docker_are_required(self) -> None:
self.assertIn(r"^[0-9a-f]{40}$", self.workflow)
self.assertIn("ref: ${{ inputs.source_commit }}", self.workflow)
self.assertIn("docker version", self.workflow)
self.assertNotIn("SKIPPED: no docker", self.workflow)
def test_security_tools_are_downloaded_with_exact_checksums(self) -> None:
self.assertIn("GITLEAKS_VERSION: 8.30.1", self.workflow)
self.assertIn(
"GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb",
self.workflow,
)
self.assertIn('printf \'GITLEAKS=%s\\n\'', self.workflow)
self.assertIn('\"${GITLEAKS}\" dir \"${public_source}\"', self.workflow)
self.assertIn("sha256sum --check --strict", self.workflow)
self.assertNotIn("command -v gitleaks", self.workflow)
def test_unmerged_branch_can_use_the_existing_managed_dispatch_entry(self) -> None:
self.assertIn("workflow_call:", self.workflow)
self.assertIn("uses: ./.gitea/workflows/public-candidate-acceptance.yml", self.managed)
self.assertIn("inputs.profile == 'build'", self.managed)
self.assertIn("source_commit: ${{ gitea.sha }}", self.managed)
def test_all_four_images_are_built_and_scanned(self) -> None:
for image in (
"modelforge-api",
"modelforge-web",
"modelforge-node-agent",
"modelforge-runtime-worker",
):
self.assertIn(image, self.acceptance)
self.assertIn('"Metadata"', self.trivy)
self.assertIn('metadata.get("ImageID") != expected_image_id', self.trivy)
self.assertIn("reject_duplicate_keys", self.trivy)
self.assertIn("fixable_high_critical + unreviewed_unfixed_high_critical", self.trivy)
self.assertIn("public-candidate-unfixed-vulnerabilities.json", self.acceptance)
def test_runtime_image_applies_security_updates_and_exact_inference_pins(self) -> None:
self.assertIn("apt-get upgrade --yes", self.runtime_dockerfile)
self.assertIn("apt-get purge --yes linux-libc-dev", self.runtime_dockerfile)
self.assertNotIn("apt-get autoremove", self.runtime_dockerfile)
self.assertIn("pip check", self.runtime_dockerfile)
for dependency in (
"Pillow==12.3.0",
"protobuf==5.29.6",
"sentencepiece==0.2.2",
"sentence-transformers==6.0.1",
"transformers==5.16.1",
"urllib3==2.7.0",
):
self.assertIn(dependency, self.runtime_pyproject)
def test_compose_project_is_isolated_and_always_removed(self) -> None:
self.assertIn('project = f"modelforge-rc-', self.acceptance)
self.assertIn('"MODELFORGE_API_BIND": "127.0.0.1"', self.acceptance)
self.assertIn('"MODELFORGE_API_PUBLISHED_PORT": "0"', self.acceptance)
self.assertIn('"production_changed": False', self.acceptance)
self.assertIn('"compute_identity_created": False', self.acceptance)
self.assertIn('"down", "--volumes", "--remove-orphans"', self.acceptance)
self.assertIn("atexit.register(cleanup_candidate_images", self.acceptance)
self.assertIn('"docker", "image", "rm", "--force", tag', self.acceptance)
self.assertNotIn("docker-compose.runtime-worker.yml", self.acceptance)
self.assertNotIn("docker-compose.node-agent.yml", self.acceptance)
def test_clean_install_bootstraps_roles_across_a_remote_docker_daemon(self) -> None:
self.assertIn("def provision_database_roles(", self.acceptance)
self.assertIn('run("docker", "cp", str(bootstrap)', self.acceptance)
self.assertIn('"psql",', self.acceptance)
self.assertIn('"--file",', self.acceptance)
self.assertIn(
'"--wait", "postgres", "redis", env=env', self.acceptance
)
self.assertLess(
self.acceptance.index("provision_database_roles(compose, env, database)"),
self.acceptance.index(
'"--wait", "api", "web", env=env'
),
)
def test_clean_install_seeds_config_across_a_remote_docker_daemon(self) -> None:
self.assertIn("def provision_config_volume(project: str)", self.acceptance)
self.assertIn('f"{project}_acceptance-config"', self.acceptance)
self.assertIn('f"com.docker.compose.project={project}"', self.acceptance)
self.assertIn('run("docker", "cp", f"{ROOT / \'config\'}/."', self.acceptance)
self.assertIn(
'run("docker", "container", "rm", "--force", seed, check=False)',
self.acceptance,
)
self.assertIn('" read_only: true\\n"', self.acceptance)
self.assertLess(
self.acceptance.index("provision_config_volume(project)"),
self.acceptance.index('"--wait", "api", "web", env=env'),
)
def test_http_contract_runs_inside_the_isolated_compose_network(self) -> None:
self.assertIn("def container_http_status(", self.acceptance)
self.assertIn("def wait_for_container_status(", self.acceptance)
self.assertIn('headers["X-ModelForge-Admin-Token"]', self.acceptance)
self.assertIn('os.environ["MODELFORGE_OPERATOR_API_KEY"]', self.acceptance)
self.assertIn('wait_for_container_status(api_container, "http://web:3000/", 200)', self.acceptance)
self.assertNotIn("urllib.request.urlopen(request, timeout=timeout)", self.acceptance)
def test_workflow_has_an_always_run_exact_project_cleanup_fallback(self) -> None:
self.assertIn("- name: Always remove acceptance Docker resources", self.workflow)
cleanup = self.workflow.split(
" - name: Always remove acceptance Docker resources", 1
)[1].split(" - name: Upload acceptance evidence", 1)[0]
self.assertIn("if: always()", cleanup)
self.assertIn('label=com.docker.compose.project=${project}', cleanup)
self.assertIn("docker container rm --force", cleanup)
self.assertIn("docker volume rm --force", cleanup)
self.assertIn("docker network rm", cleanup)
self.assertIn("docker image rm --force", cleanup)
self.assertNotIn("docker system prune", cleanup)
def test_python_entrypoints_compile(self) -> None:
for path in (ACCEPTANCE, TRIVY_VALIDATOR):
result = subprocess.run(
["python", "-m", "py_compile", str(path)],
capture_output=True,
check=False,
)
with self.subTest(path=path.name):
self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", "replace"))
def test_trivy_validator_binds_image_and_rejects_blockers(self) -> None:
spec = importlib.util.spec_from_file_location("trivy_report", TRIVY_VALIDATOR)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
image_id = "sha256:" + "a" * 64
report = {
"ArtifactName": image_id,
"ArtifactType": "container_image",
"Metadata": {"ImageID": image_id},
"Results": [
{
"Target": "debian",
"Class": "os-pkgs",
"Type": "debian",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2099-9999",
"PkgName": "blocked-package",
"InstalledVersion": "1.0.0",
"FixedVersion": "1.0.1",
"Severity": "HIGH",
}
],
}
],
}
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "report.json"
path.write_text(json.dumps(report), encoding="utf-8")
result = module.validate(path, image_id)
self.assertEqual(result["release_blockers"], 1)
with self.assertRaisesRegex(ValueError, "does not match"):
module.validate(path, "sha256:" + "b" * 64)
def test_trivy_validator_rejects_duplicate_keys_and_empty_coverage(self) -> None:
spec = importlib.util.spec_from_file_location("trivy_report_negative", TRIVY_VALIDATOR)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
image_id = "sha256:" + "a" * 64
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "report.json"
path.write_text('{"ArtifactType":"container_image","ArtifactType":"container_image"}', encoding="utf-8")
with self.assertRaisesRegex(ValueError, "duplicate JSON key"):
module.validate(path, image_id)
path.write_text(
json.dumps(
{
"ArtifactType": "container_image",
"Metadata": {"ImageID": image_id},
"Results": [],
}
),
encoding="utf-8",
)
with self.assertRaisesRegex(ValueError, "no package result coverage"):
module.validate(path, image_id)
def test_trivy_validator_allows_only_exact_reviewed_upstream_unfixed_findings(self) -> None:
spec = importlib.util.spec_from_file_location("trivy_report_reviewed", TRIVY_VALIDATOR)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
image_id = "sha256:" + "a" * 64
finding = {
"VulnerabilityID": "CVE-2099-0001",
"PkgName": "example-package",
"InstalledVersion": "1.0.0",
"FixedVersion": "",
"Severity": "HIGH",
}
report = {
"ArtifactType": "container_image",
"Metadata": {"ImageID": image_id},
"Results": [
{
"Target": "debian",
"Class": "os-pkgs",
"Vulnerabilities": [finding],
}
],
}
baseline = {
"schema_version": 1,
"vulnerabilities": [
{
"vulnerability_id": "CVE-2099-0001",
"package": "example-package",
"installed_version": "1.0.0",
"severity": "HIGH",
}
],
}
with tempfile.TemporaryDirectory() as directory:
report_path = Path(directory) / "report.json"
baseline_path = Path(directory) / "baseline.json"
report_path.write_text(json.dumps(report), encoding="utf-8")
baseline_path.write_text(json.dumps(baseline), encoding="utf-8")
summary = module.validate(report_path, image_id, baseline_path)
self.assertEqual(summary["reviewed_unfixed_high_critical"], 1)
self.assertEqual(summary["release_blockers"], 0)
finding["InstalledVersion"] = "1.0.1"
report_path.write_text(json.dumps(report), encoding="utf-8")
self.assertEqual(module.validate(report_path, image_id, baseline_path)["release_blockers"], 1)
finding["InstalledVersion"] = "1.0.0"
finding["FixedVersion"] = "1.0.2"
report_path.write_text(json.dumps(report), encoding="utf-8")
summary = module.validate(report_path, image_id, baseline_path)
self.assertEqual(summary["fixable_high_critical"], 1)
self.assertEqual(summary["release_blockers"], 1)
if __name__ == "__main__":
unittest.main()
+292
View File
@@ -0,0 +1,292 @@
name: Managed validation
# The gate on protected master. Before v1 this keyed off manifests at the repository root, and this
# repository has none — pyproject.toml, package.json and the lock files all live in backend/,
# frontend/, node-agent/ and runtime-worker/. The "full" profile therefore completed in nine seconds
# having run a whitespace check, a merge-marker scan and py_compile, and no test suite at all, while
# reporting success to a branch protection rule that required it.
#
# It now targets the component roots explicitly, and refuses to report success when a component that
# should have run tests ran none. A gate that passes because it found nothing to do is worse than no
# gate: it produces the paperwork of validation without the fact of it.
on:
# Public exports require explicit owner dispatch; fork PRs never reach private runners.
workflow_dispatch:
inputs:
profile:
description: Allowlisted validation profile
required: true
default: full
type: choice
options: [test, lint, typecheck, build, security, full]
permissions:
contents: read
concurrency:
group: managed-validation-${{ gitea.repository }}-${{ gitea.ref }}
cancel-in-progress: true
jobs:
full:
name: full
runs-on: ubuntu-latest
timeout-minutes: 45
env:
PROFILE: ${{ inputs.profile || 'full' }}
# An explicit interpreter path rather than PATH manipulation: how a runner propagates PATH
# between steps varies, and a validation gate should not depend on that detail.
VENV: /tmp/modelforge-validation-venv
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Production delivery policy
shell: bash
run: python3 -m unittest discover -s .gitea/tests -p 'test_*.py' -v
- name: Validate the requested profile
shell: bash
run: |
set -euo pipefail
case "${PROFILE}" in
test|lint|typecheck|build|security|full) ;;
*) echo "Profile is not allowlisted: ${PROFILE}" >&2; exit 2 ;;
esac
echo "profile=${PROFILE}"
echo "commit=$(git rev-parse HEAD)"
- name: Repository hygiene
shell: bash
run: |
set -euo pipefail
git diff --check
if git grep -nE '^(<<<<<<< |=======$|>>>>>>> )' -- . ':!*.lock' ':!*.patch'; then
echo "Unresolved merge markers detected" >&2
exit 1
fi
echo "hygiene: clean"
- name: Prepare the report directory
shell: bash
run: mkdir -p reports
# Deliberately not using setup-python/setup-node: this runs on a self-hosted runner whose
# image already carries both, and the previous workflow depended on that too. What changes is
# that a missing toolchain now stops the run instead of quietly reducing what gets validated.
- name: Toolchain
shell: bash
run: |
set -euo pipefail
command -v python3 >/dev/null || { echo "python3 is not on PATH" >&2; exit 1; }
command -v node >/dev/null || { echo "node is not on PATH" >&2; exit 1; }
command -v npm >/dev/null || { echo "npm is not on PATH" >&2; exit 1; }
echo " python $(python3 --version)"
echo " node $(node --version)"
echo " npm $(npm --version)"
python3 -m venv "${VENV}"
"${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet --upgrade pip
if [[ "${PROFILE}" == security || "${PROFILE}" == full ]]; then
command -v gitleaks >/dev/null || {
echo "gitleaks is required for the security profile" >&2
exit 1
}
"${VENV}/bin/python" -m pip install \
--disable-pip-version-check --quiet 'pip-audit==2.10.0'
fi
echo " venv $("${VENV}/bin/python" --version) at ${VENV}"
- name: Backend — install, lint, typecheck, test
id: backend
shell: bash
working-directory: backend
run: |
set -euo pipefail
"${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet -e '.[dev]'
if [[ "${PROFILE}" == lint || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m ruff check src tests
fi
if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m mypy src
fi
if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m pytest -q --no-header --junitxml=../reports/backend.xml
fi
- name: Node Agent — install, lint, typecheck, test
id: node_agent
shell: bash
working-directory: node-agent
run: |
set -euo pipefail
"${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet -e '.[dev]'
if [[ "${PROFILE}" == lint || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m ruff check src
fi
if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m mypy src
fi
if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m pytest -q --no-header --junitxml=../reports/node-agent.xml
fi
- name: Runtime Worker — install, lint, typecheck, test
id: runtime_worker
shell: bash
working-directory: runtime-worker
run: |
set -euo pipefail
"${VENV}/bin/python" -m pip install --disable-pip-version-check --quiet -e '.[dev]'
if [[ "${PROFILE}" == lint || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m ruff check src
fi
if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m mypy src
fi
if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then
"${VENV}/bin/python" -m pytest -q --no-header --junitxml=../reports/runtime-worker.xml
fi
- name: Console — install frozen, typecheck, test, build
id: console
shell: bash
working-directory: frontend
run: |
set -euo pipefail
npm ci
if [[ "${PROFILE}" == typecheck || "${PROFILE}" == full ]]; then npx tsc --noEmit; fi
if [[ "${PROFILE}" == test || "${PROFILE}" == full ]]; then
npx vitest run --reporter=junit --outputFile=../reports/frontend.xml
fi
if [[ "${PROFILE}" == build || "${PROFILE}" == full ]]; then npm run build; fi
- name: Security — secrets and vulnerable dependencies
if: ${{ env.PROFILE == 'full' || env.PROFILE == 'security' }}
shell: bash
run: |
set -euo pipefail
gitleaks git . --no-banner --redact --report-format json \
--report-path reports/gitleaks.json
"${VENV}/bin/python" -m pip_audit --strict --skip-editable --desc=on \
--format=json --output=reports/python-audit.json
cd frontend
npm audit --audit-level=high --omit=dev --json > ../reports/npm-audit.json
- name: Compose projections
if: ${{ env.PROFILE == 'full' || env.PROFILE == 'build' }}
shell: bash
run: |
set -euo pipefail
if ! command -v docker >/dev/null; then
echo "SKIPPED: no docker CLI on this runner; compose projections are validated by the"
echo "local release gate instead. This step never reports a pass it did not earn."
exit 0
fi
# Compose interpolation needs values, not live credentials. These fixed validation-only
# strings never reach a service and ensure the fail-closed production projection is
# actually parsed on every full/build run.
export MODELFORGE_POSTGRES_DB=modelforge_validation
export MODELFORGE_POSTGRES_ADMIN_PASSWORD=validation-admin-only
export MODELFORGE_MIGRATION_DB_PASSWORD=validation-owner-only
export MODELFORGE_RUNTIME_DB_PASSWORD=validation-runtime-only
export MODELFORGE_MIGRATION_DATABASE_URL=postgresql+psycopg://modelforge:validation-owner-only@postgres:5432/modelforge_validation
export MODELFORGE_RUNTIME_DATABASE_URL=postgresql+psycopg://modelforge_runtime:validation-runtime-only@postgres:5432/modelforge_validation
export MODELFORGE_OPERATOR_API_KEY=validation-operator-key-32-characters # gitleaks:allow — synthetic Compose interpolation only
export MODELFORGE_BACKUP_ENCRYPTION_KEY=dmFsaWRhdGlvbi1vbmx5LWtleS0zMi1ieXRlcw== # gitleaks:allow — base64 of a public validation-only string
export MODELFORGE_CORS_ORIGINS=https://modelforge.example.test
export VITE_API_BASE_URL=https://modelforge.example.test
export MODELFORGE_VERSION=1.2.1
export MODELFORGE_AGENT_CONTROL_PLANE_HOST_ADDRESS=127.0.0.1
export MODELFORGE_AGENT_CA_CERT_PATH=./config/ca.crt
failures=0
check() {
if docker compose "$@" config -q >/dev/null 2>&1; then
echo " OK $*"
else
echo " FAIL $*"; failures=$((failures + 1))
fi
}
check -f docker-compose.yml
for overlay in backup dr gpu node-agent node-recovery production runtime-worker; do
check -f docker-compose.yml -f "docker-compose.${overlay}.yml"
done
check -f docker-compose.yml -f docker-compose.node-agent.yml \
-f docker-compose.node-agent.private-ca.yml
check -f docker-compose.yml -f docker-compose.runtime-worker.yml \
-f docker-compose.runtime-worker.private-ca.yml
[[ "${failures}" -eq 0 ]] || { echo "${failures} projection(s) invalid" >&2; exit 1; }
- name: Configuration documentation is current
if: ${{ env.PROFILE == 'full' }}
shell: bash
run: |
"${VENV}/bin/python" scripts/generate_configuration_docs.py --check
# The rule that makes the rest of this meaningful. Every component above declares a manifest,
# so every component must have reported a test count. Zero tests where tests were expected is
# a failure, not a pass — that is exactly how the previous workflow reported success.
- name: Refuse a validation that silently ran no tests
if: ${{ env.PROFILE == 'full' || env.PROFILE == 'test' }}
shell: bash
run: |
set -euo pipefail
"${VENV}/bin/python" - <<'PY'
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
expected = {
"backend.xml": "backend",
"node-agent.xml": "node-agent",
"runtime-worker.xml": "runtime-worker",
"frontend.xml": "frontend",
}
reports = Path("reports")
failures = []
total = 0
for filename, component in expected.items():
path = reports / filename
if not path.is_file():
failures.append(f"{component}: no test report was produced")
continue
root = ET.parse(path).getroot()
suites = [root] if root.tag == "testsuite" else list(root.iter("testsuite"))
tests = sum(int(suite.get("tests", 0)) for suite in suites)
errors = sum(int(suite.get("errors", 0)) for suite in suites)
failed = sum(int(suite.get("failures", 0)) for suite in suites)
skipped = sum(int(suite.get("skipped", 0)) for suite in suites)
executed = tests - skipped
total += tests
print(f" {component:16} {tests:5} tests, {skipped} skipped, "
f"{failed} failed, {errors} errors")
if executed <= 0:
failures.append(f"{component}: {tests} tests collected, {executed} executed")
if failed or errors:
failures.append(f"{component}: {failed} failed, {errors} errors")
print(f" {'TOTAL':16} {total:5} tests")
if failures:
print("\nManaged validation refused:", file=sys.stderr)
for failure in failures:
print(f" - {failure}", file=sys.stderr)
sys.exit(1)
PY
- name: Validation summary
if: always()
shell: bash
run: |
echo "commit: $(git rev-parse HEAD)"
echo "profile: ${PROFILE}"
ls -l reports/ 2>/dev/null || echo "no reports directory"
# Gitea only lists dispatchable workflow files from the default branch. Reusing the dedicated
# workflow from the existing managed entry point lets an unmerged branch prove its exact public
# export on the server. Ordinary PR validation and every profile except an explicit `build`
# dispatch remain unchanged.
public_candidate_acceptance:
name: Public candidate server acceptance
if: ${{ gitea.event_name == 'workflow_dispatch' && inputs.profile == 'build' }}
uses: ./.gitea/workflows/public-candidate-acceptance.yml
with:
source_commit: ${{ gitea.sha }}
public_api_origin: https://modelforge.example.test
@@ -0,0 +1,213 @@
name: Public candidate server acceptance
# Builds only an exact, curated public-source commit in a disposable Compose namespace. This job
# never calls the Unraid deploy controller and cannot select the production deployment action.
on:
workflow_dispatch:
inputs:
source_commit:
description: Exact canonical 40-character commit SHA to export and validate
required: true
type: string
public_api_origin:
description: Bare API origin compiled into the candidate Console image
required: true
default: https://modelforge.example.test
type: string
workflow_call:
inputs:
source_commit:
description: Exact canonical 40-character commit SHA to export and validate
required: true
type: string
public_api_origin:
description: Bare API origin compiled into the candidate Console image
required: true
type: string
permissions:
contents: read
concurrency:
group: public-candidate-acceptance-${{ inputs.source_commit }}
cancel-in-progress: false
jobs:
acceptance:
name: Four images and isolated clean install
runs-on: ubuntu-latest
timeout-minutes: 240
env:
GITLEAKS_VERSION: 8.30.1
GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
TRIVY_VERSION: 0.74.0
TRIVY_SHA256: 2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a
steps:
- name: Validate immutable acceptance inputs
shell: bash
env:
SOURCE_COMMIT: ${{ inputs.source_commit }}
PUBLIC_API_ORIGIN: ${{ inputs.public_api_origin }}
run: |
set -euo pipefail
[[ "${SOURCE_COMMIT}" =~ ^[0-9a-f]{40}$ ]] || {
echo "source_commit must be an exact lowercase commit SHA" >&2; exit 2;
}
python3 - "${PUBLIC_API_ORIGIN}" <<'PY'
import sys
from urllib.parse import urlparse
parsed = urlparse(sys.argv[1])
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise SystemExit("public_api_origin must be an absolute HTTP(S) origin")
if parsed.path or parsed.params or parsed.query or parsed.fragment:
raise SystemExit("public_api_origin must be a bare origin without a path")
PY
- name: Check out the exact canonical source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.source_commit }}
fetch-depth: 0
persist-credentials: false
- name: Verify checkout and runner isolation toolchain
shell: bash
env:
SOURCE_COMMIT: ${{ inputs.source_commit }}
run: |
set -euo pipefail
[[ "$(git rev-parse HEAD)" == "${SOURCE_COMMIT}" ]]
[[ -z "$(git status --porcelain)" ]]
command -v python3 >/dev/null
command -v node >/dev/null
command -v docker >/dev/null
docker version
docker compose version
python3 -m unittest discover -s .gitea/tests -p 'test_*.py' -v
- name: Install checksum-pinned acceptance tools
shell: bash
run: |
set -euo pipefail
tool_root="$(mktemp -d /tmp/modelforge-acceptance-tools.XXXXXXXX)"
trivy_archive="${tool_root}/trivy.tar.gz"
curl --fail --location --show-error --retry 3 --retry-all-errors \
--output "${trivy_archive}" \
"https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz"
printf '%s %s\n' "${TRIVY_SHA256}" "${trivy_archive}" \
| sha256sum --check --strict
tar --extract --gzip --file "${trivy_archive}" \
--directory "${tool_root}" trivy
chmod 0755 "${tool_root}/trivy"
gitleaks_archive="${tool_root}/gitleaks.tar.gz"
curl --fail --location --show-error --retry 3 --retry-all-errors \
--output "${gitleaks_archive}" \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
printf '%s %s\n' "${GITLEAKS_SHA256}" "${gitleaks_archive}" \
| sha256sum --check --strict
tar --extract --gzip --file "${gitleaks_archive}" \
--directory "${tool_root}" gitleaks
chmod 0755 "${tool_root}/gitleaks"
printf 'TRIVY=%s\n' "${tool_root}/trivy" >> "${GITHUB_ENV}"
printf 'GITLEAKS=%s\n' "${tool_root}/gitleaks" >> "${GITHUB_ENV}"
- name: Export and validate the curated public source
shell: bash
env:
SOURCE_COMMIT: ${{ inputs.source_commit }}
run: |
set -euo pipefail
public_source="$(mktemp -d /tmp/modelforge-public-parent.XXXXXXXX)/candidate"
reports="$(mktemp -d /tmp/modelforge-public-reports.XXXXXXXX)"
node scripts/export-public-source.mjs \
--output "${public_source}" --report "${reports}/export-report.json"
(cd "${public_source}" && node scripts/validate-public-source.mjs)
"${GITLEAKS}" dir "${public_source}" --no-banner --redact \
--report-format json --report-path "${reports}/gitleaks.json"
source_date="$(git show -s --format=%cI "${SOURCE_COMMIT}")"
git -C "${public_source}" init --initial-branch=main
git -C "${public_source}" config user.name "ModelForge acceptance"
git -C "${public_source}" config user.email "acceptance@modelforge.invalid"
git -C "${public_source}" add --all
GIT_AUTHOR_DATE="${source_date}" GIT_COMMITTER_DATE="${source_date}" \
git -C "${public_source}" commit -m "Public candidate from ${SOURCE_COMMIT}"
printf 'PUBLIC_SOURCE=%s\n' "${public_source}" >> "${GITHUB_ENV}"
printf 'ACCEPTANCE_REPORTS=%s\n' "${reports}" >> "${GITHUB_ENV}"
- name: Build, scan and clean-install the public candidate
shell: bash
env:
PUBLIC_API_ORIGIN: ${{ inputs.public_api_origin }}
run: |
set -euo pipefail
cd "${PUBLIC_SOURCE}"
python3 scripts/rc_server_acceptance.py \
--public-api-origin "${PUBLIC_API_ORIGIN}" \
--trivy "${TRIVY}" \
--output acceptance-evidence \
--project-suffix "${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
- name: Always remove acceptance Docker resources
if: always()
shell: bash
run: |
set +e
cleanup_failed=0
suffix="$(printf '%s' "${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
| tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]' | tail -c 24)"
project="modelforge-rc-${suffix}"
for kind in container volume network; do
while IFS= read -r resource; do
[[ -n "${resource}" ]] || continue
case "${kind}" in
container) docker container rm --force "${resource}" || cleanup_failed=1 ;;
volume) docker volume rm --force "${resource}" || cleanup_failed=1 ;;
network) docker network rm "${resource}" || cleanup_failed=1 ;;
esac
done < <(docker "${kind}" ls --quiet \
--filter "label=com.docker.compose.project=${project}" 2>/dev/null)
done
if [[ -n "${PUBLIC_SOURCE:-}" && -d "${PUBLIC_SOURCE}/.git" ]]; then
candidate_commit="$(git -C "${PUBLIC_SOURCE}" rev-parse HEAD 2>/dev/null)"
version="$(tr -d '\r\n' < "${PUBLIC_SOURCE}/VERSION" 2>/dev/null)"
for image in modelforge-api modelforge-web modelforge-node-agent \
modelforge-runtime-worker; do
tag="${image}:${version}"
revision="$(docker inspect --format \
'{{index .Config.Labels "org.opencontainers.image.revision"}}' \
"${tag}" 2>/dev/null)"
if [[ -n "${candidate_commit}" && "${revision}" == "${candidate_commit}" ]]; then
docker image rm --force "${tag}" || cleanup_failed=1
fi
done
fi
leftovers="$(docker container ls --all --quiet \
--filter "label=com.docker.compose.project=${project}" 2>/dev/null)"
[[ -z "${leftovers}" ]] || {
echo "Acceptance containers remain after cleanup: ${leftovers}" >&2
exit 1
}
[[ "${cleanup_failed}" == 0 ]] || {
echo "One or more exact acceptance resources could not be removed" >&2
exit 1
}
- name: Upload acceptance evidence
if: always()
uses: https://gitea.com/actions/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7
with:
name: public-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}-${{ inputs.source_commit }}
path: |
${{ env.ACCEPTANCE_REPORTS }}/export-report.json
${{ env.ACCEPTANCE_REPORTS }}/gitleaks.json
${{ env.PUBLIC_SOURCE }}/PUBLIC_SOURCE_EXPORT.md
${{ env.PUBLIC_SOURCE }}/PUBLIC_SOURCE_MANIFEST.json
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/*.json
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/*.txt
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/release/*.json
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/release/*SHA256SUMS
if-no-files-found: warn
retention-days: 90
+132
View File
@@ -0,0 +1,132 @@
name: Unraid stable release deployment
on:
workflow_dispatch:
inputs:
release_tag:
description: Annotated stable release tag (for example, v1.2.1)
required: true
type: string
release_commit:
description: Exact lowercase 40-character commit SHA referenced by the tag
required: true
type: string
action:
description: Verify provenance only, or deploy the verified stable release
required: true
default: VERIFY_ONLY
type: choice
options:
- VERIFY_ONLY
- DEPLOY_STABLE_TO_PRODUCTION
concurrency:
group: unraid-production-itworx-modelforge
cancel-in-progress: false
permissions:
contents: read
jobs:
deploy:
name: Verify and optionally deploy a stable release
runs-on: unraid-deploy
timeout-minutes: 180
steps:
- name: Validate immutable dispatch inputs
shell: bash
env:
RELEASE_TAG: ${{ inputs.release_tag }}
RELEASE_COMMIT: ${{ inputs.release_commit }}
DEPLOY_ACTION: ${{ inputs.action }}
run: |
set -euo pipefail
if [[ "${GITHUB_REF:-}" != "refs/heads/master" ]]; then
echo "Stable production deployment must be dispatched from refs/heads/master" >&2
exit 2
fi
if [[ ! "${RELEASE_TAG}" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "release_tag must be a strict stable SemVer tag such as v1.2.1" >&2
exit 2
fi
if [[ ! "${RELEASE_COMMIT}" =~ ^[0-9a-f]{40}$ ]]; then
echo "release_commit must be an exact lowercase 40-character SHA" >&2
exit 2
fi
case "${DEPLOY_ACTION}" in
VERIFY_ONLY|DEPLOY_STABLE_TO_PRODUCTION) ;;
*) echo "action is not allowlisted: ${DEPLOY_ACTION}" >&2; exit 2 ;;
esac
- name: Check out the exact release commit with full history
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.release_commit }}
fetch-depth: 0
# The following provenance step performs an authenticated exact-ref fetch.
persist-credentials: true
- name: Verify stable release provenance
shell: bash
env:
RELEASE_TAG: ${{ inputs.release_tag }}
RELEASE_COMMIT: ${{ inputs.release_commit }}
run: |
set -euo pipefail
git fetch --force --no-recurse-submodules origin \
"refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}" \
"refs/heads/master:refs/remotes/origin/master"
tag_ref="refs/tags/${RELEASE_TAG}"
if [[ "$(git cat-file -t "${tag_ref}")" != "tag" ]]; then
echo "${RELEASE_TAG} must be an annotated tag" >&2
exit 1
fi
tag_object="$(git rev-parse "${tag_ref}^{tag}")"
tag_commit="$(git rev-parse "${tag_ref}^{commit}")"
head_commit="$(git rev-parse HEAD)"
if [[ "${tag_commit}" != "${RELEASE_COMMIT}" ]]; then
echo "Tag commit ${tag_commit} does not match release_commit ${RELEASE_COMMIT}" >&2
exit 1
fi
if [[ "${head_commit}" != "${RELEASE_COMMIT}" ]]; then
echo "Checked-out HEAD ${head_commit} does not match release_commit ${RELEASE_COMMIT}" >&2
exit 1
fi
if [[ ! -f VERSION ]]; then
echo "VERSION is missing at the release commit" >&2
exit 1
fi
version="$(tr -d '\r\n' < VERSION)"
expected_version="${RELEASE_TAG#v}"
if [[ "${version}" != "${expected_version}" ]]; then
echo "VERSION ${version} does not match release tag ${RELEASE_TAG}" >&2
exit 1
fi
if ! git merge-base --is-ancestor "${RELEASE_COMMIT}" refs/remotes/origin/master; then
echo "Release commit ${RELEASE_COMMIT} is not an ancestor of origin/master" >&2
exit 1
fi
printf 'DEPLOY_COMMIT=%s\n' "${RELEASE_COMMIT}" >> "${GITHUB_ENV}"
echo "Verified annotated ${RELEASE_TAG} object ${tag_object} at immutable commit ${RELEASE_COMMIT}"
- name: Verification-only result
if: ${{ inputs.action == 'VERIFY_ONLY' }}
shell: bash
run: echo "Stable release provenance verified; production was not changed."
- name: Deploy verified stable release to production
if: ${{ inputs.action == 'DEPLOY_STABLE_TO_PRODUCTION' }}
shell: bash
run: |
set -euo pipefail
test -n "${DEPLOY_COMMIT:-}"
docker exec gitea-deploy-control \
/opt/gitea-deploy/deploy.py deploy \
"${GITHUB_REPOSITORY}" "${DEPLOY_COMMIT}"
+123
View File
@@ -0,0 +1,123 @@
# Python environments, bytecode, analysis and test output
__pycache__/
*.py[cod]
*$py.class
.venv/
venv/
env/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.hypothesis/
.tox/
.nox/
.coverage
.coverage.*
coverage.xml
htmlcov/
*.egg-info/
.eggs/
# Node/frontend dependencies, caches and build output
node_modules/
dist/
build/
.vite/
.turbo/
.parcel-cache/
.npm/
.yarn/
.pnpm-store/
.playwright-mcp/
playwright-report/
test-results/
*.tsbuildinfo
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Local environment and credentials. Safe examples are explicitly allowed.
.env
.env.*
!.env.example
!.env.*.example
# Credential *material*, not every filename that mentions credentials. The bare substring globs
# that used to live here matched source: backend/tests/test_credential_security_m16.py — the entire
# M16 credential security suite, 35 tests — and docs/security/CREDENTIAL_THREAT_TESTS.md were
# silently excluded from the repository while being reported as delivered.
*.credentials
*.secret
*.token
credentials.json
credentials.yaml
credentials.yml
secrets.json
secrets.yaml
secrets.yml
*.pem
*.key
*.p12
*.pfx
id_rsa*
id_ed25519*
# IDE, OS and local browser/session state
.vscode/
.idea/
*.suo
*.user
*.userosscache
*.sln.docstates
.DS_Store
Thumbs.db
Desktop.ini
*.stackdump
# ModelForge runtime, container and generated evidence state
.data/
.state/
state/
data/
# Model weights, not configuration. This rule used to be a bare `models/`, which also matched
# config/models/ and silently excluded the candidate registry manifest the control plane
# needs at startup — a clean clone of the repository could not start.
/models/
model-artifacts/
hf-cache/
huggingface/
.cache/huggingface/
artifacts/
.quarantine/
quarantine/
downloads/
container-state/
postgres-data/
redis-data/
runtime-traces/
traces/
benchmarks/results/
benchmarks/generated/
*.sqlite
*.sqlite3
*.db
*.db-shm
*.db-wal
# Model weights and derived checkpoints are never source artifacts.
*.safetensors
*.gguf
*.bin
*.pt
*.pth
*.ckpt
*.onnx
# Logs and temporary output
*.log
logs/
tmp/
temp/
*.tmp
*.bak
dist-*/
+262
View File
@@ -0,0 +1,262 @@
# Changelog
Operator- and user-facing changes. Internal refactoring, test additions and documentation-only
commits are not listed unless they change what an operator sees or does.
## Unreleased
### Security
- Runtime Worker packaging now upgrades the digest-pinned Ubuntu base before installation and pins
the inference stack plus inherited Python security dependencies to reviewed fixed versions. The
public-candidate gate scans the resulting fourth image instead of treating the private worker as
an unexamined exception.
- The Runtime Worker removes unneeded kernel-development headers after installation, eliminating
their upstream-unfixed vulnerability surface without removing required runtime libraries.
- Public candidate scans distinguish fixable or newly observed HIGH/CRITICAL findings from an
exact reviewed upstream-unfixed baseline. A changed CVE, package, installed version, severity or
newly available fix fails closed.
- Node enrollment and publisher credentials now require their exact fixed scopes at both service
and database boundaries. Schema `20260830_0023` validates every existing row before adding the
constraints and refuses the upgrade when malformed legacy scope data exists.
- Request-body streaming now has finite ASGI event/progress budgets in addition to byte limits;
progressless frame floods receive the typed `request_body_progress_exhausted` response.
- PostgreSQL role provisioning now removes the database's default public temporary-table grant;
production startup continues to attest that the runtime role cannot create or use it.
### Fixed
- Fresh schema `20260830_0024` installs no longer let Psycopg misread PostgreSQL `%rowtype` and
`%I` syntax as client placeholders.
- Public-candidate acceptance now works across the Gitea sibling Docker daemon while retaining
isolated volumes, private workers, exact HTTP contract probes and deterministic cleanup.
## v1.2.1 — 2026-08-30
A packaging and provenance patch. No feature, API, schema or runtime-behaviour change; schema stays
`20260828_0022` and no migration runs. It exists because v1.2.0's production acceptance found two
defects that every existing gate was blind to — both in the artifacts rather than the source.
### Fixed
- **The published console could not reach its own API.** Vite inlines `VITE_API_BASE_URL` at build
time, but the release build never passed it, so every release image since v1.1.0 compiled the
Dockerfile's development default `http://localhost:8000` into an immutable bundle — and the nginx
CSP, derived from the same argument, hardcoded the same wrong origin. A release build now requires
an explicit `--public-api-origin` (or `MODELFORGE_PUBLIC_API_ORIGIN`) and refuses to package
without one.
- **The Node Agent's identity could drift from its tag.** `docker-compose.node-agent.yml` declared a
build with no arguments, so a Compose-built agent carried version `0.0.0` and empty
revision/created labels while still being tagged from `MODELFORGE_VERSION`. Production ran an
image tagged `1.1.1` whose contents were `1.2.0`. The projection now passes the release identity,
as the API and console projections already did.
### Added
- `scripts/release_image_acceptance.py`: a gate that inspects the **built** console image — its
compiled bundle, its rendered CSP and its OCI labels — rather than the source that produced it.
The release build runs it before packaging and refuses to publish an image that cannot reach the
API it was built for. Verified against the exact image that broke production.
- Regression coverage for both defects: the release build fails closed without an origin, the origin
reaches the console build, the CSP names the same origin and is never widened to a wildcard, every
release image declares complete OCI labels, and every Compose projection that builds a published
image passes the release identity.
### Operations
- The release manifest now records the console's compiled-in API origin, so an operator can see it
without unpacking the image.
- Local development is unchanged: `docker compose up` still defaults to `http://localhost:8000`.
Fail-closed applies to the release path only.
## v1.2.0 — 2026-08-29
The Premium Operator Console release. No API contract, domain model, runtime behavior or database
schema changed; schema stays `20260828_0022` and no migration runs.
### Added
- A production command center built only from backend-authoritative evidence, stating unknowns as
unknown rather than as zero.
- Quick jump (Ctrl+K): a modal command palette over all 13 workspaces, matching name, domain and
keywords.
- An evidence timeline of recent control-plane activity, each entry linking to the workspace where
it can be verified.
- A split-pane Registry detail view covering upstream facts, local governance, revisions, artifacts
and provenance.
- One console-wide guarded destructive-action dialog showing identity, dependency posture and
permanent impact before an explicit acknowledgement unlocks the confirm control.
### Changed
- Workspaces are grouped by operator mental model (Command, Model supply, Serving, Infrastructure,
Assurance, Projects) while every existing route hash is preserved.
- One refined design token layer for light and dark, with semantic status roles that always include
text and explicit focus rings.
- Every route is usable from 1440 px down to a 390 px mobile viewport through a modal navigation
drawer, with no horizontal overflow at any tested width.
### Fixed
- The ARIA tabs pattern is complete across all six tabbed workspaces: every tab carries a stable
`id` and `aria-controls`, and every panel is a real `role="tabpanel"` naming its tab back.
- Lifecycle, Migrations and Recovery tablists gained roving `tabindex`, an accessible tablist name
and Left/Right/Home/End keyboard movement, which they previously lacked entirely.
- Capability residency unload no longer uses a native `window.confirm`; it uses the console safety
dialog and shows the VRAM released and the cold-load cost the next request will pay.
- Operator token fields in Operations, Recovery, Lifecycle and Migrations now sit in a real form
with a stable `id`, a `name` and an explicit `<label for>`.
### Accessibility
- Skip link to a programmatically focusable main landmark.
- Quick jump and the mobile drawer contain focus, dismiss on Escape and restore focus to the control
that opened them.
- Status is never encoded by colour alone.
- `prefers-reduced-motion` collapses animation and transition durations; `forced-colors: active`
re-expresses focus and active navigation in system colours.
- Essential mobile controls and navigation rows meet a 44 px target.
### UX
- Warnings state the observed condition and name the workspace where an operator can resolve it.
- Destructive registry and residency actions present impact before they present a confirm control.
- Surfaces use compact rows, quiet borders and a restrained status palette rather than decorative
effect.
### Security / Operations
- The operator credential stays request-scoped: no `localStorage`, no `sessionStorage`, no cookie,
no URL. Every token form prevents its own default submission so nothing is serialized into a query
string.
- `autocomplete="off"` is retained deliberately on operator-token fields rather than moving to
`current-password`, which would invite a password manager to persist and sync a high-privilege
control-plane credential.
- The Node Agent's functional behavior is unchanged from the NVIDIA-certified v1.1.1
implementation; it is rebuilt only because `VERSION` is the single source of truth for packaged
manifests. No re-enrollment is required.
### Known limitations
- The console has no committed visual-regression baseline; visual acceptance is a human-reviewed
browser pass.
- Point-in-time database recovery remains `NOT_SUPPORTED`.
## v1.1.1 — 2026-08-29
### Fixed
- Replaced the Node Agent's Alpine/musl runtime with a digest-pinned Debian/glibc runtime so the
NVIDIA Container Toolkit can inject and load GPU Node's glibc-linked NVML driver library.
- NVIDIA-configured agents now validate real NVML inventory and matching telemetry before
enrollment or publication. Missing NVML, an empty device list, or missing GPU telemetry exits
with a typed diagnostic instead of publishing an apparently healthy zero-GPU node.
### Security / Operations
- The Node Agent remains UID 100/GID 101, read-only, capability-free and
`no-new-privileges`; its multi-stage image contains no compiler or package installer.
- CPU-only nodes remain supported through the explicit `cpu` accelerator mode. Canonical GPU
Compose projections set `nvidia` and fail closed.
### Known limitations
- External workloads can legitimately trigger `EXTERNAL_GPU_PRESSURE`; ModelForge observes but
does not terminate those workloads.
## v1.1.0 — 2026-08-28
### Added
- Operator-only compute-node decommission preview and execute APIs, with exhaustive fail-closed
blockers, generation/digest concurrency checks, idempotent exactly-once audit and retained
provenance.
- A Console danger-zone flow that explains blockers, cleanup and preserved history before requiring
a reason, operator and exact typed node identity.
- A terminal node invariant and runbook. Decommissioned identities cannot ordinarily re-enroll,
rotate credentials, publish agent state, refresh inventory or return to scheduler placement.
### Changed
- The runtime schema target is `20260828_0022`. Schema `20260827_0021` remains a supported
upgrade source, not a runtime-compatible schema; rollback requires the verified pre-upgrade
backup because downgrade migrations are intentionally unsupported.
- The Console now uses shared design tokens, persistent light/dark themes, grouped responsive
navigation, page-specific headers and keyboard-visible focus.
### Fixed
- API clients preserve typed error envelopes and correlation IDs, and accept successful empty HTTP
responses without attempting to decode JSON.
- Failure and recovery states retain meaningful visual styling in both themes.
- The Node Agent Compose projection accepts an exact release image tag or digest while retaining an
explicitly local source-build workflow; it never falls back to `latest`.
### Security / Operations
- Decommission revokes active node credentials, prevents old identities from resurrecting, removes
disposable current state and preserves historical provenance and an exactly-once audit event.
- Release tooling binds versioned images, CycloneDX SBOM, provenance and checksums to an exact source
commit without mutating floating `latest` tags.
- Published image bases are digest-pinned and security-updated; Python runtime images remove pip and
setuptools after dependency verification, and all three components retain dedicated non-root
runtime users.
### Known limitations
- Point-in-time recovery remains `NOT_SUPPORTED`; verified snapshot restore is the recovery model.
- SQLite remains an isolated unit-test backend and is not supported for production.
- One historical M13/M15 LAB cutover remains explicitly unreconciled because external Qdrant truth
is unavailable; it has no production deployment, gateway or scheduler dependency.
## v1.0.0 — 2026-08-27
The first stable release. See
[docs/RELEASE_NOTES_v1.0.0.md](docs/RELEASE_NOTES_v1.0.0.md) for the full description.
### Added
- One authoritative product version, `1.0.0`, with every packaged manifest checked against it.
- `GET /api/v1/version` — build identity and compatibility, unauthenticated.
- Declared compatibility ranges for the database schema and the agent protocol, answered as
`COMPATIBLE`, `TOO_OLD`, `TOO_NEW` or `UNKNOWN` — never a silent pass.
- Fail-closed startup validation in production: missing or weak operator key, development database
password, remote code execution, missing backup encryption key, wildcard CORS origin, invalid URL,
unwritable storage root, incompatible schema or PostgreSQL major, and impossible policy
combinations.
- A production deployment overlay that refuses to render without its secrets.
- `scripts/preflight.py`, `scripts/bootstrap.py`, `scripts/upgrade.py` and
`scripts/release_build.py`.
- A Content-Security-Policy on the operator console.
- OCI image labels and build identity arguments on all three images.
- Configurable host ports for every published service.
- Operator documentation: installation, first run, upgrade, configuration, compatibility, node
agent, Unraid deployment, capabilities, project integration, operations and troubleshooting.
### Changed
- `GET /api/v1/system` reports `release_channel` instead of `milestone`.
- The console no longer displays development milestone labels.
- `.env.example` and the configuration reference are generated from the typed settings.
- The Runtime Worker's inference stack moved to an optional `inference` extra, so lint, typecheck
and its tests no longer require the full PyTorch stack.
### Fixed
- **A clean clone could not start.** A `models/` ignore rule intended for model weights also matched
`config/models/`, so the candidate registry manifest was never in the repository.
- **Migrations ran against the wrong database.** The Alembic environment discarded an explicitly
supplied URL and used the configured one, so an upgrade rehearsal aimed at an isolated copy would
have migrated the deployment's own database.
- **The disaster-recovery projection published its PostgreSQL to the LAN**, reachable as a superuser
with development credentials.
- **The readiness probe re-read every manifest on every call** — 77 file reads and 528 ms per probe.
- **The Node Agent pinned an old control-plane version**, making its image unbuildable at release.
- **The console rendered a "not implemented" panel over the delivered Recovery workspace**, because
the placeholder list had drifted from the navigation.
- **Managed validation reported success without running any test suite.**
### Known limitations
Point-in-time recovery is `NOT_SUPPORTED`. Upgrades require downtime. Model Lab and Benchmarks have
no console workspace. See the release notes for the full list.
+46
View File
@@ -0,0 +1,46 @@
# Contributing
Thank you for improving ModelForge. Small, focused changes with tests and an explicit security
impact are easiest to review.
## Set up
ModelForge requires Python 3.12, Node.js, npm, Docker Engine 24+ and Docker Compose 2+. Each Python
component has its own package metadata; the frontend uses its committed npm lockfile.
```bash
python -m venv .venv
. .venv/bin/activate
python -m pip install -e 'backend[dev]' -e 'node-agent[dev]' -e 'runtime-worker[dev]'
cd frontend && npm ci
```
On Windows, activate with `.venv\Scripts\Activate.ps1`.
## Before opening a change
```bash
python -m ruff check backend/src backend/tests node-agent/src runtime-worker/src
python -m mypy backend/src
python -m mypy --config-file node-agent/pyproject.toml node-agent/src
python -m mypy --config-file runtime-worker/pyproject.toml runtime-worker/src
python -m pytest backend/tests node-agent/tests runtime-worker/tests -q
cd frontend && npm test -- --run && npm run build
```
Also run `git diff --check` and Gitleaks. Do not commit `.env`, credentials, private hostnames,
production evidence, model weights, generated backups or personal data.
## Design rules
- Applications depend on versioned capabilities, never concrete model names.
- Production identities use exact commits, artifact digests and image digests.
- No production promotion occurs without evidence and explicit approval.
- `trust_remote_code` stays false; untrusted model code is not executed.
- Agents and runtime workers use narrow identities and least privilege.
- Destructive actions require a bounded plan, current preconditions and an audit record.
- Do not introduce Kubernetes or inbound compute-node orchestration.
Update user documentation when behavior or configuration changes. Add a migration for schema
changes and prove both the upgrade chain and the PostgreSQL privilege boundary. Security issues must
follow [SECURITY.md](SECURITY.md), not the public issue tracker.
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
+5
View File
@@ -0,0 +1,5 @@
# Curated public source export
Generated from private canonical revision `b8755394117affad0178a79a627cf5574d3c8eef`.
This parentless candidate excludes private operational history and uses synthetic example identifiers.
File diff suppressed because it is too large Load Diff
+141
View File
@@ -0,0 +1,141 @@
# ITWorx ModelForge
ModelForge is a self-hosted control plane for running AI models on hardware you control. Applications
ask for a stable capability such as `rag.embedding@1`; ModelForge selects an approved model revision,
places it on an eligible compute node, and retains the evidence needed to explain and roll back that
decision.
Current release: **1.2.1**
## What you get
- A web console for models, hardware, capabilities, deployments, incidents and recovery.
- A FastAPI control plane with capability-scoped service credentials.
- Outbound-only node agents: compute nodes connect to the control plane and need no inbound agent port.
- Quarantined model acquisition with immutable revisions, SHA-256 evidence and static inspection.
- GPU-aware placement, leases, load-on-demand residency and typed runtime workers.
- Approval-gated promotion, canary and rollback workflows with an append-only audit chain.
- Backup, restore rehearsal, observability and operational runbooks.
ModelForge is designed for a small self-hosted fleet. It uses Docker Compose rather than Kubernetes,
and a GPU is needed only on nodes that serve GPU-backed capabilities.
## Quick start
Requirements: Docker Engine 24+, Docker Compose 2+, Python 3.12 for the host-side tools, and at least
50 GiB free plus room for model weights.
```bash
python scripts/preflight.py
cp .env.example .env
# Generate and enter the distinct secrets described in docs/INSTALLATION.md.
docker compose -f docker-compose.yml -f docker-compose.production.yml up -d --build
```
Then verify the control plane and open the console:
```bash
curl http://127.0.0.1:8000/api/v1/health/live
curl http://127.0.0.1:8000/api/v1/health/ready
```
- Console: `http://127.0.0.1:3000`
- API documentation: `http://127.0.0.1:8000/docs`
`docker-compose.yml` by itself is for development. The production overlay requires separate
bootstrap, migration-owner and runtime database credentials and applies the production startup
guards. Follow the complete [installation guide](docs/INSTALLATION.md) before exposing an instance.
## First useful workflow
1. Enrol a compute node with a short-lived, single-use token.
2. Discover a model and resolve it to an immutable upstream commit.
3. Approve a download plan; the node agent quarantines, verifies and atomically promotes the files.
4. Register a versioned capability contract and promote an evidence-backed deployment.
5. Give an application a credential scoped only to the capability it needs.
The application calls a stable endpoint such as:
```bash
curl -X POST \
-H "Authorization: Bearer $MODELFORGE_CLIENT_CREDENTIAL" \
-H "Content-Type: application/json" \
-d '{"input":["text to embed"]}' \
http://127.0.0.1:8000/api/v1/capabilities/rag.embedding@1/invoke
```
The caller does not need to know the model name, artifact path, runtime or GPU node. See
[First run](docs/FIRST_RUN.md) and [Project integration](docs/PROJECT_INTEGRATION.md).
## Architecture
```text
Applications -> Capability Gateway -> Scheduler -> typed runtime workers
| ^
v |
Control Plane <--- outbound node agents
|
PostgreSQL + Redis + verified artifacts
```
Important boundaries:
- exact model commits and container digests replace mutable names;
- `trust_remote_code` remains disabled;
- production changes require evidence and explicit approval;
- compute agents publish observations but cannot act as operators;
- runtime workers have no external network access by default;
- destructive and recovery actions are planned, bounded and audited.
The [system architecture](docs/architecture/SYSTEM_ARCHITECTURE.md),
[threat model](docs/security/THREAT_MODEL.md) and
[model supply-chain policy](docs/security/MODEL_SUPPLY_CHAIN_POLICY.md) describe these guarantees in
detail.
## Documentation
| Goal | Guide |
| --- | --- |
| Install or upgrade | [Installation](docs/INSTALLATION.md) · [Upgrade](docs/UPGRADE.md) |
| Enrol a compute node | [Node Agent](docs/NODE_AGENT.md) |
| Configure the platform | [Configuration](docs/CONFIGURATION.md) |
| Integrate an application | [Project integration](docs/PROJECT_INTEGRATION.md) |
| Operate and recover it | [Operations](docs/OPERATIONS.md) · [Troubleshooting](docs/TROUBLESHOOTING.md) |
| Review security | [Security](docs/SECURITY.md) · [Threat model](docs/security/THREAT_MODEL.md) |
| Understand compatibility | [Compatibility](docs/COMPATIBILITY.md) |
| See release changes | [Changelog](CHANGELOG.md) · [1.2.1 release notes](docs/RELEASE_NOTES_v1.2.1.md) |
## Repository layout
```text
backend/ FastAPI control plane, persistence and migrations
frontend/ React operator console
node-agent/ Outbound compute-node observer and artifact acquirer
runtime-worker/ Isolated typed model runtime
config/ Example capability, policy and model manifests
docs/ User, architecture, operations and security documentation
scripts/ Preflight, bootstrap, release and recovery tools
```
## Development
Each component keeps its own pinned dependencies and tests. The protected-branch validation installs
all four components, runs linting, strict type checks, tests and the frontend build, verifies Compose
projections, scans for secrets, and audits Python and production npm dependencies.
```bash
python -m pytest backend/tests -q
python -m pytest node-agent/tests -q
python -m pytest runtime-worker/tests -q
cd frontend && npm ci && npm test -- --run && npm run build
```
See [CONTRIBUTING.md](CONTRIBUTING.md) before proposing a change and [SECURITY.md](SECURITY.md) for
private vulnerability reporting.
## License status
ModelForge is licensed under **GNU AGPL-3.0-or-later**. If you run a modified version for users over
a network, the AGPL requires that those users can obtain the corresponding source. See
[LICENSE](LICENSE). Downloaded models, model weights and datasets retain their own upstream license
terms and are not relicensed by ModelForge.
+32
View File
@@ -0,0 +1,32 @@
# Security policy
## Supported version
Security fixes are made for the latest released minor version. At the time of publication that is
ModelForge 1.2.x. Upgrade to the latest patch before reporting a problem that may already be fixed.
## Reporting a vulnerability
Do not open a public issue for a suspected vulnerability, leaked credential, private topology or
exploit. Send the report privately to **security@itworx.tech** with:
- the affected version or commit;
- the component and reachable entry point;
- reproduction steps or a minimal proof of concept;
- the impact and any prerequisites you observed;
- whether you believe active exploitation or credential exposure occurred.
Do not access data that is not yours, degrade a running service, persist access, or publish the
details before a fix is available. We will acknowledge a usable report, coordinate validation and
credit, and publish an advisory when users have a remediation.
The repository owner must confirm that `security@itworx.tech` is a monitored mailbox before the
public repository is enabled. Until then, contact the owner privately through the repository host.
## Security model
Model artifacts are untrusted input. ModelForge resolves immutable upstream revisions, keeps
downloads in quarantine, verifies their size and digest, performs static inspection, disables
remote code, and requires evidence plus human approval before production promotion. See
[docs/security/THREAT_MODEL.md](docs/security/THREAT_MODEL.md) and
[docs/security/MODEL_SUPPLY_CHAIN_POLICY.md](docs/security/MODEL_SUPPLY_CHAIN_POLICY.md).
+1
View File
@@ -0,0 +1 @@
1.2.2
+5
View File
@@ -0,0 +1,5 @@
__pycache__
*.pyc
.pytest_cache
.ruff_cache
tests
+52
View File
@@ -0,0 +1,52 @@
FROM python:3.12-alpine3.23@sha256:31a768b01976652c222e318fe5bd6e7c252f056cbf489c88fa256f1bf0af58e3
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# PostgreSQL 17 client tools. ModelForge owns its own consistent logical backup and restore and
# must never fall back to copying a live data directory, so pg_dump/pg_restore/psql ship with the
# control plane and are pinned to the same major version as the server. Upgrade the signed Alpine
# repository packages during the build so a digest-pinned base does not retain already-fixed CVEs.
ARG POSTGRES_MAJOR=17
RUN set -eux; \
apk upgrade --no-cache; \
apk add --no-cache ca-certificates "postgresql${POSTGRES_MAJOR}-client"
# Build identity. These are stamped in at build time so a running container can say exactly
# where it came from; an argument that is never passed stays empty and is reported as null
# rather than becoming a claimed commit.
ARG MODELFORGE_VERSION=0.0.0
ARG MODELFORGE_COMMIT=""
ARG MODELFORGE_BUILT_AT=""
ENV MODELFORGE_BUILD_COMMIT=${MODELFORGE_COMMIT}
ENV MODELFORGE_BUILD_TIMESTAMP=${MODELFORGE_BUILT_AT}
LABEL org.opencontainers.image.title="ITWorx ModelForge control plane"
LABEL org.opencontainers.image.description="Capability-first local AI ModelOps and GPU control plane"
LABEL org.opencontainers.image.version="${MODELFORGE_VERSION}"
LABEL org.opencontainers.image.revision="${MODELFORGE_COMMIT}"
LABEL org.opencontainers.image.created="${MODELFORGE_BUILT_AT}"
LABEL org.opencontainers.image.source="https://git.example.com/example/modelforge.git"
LABEL org.opencontainers.image.vendor="ITWorx"
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
WORKDIR /app
COPY pyproject.toml ./
COPY src ./src
COPY alembic.ini ./
COPY alembic ./alembic
# Upgrade the installer before it resolves anything: the pinned base image ships a pip
# carrying archive-extraction advisories. pip never runs at runtime, but a release image
# should not carry a known-vulnerable installer.
RUN pip install --no-cache-dir --upgrade pip "setuptools>=78.1.1" "msgpack>=1.2.1" \
&& pip install --no-cache-dir . \
&& pip check \
&& python -m pip uninstall --yes pip setuptools
RUN addgroup -S modelforge && adduser -S -G modelforge -h /app modelforge \
&& mkdir -p /data/state /data/hf-cache /data/artifacts /data/quarantine \
/data/backups /data/restore \
&& chown -R modelforge:modelforge /app /data
USER modelforge
EXPOSE 8000
CMD ["uvicorn", "modelforge_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
+34
View File
@@ -0,0 +1,34 @@
[alembic]
script_location = alembic
prepend_sys_path = src
# Left empty deliberately. The URL comes from -x db_url=..., from a programmatic
# set_main_option, or from MODELFORGE_DATABASE_URL — in that order. A hardcoded value
# here is one an operator can migrate the wrong database with.
sqlalchemy.url =
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
+76
View File
@@ -0,0 +1,76 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context
from modelforge_api.persistence.models import Base
from modelforge_api.settings import get_settings
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
def _database_url() -> str:
"""Where the migration runs, in order of explicitness.
1. `-x db_url=...` on the command line
2. a URL the caller set programmatically, or in alembic.ini
3. MODELFORGE_MIGRATION_DATABASE_URL
4. MODELFORGE_DATABASE_URL only outside production
env.py used to overwrite whatever the caller had set with the settings default
unconditionally, so both explicit forms were silently discarded. A bootstrap or upgrade
rehearsal aimed at an isolated copy would have migrated the deployment's own database while
reporting success against the copy.
"""
supplied = context.get_x_argument(as_dictionary=True).get("db_url")
if supplied:
return str(supplied)
configured = config.get_main_option("sqlalchemy.url", None)
if configured:
return configured
settings = get_settings()
if settings.migration_database_url is not None:
return settings.migration_database_url.get_secret_value()
if settings.env == "production":
raise RuntimeError(
"production migrations require MODELFORGE_MIGRATION_DATABASE_URL; the API runtime "
"credential is intentionally not a migration credential"
)
return settings.database_url
config.set_main_option("sqlalchemy.url", _database_url())
target_metadata = Base.metadata
def run_migrations_offline() -> None:
context.configure(
url=config.get_main_option("sqlalchemy.url"),
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+20
View File
@@ -0,0 +1,20 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
@@ -0,0 +1,222 @@
"""M0 domain foundation.
Revision ID: 20260824_0001
Revises: None
"""
import sqlalchemy as sa
from alembic import op
from modelforge_api.persistence.models import Base
revision = "20260824_0001"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# The declarative metadata is the reviewed M0 schema contract. create_all is
# used only from this baseline migration; later changes require explicit ops.
Base.metadata.create_all(bind=op.get_bind(), checkfirst=False)
# Freeze the original M0 shape even though the declarative metadata evolves.
# Remove newer foreign-key columns before dropping the tables they target.
for column in (
"profile_config",
"health_contract",
"device_policy",
"modality",
"dtype",
"version",
"artifact_set_id",
"runtime_environment_id",
):
op.drop_column("runtime_profiles", column)
for table in (
# Later milestone tables are present in the evolving declarative metadata
# used to build this baseline. Drop dependants before their M5/M0 targets
# so a fresh upgrade still reconstructs each historical revision exactly.
"audit_chain_heads",
"node_decommission_operations",
# M15 recovery tables come first: artifact_recovery_operations references
# artifact_jobs, download_plans, artifact_sets, model_revisions and storage_roots,
# all of which are dropped further down this list.
"artifact_recovery_operations",
"restore_operation_events",
"restore_operations",
"restore_plans",
"backup_manifest_entries",
"backup_sets",
"recovery_asset_records",
"recovery_policy_revisions",
"incident_timeline_events",
"alert_history_events",
"operational_alerts",
"operational_incidents",
"maintenance_windows",
"alert_rule_revisions",
"slo_evaluations",
"slo_policy_revisions",
"service_level_indicators",
"capacity_aggregates",
"capacity_snapshots",
"migration_events",
"migration_cutover_operations",
"migration_rollback_snapshots",
"migration_shadow_sessions",
"migration_validation_snapshots",
"migration_batch_checkpoints",
"migration_plans",
"migration_validation_policy_revisions",
"artifact_location_removal_records",
"lifecycle_canary_runs",
"lifecycle_operations",
"lifecycle_rollback_snapshots",
"lifecycle_promotion_plans",
"lifecycle_approval_evidence",
"lifecycle_approval_requests",
"lifecycle_cleanup_plans",
"lifecycle_retention_records",
"retention_policy_revisions",
"lifecycle_events",
"lifecycle_policy_revisions",
"lifecycle_subjects",
"project_fit_evidence",
"scheduler_evictions",
"co_residency_evidence",
"placement_plans",
"scheduler_policy_revisions",
"scheduler_accelerator_states",
"capability_evaluation_runs",
"capability_evaluation_suites",
"reranking_case_results",
"reranking_evaluation_runs",
"retrieval_candidate_pools",
"advisor_recommendations",
"retrieval_pipeline_identities",
"discovery_candidate_assessments",
"advisor_policies",
"model_comparisons",
"evaluation_comparisons",
"evaluation_case_results",
"evaluation_runs",
"evaluation_cases",
"evaluation_suite_revisions",
"evaluation_suites",
"embedding_migrations",
"project_evaluation_bindings",
"serving_gpu_leases",
"serving_jobs",
"gateway_requests",
"residency_allocations",
"service_credentials",
"service_clients",
"capability_resource_envelopes",
"capability_experiment_routes",
"capability_deployments",
"embedding_spaces",
"production_execution_approvals",
"deployment_candidates",
"runtime_probe_metrics",
"runtime_probes",
"execution_approvals",
"runtime_compatibility_assessments",
"artifact_set_members",
"artifact_inspections",
"artifact_job_attempts",
"artifact_jobs",
"download_plan_files",
"download_plans",
"artifact_sets",
"upstream_files",
"upstream_snapshots",
"artifact_locations",
"derived_artifact_sources",
"storage_roots",
):
op.drop_table(table)
op.drop_table("runtime_environments")
for column in ("source_type", "upstream_metadata", "local_metadata", "interpretation_metadata", "description"):
op.drop_column("models", column)
op.drop_column("model_revisions", "archived_at")
op.drop_column("audit_events", "canonical_payload")
op.drop_column("audit_events", "hash_format")
for column in ("status", "verification_details", "archived_at"):
op.drop_column("model_artifacts", column)
for column in ("transformation_type", "environment_snapshot", "status", "verification_details"):
op.drop_column("derived_artifacts", column)
for table in (
"node_credentials",
"node_enrollments",
"hardware_inventory_runs",
"storage_volume_states",
"accelerator_telemetry_latest",
"host_telemetry_latest",
):
op.drop_table(table)
for column in (
"inventory_at",
"last_seen_at",
"first_seen_at",
"inventory_source",
"status_reason",
"status",
"mig_mode_current",
"total_vram_bytes",
"compute_capability_minor",
"compute_capability_major",
"architecture",
"vendor",
"pci_bus_id",
):
op.drop_column("accelerators", column)
for column in (
"decommissioned_by",
"decommission_reason",
"decommissioned_at",
"generation",
"telemetry_sequence",
"inventory_sequence",
"last_connection_error",
"last_telemetry_observed_at",
"last_telemetry_received_at",
"last_inventory_observed_at",
"last_inventory_received_at",
"last_heartbeat_at",
"agent_started_at",
"agent_capabilities",
"agent_protocol_version",
"liveness_state",
"observation_source",
"benchmark_eligible",
"lab_eligible",
"production_eligible",
"labels",
"role",
"enabled",
):
op.drop_column("compute_nodes", column)
for column in (
"inventory_at",
"first_seen_at",
"status_reason",
"agent_version",
"total_ram_bytes",
"physical_core_count",
"logical_cpu_count",
"cpu_model",
"kernel_version",
"architecture",
"os_version",
"os_name",
"identity_source",
"display_name",
):
op.drop_column("compute_nodes", column)
def downgrade() -> None:
existing = set(sa.inspect(op.get_bind()).get_table_names())
for table in reversed(Base.metadata.sorted_tables):
if table.name in existing:
op.drop_table(table.name)
@@ -0,0 +1,194 @@
"""M1 hardware plane persistence.
Revision ID: 20260824_0002
Revises: 20260824_0001
"""
import sqlalchemy as sa
from alembic import op
revision = "20260824_0002"
down_revision = "20260824_0001"
branch_labels = None
depends_on = None
def upgrade() -> None:
for column in (
sa.Column("display_name", sa.String(255), nullable=False, server_default=""),
sa.Column("identity_source", sa.String(32), nullable=False, server_default="unknown"),
sa.Column("os_name", sa.String(128)),
sa.Column("os_version", sa.Text()),
sa.Column("architecture", sa.String(128)),
sa.Column("kernel_version", sa.String(255)),
sa.Column("cpu_model", sa.String(255)),
sa.Column("logical_cpu_count", sa.Integer()),
sa.Column("physical_core_count", sa.Integer()),
sa.Column("total_ram_bytes", sa.BigInteger()),
sa.Column("agent_version", sa.String(64)),
sa.Column("status_reason", sa.Text()),
sa.Column(
"first_seen_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
sa.Column("inventory_at", sa.DateTime(timezone=True)),
):
op.add_column("compute_nodes", column)
for column in (
sa.Column("pci_bus_id", sa.String(64)),
sa.Column("vendor", sa.String(64), nullable=False, server_default="NVIDIA"),
sa.Column("architecture", sa.String(64)),
sa.Column("compute_capability_major", sa.Integer()),
sa.Column("compute_capability_minor", sa.Integer()),
sa.Column("total_vram_bytes", sa.BigInteger()),
sa.Column("mig_mode_current", sa.Boolean()),
sa.Column("status", sa.String(32), nullable=False, server_default="active"),
sa.Column("status_reason", sa.Text()),
sa.Column("inventory_source", sa.String(32), nullable=False, server_default="nvidia_nvml"),
sa.Column(
"first_seen_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
sa.Column("last_seen_at", sa.DateTime(timezone=True)),
sa.Column("inventory_at", sa.DateTime(timezone=True)),
):
op.add_column("accelerators", column)
op.create_table(
"host_telemetry_latest",
sa.Column(
"compute_node_id",
sa.Uuid(),
sa.ForeignKey("compute_nodes.id", ondelete="CASCADE"),
nullable=False,
unique=True,
),
sa.Column("available_ram_bytes", sa.BigInteger()),
sa.Column("availability", sa.JSON(), nullable=False),
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("id", sa.Uuid(), primary_key=True),
)
op.create_index(
"ix_host_telemetry_latest_compute_node_id",
"host_telemetry_latest",
["compute_node_id"],
unique=True,
)
op.create_table(
"accelerator_telemetry_latest",
sa.Column(
"accelerator_id",
sa.Uuid(),
sa.ForeignKey("accelerators.id", ondelete="CASCADE"),
nullable=False,
unique=True,
),
sa.Column("used_vram_bytes", sa.BigInteger()),
sa.Column("free_vram_bytes", sa.BigInteger()),
sa.Column("gpu_utilization_percent", sa.Integer()),
sa.Column("memory_utilization_percent", sa.Integer()),
sa.Column("temperature_c", sa.Integer()),
sa.Column("power_draw_w", sa.Float()),
sa.Column("power_limit_w", sa.Float()),
sa.Column("graphics_clock_mhz", sa.Integer()),
sa.Column("memory_clock_mhz", sa.Integer()),
sa.Column("fan_speed_percent", sa.Integer()),
sa.Column("performance_state", sa.String(32)),
sa.Column("availability", sa.JSON(), nullable=False),
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("id", sa.Uuid(), primary_key=True),
)
op.create_index(
"ix_accelerator_telemetry_latest_accelerator_id",
"accelerator_telemetry_latest",
["accelerator_id"],
unique=True,
)
op.create_table(
"storage_volume_states",
sa.Column(
"compute_node_id",
sa.Uuid(),
sa.ForeignKey("compute_nodes.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("purpose", sa.String(64), nullable=False),
sa.Column("path", sa.Text(), nullable=False),
sa.Column("total_bytes", sa.BigInteger()),
sa.Column("used_bytes", sa.BigInteger()),
sa.Column("free_bytes", sa.BigInteger()),
sa.Column("availability", sa.JSON(), nullable=False),
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.UniqueConstraint("compute_node_id", "purpose", "path", name="uq_node_storage_path"),
)
op.create_index(
"ix_storage_volume_states_compute_node_id", "storage_volume_states", ["compute_node_id"]
)
op.create_table(
"hardware_inventory_runs",
sa.Column(
"compute_node_id", sa.Uuid(), sa.ForeignKey("compute_nodes.id", ondelete="SET NULL")
),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("source", sa.String(64), nullable=False),
sa.Column("fingerprint", sa.String(64)),
sa.Column("summary", sa.JSON(), nullable=False),
sa.Column("error", sa.Text()),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("completed_at", sa.DateTime(timezone=True)),
sa.Column("id", sa.Uuid(), primary_key=True),
)
op.create_index(
"ix_hardware_inventory_runs_compute_node_id", "hardware_inventory_runs", ["compute_node_id"]
)
op.create_index(
"ix_hardware_inventory_runs_fingerprint", "hardware_inventory_runs", ["fingerprint"]
)
def downgrade() -> None:
for table in (
"hardware_inventory_runs",
"storage_volume_states",
"accelerator_telemetry_latest",
"host_telemetry_latest",
):
op.drop_table(table)
for column in (
"inventory_at",
"last_seen_at",
"first_seen_at",
"inventory_source",
"status_reason",
"status",
"mig_mode_current",
"total_vram_bytes",
"compute_capability_minor",
"compute_capability_major",
"architecture",
"vendor",
"pci_bus_id",
):
op.drop_column("accelerators", column)
for column in (
"inventory_at",
"first_seen_at",
"status_reason",
"agent_version",
"total_ram_bytes",
"physical_core_count",
"logical_cpu_count",
"cpu_model",
"kernel_version",
"architecture",
"os_version",
"os_name",
"identity_source",
"display_name",
):
op.drop_column("compute_nodes", column)
@@ -0,0 +1,141 @@
"""M1.5 remote compute node agent.
Revision ID: 20260825_0003
Revises: 20260824_0002
"""
import sqlalchemy as sa
from alembic import op
revision = "20260825_0003"
down_revision = "20260824_0002"
branch_labels = None
depends_on = None
def upgrade() -> None:
for column in (
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("role", sa.String(64)),
sa.Column("labels", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("production_eligible", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("lab_eligible", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("benchmark_eligible", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column(
"observation_source",
sa.String(32),
nullable=False,
server_default="local_control_plane",
),
sa.Column("liveness_state", sa.String(32), nullable=False, server_default="offline"),
sa.Column("agent_protocol_version", sa.Integer()),
sa.Column("agent_capabilities", sa.JSON(), nullable=False, server_default=sa.text("'[]'")),
sa.Column("agent_started_at", sa.DateTime(timezone=True)),
sa.Column("last_heartbeat_at", sa.DateTime(timezone=True)),
sa.Column("last_inventory_received_at", sa.DateTime(timezone=True)),
sa.Column("last_inventory_observed_at", sa.DateTime(timezone=True)),
sa.Column("last_telemetry_received_at", sa.DateTime(timezone=True)),
sa.Column("last_telemetry_observed_at", sa.DateTime(timezone=True)),
sa.Column("last_connection_error", sa.Text()),
sa.Column("inventory_sequence", sa.BigInteger(), nullable=False, server_default="0"),
sa.Column("telemetry_sequence", sa.BigInteger(), nullable=False, server_default="0"),
):
op.add_column("compute_nodes", column)
for table in (
"host_telemetry_latest",
"accelerator_telemetry_latest",
"storage_volume_states",
):
op.add_column(table, sa.Column("received_at", sa.DateTime(timezone=True)))
for column in (
sa.Column("observation_sequence", sa.BigInteger()),
sa.Column("observed_at", sa.DateTime(timezone=True)),
sa.Column("received_at", sa.DateTime(timezone=True)),
):
op.add_column("hardware_inventory_runs", column)
op.create_table(
"node_enrollments",
sa.Column("token_hash", sa.String(64), nullable=False, unique=True),
sa.Column("scope", sa.String(64), nullable=False),
sa.Column("requested_metadata", sa.JSON(), nullable=False),
sa.Column(
"created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("used_at", sa.DateTime(timezone=True)),
sa.Column("revoked_at", sa.DateTime(timezone=True)),
sa.Column(
"enrolled_node_id",
sa.Uuid(),
sa.ForeignKey("compute_nodes.id", ondelete="SET NULL"),
),
sa.Column("id", sa.Uuid(), primary_key=True),
)
op.create_index("ix_node_enrollments_expires_at", "node_enrollments", ["expires_at"])
op.create_index(
"ix_node_enrollments_enrolled_node_id", "node_enrollments", ["enrolled_node_id"]
)
op.create_table(
"node_credentials",
sa.Column(
"compute_node_id",
sa.Uuid(),
sa.ForeignKey("compute_nodes.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("secret_hash", sa.String(64), nullable=False, unique=True),
sa.Column("scope", sa.String(64), nullable=False),
sa.Column(
"created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
),
sa.Column("last_used_at", sa.DateTime(timezone=True)),
sa.Column("revoked_at", sa.DateTime(timezone=True)),
sa.Column("id", sa.Uuid(), primary_key=True),
)
op.create_index("ix_node_credentials_compute_node_id", "node_credentials", ["compute_node_id"])
op.create_index(
"uq_active_node_credential",
"node_credentials",
["compute_node_id"],
unique=True,
postgresql_where=sa.text("revoked_at IS NULL"),
sqlite_where=sa.text("revoked_at IS NULL"),
)
def downgrade() -> None:
op.drop_table("node_credentials")
op.drop_table("node_enrollments")
for column in ("received_at", "observed_at", "observation_sequence"):
op.drop_column("hardware_inventory_runs", column)
for table in (
"storage_volume_states",
"accelerator_telemetry_latest",
"host_telemetry_latest",
):
op.drop_column(table, "received_at")
for column in (
"telemetry_sequence",
"inventory_sequence",
"last_connection_error",
"last_telemetry_observed_at",
"last_telemetry_received_at",
"last_inventory_observed_at",
"last_inventory_received_at",
"last_heartbeat_at",
"agent_started_at",
"agent_capabilities",
"agent_protocol_version",
"liveness_state",
"observation_source",
"benchmark_eligible",
"lab_eligible",
"production_eligible",
"labels",
"role",
"enabled",
):
op.drop_column("compute_nodes", column)
@@ -0,0 +1,170 @@
"""M2 operational model registry and artifact provenance.
Revision ID: 20260825_0004
Revises: 20260825_0003
"""
import sqlalchemy as sa
from alembic import op
revision = "20260825_0004"
down_revision = "20260825_0003"
branch_labels = None
depends_on = None
def upgrade() -> None:
for column in (
sa.Column("source_type", sa.String(32), nullable=False, server_default="huggingface"),
sa.Column("upstream_metadata", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("local_metadata", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column(
"interpretation_metadata", sa.JSON(), nullable=False, server_default=sa.text("'{}'")
),
sa.Column("description", sa.Text()),
):
op.add_column("models", column)
op.add_column("model_revisions", sa.Column("archived_at", sa.DateTime(timezone=True)))
for column in (
sa.Column("status", sa.String(32), nullable=False, server_default="remote"),
sa.Column(
"verification_details", sa.JSON(), nullable=False, server_default=sa.text("'{}'")
),
sa.Column("archived_at", sa.DateTime(timezone=True)),
):
op.add_column("model_artifacts", column)
op.alter_column("model_artifacts", "storage_uri", existing_type=sa.Text(), nullable=True)
for column in (
sa.Column(
"transformation_type", sa.String(64), nullable=False, server_default="conversion"
),
sa.Column(
"environment_snapshot", sa.JSON(), nullable=False, server_default=sa.text("'{}'")
),
sa.Column("status", sa.String(32), nullable=False, server_default="remote"),
sa.Column(
"verification_details", sa.JSON(), nullable=False, server_default=sa.text("'{}'")
),
):
op.add_column("derived_artifacts", column)
op.alter_column("derived_artifacts", "storage_uri", existing_type=sa.Text(), nullable=True)
op.alter_column(
"derived_artifacts", "source_artifact_id", existing_type=sa.Uuid(), nullable=True
)
op.create_table(
"storage_roots",
sa.Column("compute_node_id", sa.Uuid(), nullable=False),
sa.Column("name", sa.String(128), nullable=False),
sa.Column("purpose", sa.String(64), nullable=False, server_default="model_artifacts"),
sa.Column("path", sa.Text(), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="unknown"),
sa.Column("writable", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("capacity_bytes", sa.BigInteger()),
sa.Column("free_bytes", sa.BigInteger()),
sa.Column("reserve_bytes", sa.BigInteger(), nullable=False, server_default="0"),
sa.Column("reserve_percent", sa.Integer(), nullable=False, server_default="10"),
sa.Column("capacity_observed_at", sa.DateTime(timezone=True)),
sa.Column("validation_details", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("deprecated_at", sa.DateTime(timezone=True)),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.ForeignKeyConstraint(["compute_node_id"], ["compute_nodes.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("compute_node_id", "path", name="uq_storage_root_node_path"),
sa.CheckConstraint("reserve_bytes >= 0", name="ck_storage_root_reserve_bytes"),
sa.CheckConstraint(
"reserve_percent >= 0 AND reserve_percent <= 100",
name="ck_storage_root_reserve_percent",
),
)
op.create_index("ix_storage_roots_compute_node_id", "storage_roots", ["compute_node_id"])
op.create_table(
"derived_artifact_sources",
sa.Column("derived_artifact_id", sa.Uuid(), nullable=False),
sa.Column("source_artifact_id", sa.Uuid(), nullable=False),
sa.Column("ordinal", sa.Integer(), nullable=False, server_default="0"),
sa.Column("source_sha256", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(
["derived_artifact_id"], ["derived_artifacts.id"], ondelete="RESTRICT"
),
sa.ForeignKeyConstraint(
["source_artifact_id"], ["model_artifacts.id"], ondelete="RESTRICT"
),
sa.PrimaryKeyConstraint("derived_artifact_id", "source_artifact_id"),
sa.UniqueConstraint(
"derived_artifact_id", "source_artifact_id", name="uq_derived_artifact_source"
),
)
op.create_table(
"artifact_locations",
sa.Column("artifact_id", sa.Uuid()),
sa.Column("derived_artifact_id", sa.Uuid()),
sa.Column("storage_root_id", sa.Uuid(), nullable=False),
sa.Column("relative_path", sa.Text(), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="unknown"),
sa.Column("size_bytes", sa.BigInteger()),
sa.Column("observed_sha256", sa.String(64)),
sa.Column("last_checked_at", sa.DateTime(timezone=True)),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.ForeignKeyConstraint(["artifact_id"], ["model_artifacts.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(
["derived_artifact_id"], ["derived_artifacts.id"], ondelete="RESTRICT"
),
sa.ForeignKeyConstraint(["storage_root_id"], ["storage_roots.id"], ondelete="RESTRICT"),
sa.CheckConstraint(
"(artifact_id IS NOT NULL AND derived_artifact_id IS NULL) OR "
"(artifact_id IS NULL AND derived_artifact_id IS NOT NULL)",
name="ck_artifact_location_one_owner",
),
sa.UniqueConstraint(
"storage_root_id", "relative_path", name="uq_storage_root_relative_path"
),
)
op.create_index("ix_artifact_locations_artifact_id", "artifact_locations", ["artifact_id"])
op.create_index(
"ix_artifact_locations_derived_artifact_id",
"artifact_locations",
["derived_artifact_id"],
)
op.create_index(
"ix_artifact_locations_storage_root_id", "artifact_locations", ["storage_root_id"]
)
def downgrade() -> None:
op.drop_table("artifact_locations")
op.drop_table("derived_artifact_sources")
op.drop_table("storage_roots")
op.execute(
"UPDATE model_artifacts SET storage_uri = 'registry://artifact/' || id::text "
"WHERE storage_uri IS NULL"
)
op.execute(
"UPDATE derived_artifacts SET storage_uri = 'registry://derived/' || id::text "
"WHERE storage_uri IS NULL"
)
op.alter_column("derived_artifacts", "source_artifact_id", existing_type=sa.Uuid(), nullable=False)
op.alter_column("derived_artifacts", "storage_uri", existing_type=sa.Text(), nullable=False)
for column in (
"verification_details",
"status",
"environment_snapshot",
"transformation_type",
):
op.drop_column("derived_artifacts", column)
op.alter_column("model_artifacts", "storage_uri", existing_type=sa.Text(), nullable=False)
for column in ("archived_at", "verification_details", "status"):
op.drop_column("model_artifacts", column)
op.drop_column("model_revisions", "archived_at")
for column in (
"description",
"interpretation_metadata",
"local_metadata",
"upstream_metadata",
"source_type",
):
op.drop_column("models", column)
@@ -0,0 +1,210 @@
"""M3 Hugging Face discovery and node-local artifact acquisition.
Revision ID: 20260825_0005
Revises: 20260825_0004
"""
import sqlalchemy as sa
from alembic import op
revision = "20260825_0005"
down_revision = "20260825_0004"
branch_labels = None
depends_on = None
def _identity() -> list[sa.Column]:
return [
sa.Column("id", sa.Uuid(), primary_key=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
]
def upgrade() -> None:
op.add_column("storage_roots", sa.Column("agent_path", sa.Text()))
op.create_table(
"upstream_snapshots",
sa.Column("model_id", sa.Uuid()),
sa.Column("provider", sa.String(64), nullable=False, server_default="huggingface"),
sa.Column("repository_id", sa.String(255), nullable=False),
sa.Column("requested_revision", sa.String(255), nullable=False, server_default="main"),
sa.Column("resolved_commit_sha", sa.String(64), nullable=False),
sa.Column("access_state", sa.String(32), nullable=False, server_default="public"),
sa.Column("metadata_snapshot", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("card_metadata", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("security_metadata", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("source_updated_at", sa.DateTime(timezone=True)),
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.Column("stale_after", sa.DateTime(timezone=True), nullable=False),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.ForeignKeyConstraint(["model_id"], ["models.id"], ondelete="RESTRICT"),
sa.CheckConstraint("length(resolved_commit_sha) >= 40", name="ck_snapshot_commit_length"),
)
op.create_index("ix_upstream_snapshots_model_id", "upstream_snapshots", ["model_id"])
op.create_index("ix_upstream_snapshots_repository_id", "upstream_snapshots", ["repository_id"])
op.create_index("ix_upstream_snapshots_resolved_commit_sha", "upstream_snapshots", ["resolved_commit_sha"])
op.create_table(
"upstream_files",
sa.Column("snapshot_id", sa.Uuid(), nullable=False),
sa.Column("path", sa.Text(), nullable=False),
sa.Column("size_bytes", sa.BigInteger()),
sa.Column("blob_id", sa.String(128)),
sa.Column("upstream_sha256", sa.String(64)),
sa.Column("file_format", sa.String(64), nullable=False, server_default="unknown"),
sa.Column("role", sa.String(64), nullable=False, server_default="other"),
sa.Column("risk_flags", sa.JSON(), nullable=False, server_default=sa.text("'[]'")),
sa.Column("metadata_snapshot", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.ForeignKeyConstraint(["snapshot_id"], ["upstream_snapshots.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("snapshot_id", "path", name="uq_upstream_file_snapshot_path"),
sa.CheckConstraint("size_bytes IS NULL OR size_bytes >= 0", name="ck_upstream_file_size"),
)
op.create_index("ix_upstream_files_snapshot_id", "upstream_files", ["snapshot_id"])
op.create_table(
"artifact_sets",
sa.Column("revision_id", sa.Uuid(), nullable=False),
sa.Column("snapshot_id", sa.Uuid(), nullable=False),
sa.Column("variant_key", sa.String(128), nullable=False),
sa.Column("label", sa.String(255), nullable=False),
sa.Column("selection_reason", sa.Text(), nullable=False),
sa.Column("selected_paths", sa.JSON(), nullable=False, server_default=sa.text("'[]'")),
sa.Column("total_size_bytes", sa.BigInteger(), nullable=False, server_default="0"),
sa.Column("file_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column("availability", sa.String(32), nullable=False, server_default="remote"),
sa.Column("status", sa.String(32), nullable=False, server_default="remote"),
sa.Column("completeness", sa.String(32), nullable=False, server_default="planned"),
sa.Column("security_status", sa.String(32), nullable=False, server_default="unverified"),
sa.Column("license_status", sa.String(32), nullable=False, server_default="unknown"),
sa.Column("immutable_at", sa.DateTime(timezone=True), nullable=False),
*_identity(),
sa.ForeignKeyConstraint(["revision_id"], ["model_revisions.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["snapshot_id"], ["upstream_snapshots.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("revision_id", "variant_key", name="uq_artifact_set_revision_variant"),
sa.CheckConstraint("total_size_bytes >= 0", name="ck_artifact_set_size"),
)
op.create_index("ix_artifact_sets_revision_id", "artifact_sets", ["revision_id"])
op.create_index("ix_artifact_sets_snapshot_id", "artifact_sets", ["snapshot_id"])
op.create_table(
"download_plans",
sa.Column("artifact_set_id", sa.Uuid(), nullable=False),
sa.Column("compute_node_id", sa.Uuid(), nullable=False),
sa.Column("storage_root_id", sa.Uuid(), nullable=False),
sa.Column("repository_id", sa.String(255), nullable=False),
sa.Column("resolved_commit_sha", sa.String(64), nullable=False),
sa.Column("total_size_bytes", sa.BigInteger(), nullable=False),
sa.Column("file_count", sa.Integer(), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="ready"),
sa.Column("idempotency_key", sa.String(64), nullable=False),
sa.Column("preflight", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("immutable_payload", sa.JSON(), nullable=False),
sa.Column("planned_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("immutable_at", sa.DateTime(timezone=True), nullable=False),
*_identity(),
sa.ForeignKeyConstraint(["artifact_set_id"], ["artifact_sets.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["compute_node_id"], ["compute_nodes.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["storage_root_id"], ["storage_roots.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("idempotency_key", name="uq_download_plan_idempotency"),
sa.CheckConstraint("total_size_bytes >= 0", name="ck_download_plan_size"),
)
for col in ("artifact_set_id", "compute_node_id", "storage_root_id"):
op.create_index(f"ix_download_plans_{col}", "download_plans", [col])
op.create_table(
"download_plan_files",
sa.Column("plan_id", sa.Uuid(), nullable=False),
sa.Column("ordinal", sa.Integer(), nullable=False),
sa.Column("path", sa.Text(), nullable=False),
sa.Column("size_bytes", sa.BigInteger(), nullable=False),
sa.Column("upstream_sha256", sa.String(64)),
sa.Column("file_format", sa.String(64), nullable=False),
sa.Column("role", sa.String(64), nullable=False),
sa.Column("risk_flags", sa.JSON(), nullable=False, server_default=sa.text("'[]'")),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.ForeignKeyConstraint(["plan_id"], ["download_plans.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("plan_id", "path", name="uq_download_plan_file_path"),
sa.UniqueConstraint("plan_id", "ordinal", name="uq_download_plan_file_ordinal"),
)
op.create_index("ix_download_plan_files_plan_id", "download_plan_files", ["plan_id"])
op.create_table(
"artifact_jobs",
sa.Column("plan_id", sa.Uuid(), nullable=False),
sa.Column("compute_node_id", sa.Uuid(), nullable=False),
sa.Column("storage_root_id", sa.Uuid(), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="queued"),
sa.Column("idempotency_key", sa.String(64), nullable=False),
sa.Column("attempt_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column("lease_token_hash", sa.String(64)),
sa.Column("lease_expires_at", sa.DateTime(timezone=True)),
sa.Column("progress_bytes", sa.BigInteger(), nullable=False, server_default="0"),
sa.Column("total_bytes", sa.BigInteger(), nullable=False),
sa.Column("current_file", sa.Text()),
sa.Column("cancel_requested", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("quarantine_relative_path", sa.Text()),
sa.Column("promoted_relative_path", sa.Text()),
sa.Column("error_code", sa.String(64)),
sa.Column("error_message", sa.Text()),
sa.Column("result", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("started_at", sa.DateTime(timezone=True)),
sa.Column("completed_at", sa.DateTime(timezone=True)),
*_identity(),
sa.ForeignKeyConstraint(["plan_id"], ["download_plans.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["compute_node_id"], ["compute_nodes.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["storage_root_id"], ["storage_roots.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("plan_id", name="uq_artifact_job_plan"),
sa.UniqueConstraint("idempotency_key", name="uq_artifact_job_idempotency"),
)
for col in ("plan_id", "compute_node_id", "storage_root_id", "status"):
op.create_index(f"ix_artifact_jobs_{col}", "artifact_jobs", [col])
op.create_table(
"artifact_job_attempts",
sa.Column("job_id", sa.Uuid(), nullable=False),
sa.Column("attempt", sa.Integer(), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.Column("completed_at", sa.DateTime(timezone=True)),
sa.Column("details", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.ForeignKeyConstraint(["job_id"], ["artifact_jobs.id"], ondelete="RESTRICT"),
)
op.create_index("ix_artifact_job_attempts_job_id", "artifact_job_attempts", ["job_id"])
op.create_table(
"artifact_inspections",
sa.Column("job_id", sa.Uuid(), nullable=False),
sa.Column("file_path", sa.Text(), nullable=False),
sa.Column("inspection_type", sa.String(64), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("severity", sa.String(32), nullable=False),
sa.Column("evidence", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.ForeignKeyConstraint(["job_id"], ["artifact_jobs.id"], ondelete="RESTRICT"),
)
op.create_index("ix_artifact_inspections_job_id", "artifact_inspections", ["job_id"])
op.create_table(
"artifact_set_members",
sa.Column("artifact_set_id", sa.Uuid(), nullable=False),
sa.Column("artifact_id", sa.Uuid(), nullable=False),
sa.Column("ordinal", sa.Integer(), nullable=False),
sa.Column("required", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.ForeignKeyConstraint(["artifact_set_id"], ["artifact_sets.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["artifact_id"], ["model_artifacts.id"], ondelete="RESTRICT"),
sa.PrimaryKeyConstraint("artifact_set_id", "artifact_id"),
)
def downgrade() -> None:
for table in (
"artifact_set_members",
"artifact_inspections",
"artifact_job_attempts",
"artifact_jobs",
"download_plan_files",
"download_plans",
"artifact_sets",
"upstream_files",
"upstream_snapshots",
):
op.drop_table(table)
op.drop_column("storage_roots", "agent_path")
@@ -0,0 +1,221 @@
"""M4 runtime compatibility and runtime plane.
Revision ID: 20260825_0006
Revises: 20260825_0005
"""
import sqlalchemy as sa
from alembic import op
revision = "20260825_0006"
down_revision = "20260825_0005"
branch_labels = None
depends_on = None
def _timestamps() -> list[sa.Column]:
return [
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
]
def upgrade() -> None:
op.create_table(
"runtime_environments",
sa.Column("name", sa.String(255), nullable=False),
sa.Column("adapter", sa.String(64), nullable=False),
sa.Column("runtime_version", sa.String(128), nullable=False),
sa.Column("image_repository", sa.String(255), nullable=False),
sa.Column("image_digest", sa.String(71), nullable=False),
sa.Column("python_version", sa.String(64), nullable=False),
sa.Column("cuda_runtime_version", sa.String(64)),
sa.Column("package_versions", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("supported_model_types", sa.JSON(), nullable=False, server_default=sa.text("'[]'")),
sa.Column("supported_formats", sa.JSON(), nullable=False, server_default=sa.text("'[]'")),
sa.Column("supported_modalities", sa.JSON(), nullable=False, server_default=sa.text("'[]'")),
sa.Column("network_policy", sa.String(64), nullable=False),
sa.Column("fingerprint", sa.String(64), nullable=False),
sa.Column("immutable_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.UniqueConstraint("fingerprint", name="uq_runtime_environment_fingerprint"),
)
op.create_index("ix_runtime_environments_adapter", "runtime_environments", ["adapter"])
for column in (
sa.Column("runtime_environment_id", sa.Uuid()),
sa.Column("artifact_set_id", sa.Uuid()),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.Column("dtype", sa.String(32)),
sa.Column("modality", sa.String(32)),
sa.Column("device_policy", sa.String(32), nullable=False, server_default="cuda_required"),
sa.Column("health_contract", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("profile_config", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
):
op.add_column("runtime_profiles", column)
op.create_foreign_key(
"fk_runtime_profiles_environment",
"runtime_profiles",
"runtime_environments",
["runtime_environment_id"],
["id"],
ondelete="RESTRICT",
)
op.create_foreign_key(
"fk_runtime_profiles_artifact_set",
"runtime_profiles",
"artifact_sets",
["artifact_set_id"],
["id"],
ondelete="RESTRICT",
)
op.create_index("ix_runtime_profiles_runtime_environment_id", "runtime_profiles", ["runtime_environment_id"])
op.create_index("ix_runtime_profiles_artifact_set_id", "runtime_profiles", ["artifact_set_id"])
op.create_table(
"runtime_compatibility_assessments",
sa.Column("artifact_set_id", sa.Uuid(), nullable=False),
sa.Column("runtime_profile_id", sa.Uuid(), nullable=False),
sa.Column("compute_node_id", sa.Uuid(), nullable=False),
sa.Column("adapter", sa.String(64), nullable=False),
sa.Column("runtime_version", sa.String(128), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("static_result", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("evidence", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("blockers", sa.JSON(), nullable=False, server_default=sa.text("'[]'")),
sa.Column("warnings", sa.JSON(), nullable=False, server_default=sa.text("'[]'")),
sa.Column("required_approvals", sa.JSON(), nullable=False, server_default=sa.text("'[]'")),
sa.Column("hardware_facts", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("artifact_facts", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("environment_fingerprint", sa.String(64), nullable=False),
sa.Column("stale", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("stale_reason", sa.Text()),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.ForeignKeyConstraint(["artifact_set_id"], ["artifact_sets.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["runtime_profile_id"], ["runtime_profiles.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["compute_node_id"], ["compute_nodes.id"], ondelete="RESTRICT"),
)
for column in ("artifact_set_id", "runtime_profile_id", "compute_node_id", "status", "environment_fingerprint"):
op.create_index(f"ix_runtime_compatibility_assessments_{column}", "runtime_compatibility_assessments", [column])
op.create_table(
"execution_approvals",
sa.Column("artifact_set_id", sa.Uuid(), nullable=False),
sa.Column("scope", sa.String(32), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("evidence_fingerprint", sa.String(64), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("approved_by", sa.String(255), nullable=False),
sa.Column("approved_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("expires_at", sa.DateTime(timezone=True)),
sa.Column("revoked_at", sa.DateTime(timezone=True)),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.ForeignKeyConstraint(["artifact_set_id"], ["artifact_sets.id"], ondelete="RESTRICT"),
)
op.create_index("ix_execution_approvals_artifact_set_id", "execution_approvals", ["artifact_set_id"])
op.create_index("ix_execution_approvals_status", "execution_approvals", ["status"])
op.create_table(
"runtime_probes",
sa.Column("artifact_set_id", sa.Uuid(), nullable=False),
sa.Column("runtime_profile_id", sa.Uuid(), nullable=False),
sa.Column("compute_node_id", sa.Uuid(), nullable=False),
sa.Column("compatibility_assessment_id", sa.Uuid(), nullable=False),
sa.Column("execution_approval_id", sa.Uuid(), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="queued"),
sa.Column("phase", sa.String(64)),
sa.Column("probe_input", sa.Text(), nullable=False),
sa.Column("idempotency_key", sa.String(64), nullable=False),
sa.Column("attempt_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column("lease_token_hash", sa.String(64)),
sa.Column("lease_expires_at", sa.DateTime(timezone=True)),
sa.Column("cancel_requested", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("load_result", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("health_result", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("inference_result", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("unload_result", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("measured_resources", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("runtime_facts", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("environment_fingerprint", sa.String(64), nullable=False),
sa.Column("failure_code", sa.String(64)),
sa.Column("failure_message", sa.Text()),
sa.Column("started_at", sa.DateTime(timezone=True)),
sa.Column("finished_at", sa.DateTime(timezone=True)),
sa.Column("id", sa.Uuid(), primary_key=True),
*_timestamps(),
sa.ForeignKeyConstraint(["artifact_set_id"], ["artifact_sets.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["runtime_profile_id"], ["runtime_profiles.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["compute_node_id"], ["compute_nodes.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["compatibility_assessment_id"], ["runtime_compatibility_assessments.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["execution_approval_id"], ["execution_approvals.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("idempotency_key", name="uq_runtime_probe_idempotency"),
)
for column in ("artifact_set_id", "runtime_profile_id", "compute_node_id", "compatibility_assessment_id", "execution_approval_id", "status"):
op.create_index(f"ix_runtime_probes_{column}", "runtime_probes", [column])
op.create_table(
"runtime_probe_metrics",
sa.Column("runtime_probe_id", sa.Uuid(), nullable=False),
sa.Column("phase", sa.String(64), nullable=False),
sa.Column("measurement_type", sa.String(64), nullable=False),
sa.Column("source", sa.String(64), nullable=False),
sa.Column("values", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.ForeignKeyConstraint(["runtime_probe_id"], ["runtime_probes.id"], ondelete="RESTRICT"),
)
op.create_index("ix_runtime_probe_metrics_runtime_probe_id", "runtime_probe_metrics", ["runtime_probe_id"])
op.create_table(
"deployment_candidates",
sa.Column("artifact_set_id", sa.Uuid(), nullable=False),
sa.Column("runtime_profile_id", sa.Uuid(), nullable=False),
sa.Column("compute_node_id", sa.Uuid(), nullable=False),
sa.Column("compatibility_assessment_id", sa.Uuid(), nullable=False),
sa.Column("runtime_probe_id", sa.Uuid(), nullable=False),
sa.Column("channel", sa.String(32), nullable=False, server_default="lab"),
sa.Column("status", sa.String(32), nullable=False, server_default="lab_ready"),
sa.Column("production", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("health_contract", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("measured_resources", sa.JSON(), nullable=False, server_default=sa.text("'{}'")),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("id", sa.Uuid(), primary_key=True),
sa.ForeignKeyConstraint(["artifact_set_id"], ["artifact_sets.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["runtime_profile_id"], ["runtime_profiles.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["compute_node_id"], ["compute_nodes.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["compatibility_assessment_id"], ["runtime_compatibility_assessments.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["runtime_probe_id"], ["runtime_probes.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("runtime_probe_id", name="uq_deployment_candidate_probe"),
)
for column in ("artifact_set_id", "runtime_profile_id", "compute_node_id", "runtime_probe_id", "status"):
op.create_index(f"ix_deployment_candidates_{column}", "deployment_candidates", [column])
def downgrade() -> None:
for table in (
"deployment_candidates",
"runtime_probe_metrics",
"runtime_probes",
"execution_approvals",
"runtime_compatibility_assessments",
):
op.drop_table(table)
op.drop_index("ix_runtime_profiles_artifact_set_id", table_name="runtime_profiles")
op.drop_index("ix_runtime_profiles_runtime_environment_id", table_name="runtime_profiles")
op.drop_constraint("fk_runtime_profiles_artifact_set", "runtime_profiles", type_="foreignkey")
op.drop_constraint("fk_runtime_profiles_environment", "runtime_profiles", type_="foreignkey")
for column in (
"profile_config",
"health_contract",
"device_policy",
"modality",
"dtype",
"version",
"artifact_set_id",
"runtime_environment_id",
):
op.drop_column("runtime_profiles", column)
op.drop_table("runtime_environments")
@@ -0,0 +1,28 @@
"""M4 bounded runtime log reference.
Revision ID: 20260825_0007
Revises: 20260825_0006
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260825_0007"
down_revision: str | None = "20260825_0006"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.add_column("runtime_probes", sa.Column("logs_reference", sa.String(512)))
op.execute(
"UPDATE runtime_probes SET logs_reference = "
"'runtime-worker://historical-probe/' || CAST(id AS VARCHAR)"
)
def downgrade() -> None:
op.drop_column("runtime_probes", "logs_reference")
@@ -0,0 +1,382 @@
"""M5 capability serving, gateway and GPU scheduling.
Revision ID: 20260825_0008
Revises: 20260825_0007
"""
import sqlalchemy as sa
from alembic import op
revision = "20260825_0008"
down_revision = "20260825_0007"
branch_labels = None
depends_on = None
def _identity_columns() -> list[sa.Column]:
return [sa.Column("id", sa.Uuid(), primary_key=True)]
def _timestamps() -> list[sa.Column]:
return [
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
]
def upgrade() -> None:
op.create_table(
"production_execution_approvals",
*_identity_columns(),
sa.Column("deployment_candidate_id", sa.Uuid(), nullable=False),
sa.Column("capability_contract_id", sa.Uuid(), nullable=False),
sa.Column("artifact_set_id", sa.Uuid(), nullable=False),
sa.Column("runtime_profile_id", sa.Uuid(), nullable=False),
sa.Column("compute_node_id", sa.Uuid(), nullable=False),
sa.Column("deployment_config", sa.JSON(), nullable=False),
sa.Column("supply_chain_evidence", sa.JSON(), nullable=False),
sa.Column("evidence_fingerprint", sa.String(64), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="approved"),
sa.Column("approved_by", sa.String(255), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("approved_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("revoked_at", sa.DateTime(timezone=True)),
sa.ForeignKeyConstraint(["deployment_candidate_id"], ["deployment_candidates.id"]),
sa.ForeignKeyConstraint(["capability_contract_id"], ["capability_contracts.id"]),
sa.ForeignKeyConstraint(["artifact_set_id"], ["artifact_sets.id"]),
sa.ForeignKeyConstraint(["runtime_profile_id"], ["runtime_profiles.id"]),
sa.ForeignKeyConstraint(["compute_node_id"], ["compute_nodes.id"]),
sa.UniqueConstraint("evidence_fingerprint", name="uq_production_approval_fingerprint"),
)
for column in (
"deployment_candidate_id",
"capability_contract_id",
"artifact_set_id",
"runtime_profile_id",
"compute_node_id",
"status",
):
op.create_index(
f"ix_production_execution_approvals_{column}",
"production_execution_approvals",
[column],
)
op.create_table(
"embedding_spaces",
*_identity_columns(),
sa.Column("capability_contract_id", sa.Uuid(), nullable=False),
sa.Column("artifact_set_id", sa.Uuid(), nullable=False),
sa.Column("runtime_profile_id", sa.Uuid(), nullable=False),
sa.Column("identity_digest", sa.String(64), nullable=False),
sa.Column("dimension", sa.Integer(), nullable=False),
sa.Column("normalized", sa.Boolean(), nullable=False),
sa.Column("migration_class", sa.String(32), nullable=False),
sa.Column("identity_facts", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.ForeignKeyConstraint(["capability_contract_id"], ["capability_contracts.id"]),
sa.ForeignKeyConstraint(["artifact_set_id"], ["artifact_sets.id"]),
sa.ForeignKeyConstraint(["runtime_profile_id"], ["runtime_profiles.id"]),
sa.UniqueConstraint("identity_digest", name="uq_embedding_space_digest"),
)
for column in ("capability_contract_id", "artifact_set_id", "runtime_profile_id"):
op.create_index(f"ix_embedding_spaces_{column}", "embedding_spaces", [column])
op.create_table(
"capability_deployments",
*_identity_columns(),
*_timestamps(),
sa.Column("capability_contract_id", sa.Uuid(), nullable=False),
sa.Column("deployment_candidate_id", sa.Uuid(), nullable=False),
sa.Column("production_approval_id", sa.Uuid(), nullable=False),
sa.Column("embedding_space_id", sa.Uuid(), nullable=False),
sa.Column("artifact_set_id", sa.Uuid(), nullable=False),
sa.Column("runtime_profile_id", sa.Uuid(), nullable=False),
sa.Column("compute_node_id", sa.Uuid(), nullable=False),
sa.Column("accelerator_id", sa.Uuid(), nullable=False),
sa.Column("channel", sa.String(32), nullable=False, server_default="stable"),
sa.Column("status", sa.String(32), nullable=False, server_default="approved"),
sa.Column("production", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("health_status", sa.String(32), nullable=False, server_default="ready_on_demand"),
sa.Column("routing_weight", sa.Integer(), nullable=False, server_default="100"),
sa.Column("fallback_policy", sa.JSON(), nullable=False),
sa.Column("residency_policy", sa.String(32), nullable=False),
sa.Column("keep_warm_seconds", sa.Integer(), nullable=False),
sa.Column("max_concurrency", sa.Integer(), nullable=False),
sa.Column("max_queue_depth", sa.Integer(), nullable=False),
sa.Column("config_fingerprint", sa.String(64), nullable=False),
sa.Column("provenance", sa.JSON(), nullable=False),
sa.Column("rollback_policy", sa.JSON(), nullable=False),
sa.Column("promoted_at", sa.DateTime(timezone=True)),
sa.Column("draining_at", sa.DateTime(timezone=True)),
sa.Column("deprecated_at", sa.DateTime(timezone=True)),
sa.ForeignKeyConstraint(["capability_contract_id"], ["capability_contracts.id"]),
sa.ForeignKeyConstraint(["deployment_candidate_id"], ["deployment_candidates.id"]),
sa.ForeignKeyConstraint(["production_approval_id"], ["production_execution_approvals.id"]),
sa.ForeignKeyConstraint(["embedding_space_id"], ["embedding_spaces.id"]),
sa.ForeignKeyConstraint(["artifact_set_id"], ["artifact_sets.id"]),
sa.ForeignKeyConstraint(["runtime_profile_id"], ["runtime_profiles.id"]),
sa.ForeignKeyConstraint(["compute_node_id"], ["compute_nodes.id"]),
sa.ForeignKeyConstraint(["accelerator_id"], ["accelerators.id"]),
sa.UniqueConstraint("config_fingerprint", name="uq_capability_deployment_fingerprint"),
)
for column in (
"capability_contract_id",
"deployment_candidate_id",
"production_approval_id",
"embedding_space_id",
"artifact_set_id",
"runtime_profile_id",
"compute_node_id",
"accelerator_id",
"channel",
"status",
):
op.create_index(f"ix_capability_deployments_{column}", "capability_deployments", [column])
op.create_index(
"ix_capability_deployments_contract_channel_status",
"capability_deployments",
["capability_contract_id", "channel", "status"],
)
op.create_table(
"capability_resource_envelopes",
*_identity_columns(),
*_timestamps(),
sa.Column("capability_deployment_id", sa.Uuid(), nullable=False),
sa.Column("runtime_probe_id", sa.Uuid(), nullable=False),
sa.Column("accelerator_kind", sa.String(255), nullable=False),
sa.Column("accelerator_uuid", sa.String(255), nullable=False),
sa.Column("environment_fingerprint", sa.String(64), nullable=False),
sa.Column("concurrency", sa.Integer(), nullable=False),
sa.Column("batch_size", sa.Integer(), nullable=False),
sa.Column("max_sequence_length", sa.Integer(), nullable=False),
sa.Column("baseline_vram_bytes", sa.BigInteger(), nullable=False),
sa.Column("resident_vram_bytes", sa.BigInteger(), nullable=False),
sa.Column("peak_vram_bytes", sa.BigInteger(), nullable=False),
sa.Column("required_vram_bytes", sa.BigInteger(), nullable=False),
sa.Column("cold_load_time_ms", sa.Float(), nullable=False),
sa.Column("inference_latency_ms", sa.Float(), nullable=False),
sa.Column("evidence", sa.JSON(), nullable=False),
sa.Column("stale", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("stale_reason", sa.Text()),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.ForeignKeyConstraint(["capability_deployment_id"], ["capability_deployments.id"]),
sa.ForeignKeyConstraint(["runtime_probe_id"], ["runtime_probes.id"]),
sa.UniqueConstraint("capability_deployment_id", name="uq_capability_resource_envelope"),
)
op.create_index(
"ix_capability_resource_envelopes_capability_deployment_id",
"capability_resource_envelopes",
["capability_deployment_id"],
)
op.create_index(
"ix_capability_resource_envelopes_runtime_probe_id",
"capability_resource_envelopes",
["runtime_probe_id"],
)
op.create_index(
"ix_capability_resource_envelopes_environment_fingerprint",
"capability_resource_envelopes",
["environment_fingerprint"],
)
op.create_table(
"service_clients",
*_identity_columns(),
*_timestamps(),
sa.Column("name", sa.String(255), nullable=False, unique=True),
sa.Column("status", sa.String(32), nullable=False, server_default="active"),
sa.Column("allowed_capabilities", sa.JSON(), nullable=False),
sa.Column("requests_per_minute", sa.Integer(), nullable=False),
sa.Column("max_concurrent_requests", sa.Integer(), nullable=False),
sa.Column("last_used_at", sa.DateTime(timezone=True)),
sa.Column("disabled_at", sa.DateTime(timezone=True)),
)
op.create_index("ix_service_clients_status", "service_clients", ["status"])
op.create_table(
"service_credentials",
*_identity_columns(),
sa.Column("service_client_id", sa.Uuid(), nullable=False),
sa.Column("secret_hash", sa.String(64), nullable=False, unique=True),
sa.Column("secret_prefix", sa.String(16), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("expires_at", sa.DateTime(timezone=True)),
sa.Column("last_used_at", sa.DateTime(timezone=True)),
sa.Column("revoked_at", sa.DateTime(timezone=True)),
sa.ForeignKeyConstraint(["service_client_id"], ["service_clients.id"], ondelete="CASCADE"),
)
op.create_index(
"ix_service_credentials_service_client_id", "service_credentials", ["service_client_id"]
)
op.create_index(
"uq_active_service_client_credential",
"service_credentials",
["service_client_id"],
unique=True,
postgresql_where=sa.text("revoked_at IS NULL"),
)
op.create_table(
"residency_allocations",
*_identity_columns(),
*_timestamps(),
sa.Column("capability_deployment_id", sa.Uuid(), nullable=False),
sa.Column("compute_node_id", sa.Uuid(), nullable=False),
sa.Column("accelerator_id", sa.Uuid(), nullable=False),
sa.Column("state", sa.String(32), nullable=False, server_default="cold"),
sa.Column("worker_instance_id", sa.String(255)),
sa.Column("load_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column("active_requests", sa.Integer(), nullable=False, server_default="0"),
sa.Column(
"measured_resident_vram_bytes", sa.BigInteger(), nullable=False, server_default="0"
),
sa.Column(
"external_baseline_vram_bytes", sa.BigInteger(), nullable=False, server_default="0"
),
sa.Column("health", sa.JSON(), nullable=False),
sa.Column("resident_since", sa.DateTime(timezone=True)),
sa.Column("last_used_at", sa.DateTime(timezone=True)),
sa.Column("transition_started_at", sa.DateTime(timezone=True)),
sa.Column("failure_code", sa.String(64)),
sa.Column("failure_message", sa.Text()),
sa.ForeignKeyConstraint(["capability_deployment_id"], ["capability_deployments.id"]),
sa.ForeignKeyConstraint(["compute_node_id"], ["compute_nodes.id"]),
sa.ForeignKeyConstraint(["accelerator_id"], ["accelerators.id"]),
sa.UniqueConstraint("capability_deployment_id", name="uq_residency_deployment"),
)
for column in ("capability_deployment_id", "compute_node_id", "accelerator_id", "state"):
op.create_index(f"ix_residency_allocations_{column}", "residency_allocations", [column])
op.create_table(
"serving_gpu_leases",
*_identity_columns(),
sa.Column("accelerator_id", sa.Uuid(), nullable=False),
sa.Column("capability_deployment_id", sa.Uuid(), nullable=False),
sa.Column("request_id", sa.Uuid()),
sa.Column("reserved_vram_bytes", sa.BigInteger(), nullable=False),
sa.Column("priority", sa.String(32), nullable=False),
sa.Column("state", sa.String(32), nullable=False, server_default="pending"),
sa.Column("owner", sa.String(255), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("acquired_at", sa.DateTime(timezone=True)),
sa.Column("heartbeat_at", sa.DateTime(timezone=True)),
sa.Column("released_at", sa.DateTime(timezone=True)),
sa.Column("failure_code", sa.String(64)),
sa.ForeignKeyConstraint(["accelerator_id"], ["accelerators.id"]),
sa.ForeignKeyConstraint(["capability_deployment_id"], ["capability_deployments.id"]),
)
for column in (
"accelerator_id",
"capability_deployment_id",
"request_id",
"state",
"expires_at",
):
op.create_index(f"ix_serving_gpu_leases_{column}", "serving_gpu_leases", [column])
op.create_table(
"gateway_requests",
*_identity_columns(),
sa.Column("request_id", sa.Uuid(), nullable=False, unique=True),
sa.Column("service_client_id", sa.Uuid()),
sa.Column("capability_key", sa.String(128), nullable=False),
sa.Column("capability_version", sa.Integer(), nullable=False),
sa.Column("capability_deployment_id", sa.Uuid()),
sa.Column("compute_node_id", sa.Uuid()),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("priority", sa.String(32), nullable=False),
sa.Column("input_sha256", sa.String(64), nullable=False),
sa.Column("input_count", sa.Integer(), nullable=False),
sa.Column("cold", sa.Boolean()),
sa.Column("queue_time_ms", sa.Float()),
sa.Column("load_time_ms", sa.Float()),
sa.Column("inference_time_ms", sa.Float()),
sa.Column("total_latency_ms", sa.Float()),
sa.Column("failure_code", sa.String(64)),
sa.Column("decision_evidence", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("finished_at", sa.DateTime(timezone=True)),
sa.ForeignKeyConstraint(["service_client_id"], ["service_clients.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(
["capability_deployment_id"], ["capability_deployments.id"], ondelete="SET NULL"
),
sa.ForeignKeyConstraint(["compute_node_id"], ["compute_nodes.id"], ondelete="SET NULL"),
)
for column in (
"request_id",
"service_client_id",
"capability_key",
"capability_deployment_id",
"compute_node_id",
"status",
"created_at",
):
op.create_index(f"ix_gateway_requests_{column}", "gateway_requests", [column])
op.create_index(
"ix_gateway_requests_client_created",
"gateway_requests",
["service_client_id", "created_at"],
)
op.create_table(
"serving_jobs",
*_identity_columns(),
*_timestamps(),
sa.Column("capability_deployment_id", sa.Uuid(), nullable=False),
sa.Column("compute_node_id", sa.Uuid(), nullable=False),
sa.Column("gateway_request_id", sa.Uuid()),
sa.Column("operation", sa.String(32), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="queued"),
sa.Column("priority", sa.String(32), nullable=False),
sa.Column("idempotency_key", sa.String(64), nullable=False),
sa.Column("payload_reference", sa.String(128)),
sa.Column("attempt_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column("lease_token_hash", sa.String(64)),
sa.Column("lease_expires_at", sa.DateTime(timezone=True)),
sa.Column("result_reference", sa.String(128)),
sa.Column("result_summary", sa.JSON(), nullable=False),
sa.Column("failure_code", sa.String(64)),
sa.Column("failure_message", sa.Text()),
sa.Column("started_at", sa.DateTime(timezone=True)),
sa.Column("finished_at", sa.DateTime(timezone=True)),
sa.ForeignKeyConstraint(["capability_deployment_id"], ["capability_deployments.id"]),
sa.ForeignKeyConstraint(["compute_node_id"], ["compute_nodes.id"]),
sa.ForeignKeyConstraint(
["gateway_request_id"], ["gateway_requests.id"], ondelete="SET NULL"
),
sa.UniqueConstraint("idempotency_key", name="uq_serving_job_idempotency"),
)
for column in (
"capability_deployment_id",
"compute_node_id",
"gateway_request_id",
"operation",
"status",
):
op.create_index(f"ix_serving_jobs_{column}", "serving_jobs", [column])
op.create_index(
"ix_serving_jobs_node_status_priority",
"serving_jobs",
["compute_node_id", "status", "priority"],
)
def downgrade() -> None:
for table in (
"serving_jobs",
"gateway_requests",
"serving_gpu_leases",
"residency_allocations",
"service_credentials",
"service_clients",
"capability_resource_envelopes",
"capability_deployments",
"embedding_spaces",
"production_execution_approvals",
):
op.drop_table(table)
@@ -0,0 +1,165 @@
"""M6 project evaluation and isolated embedding migrations.
Revision ID: 20260825_0009
Revises: 20260825_0008
"""
import sqlalchemy as sa
from alembic import op
revision = "20260825_0009"
down_revision = "20260825_0008"
branch_labels = None
depends_on = None
def _id() -> sa.Column:
return sa.Column("id", sa.Uuid(), primary_key=True)
def _created() -> sa.Column:
return sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now())
def upgrade() -> None:
op.add_column(
"service_clients",
sa.Column("workload_priority", sa.String(32), nullable=False, server_default="production"),
)
op.create_table(
"project_evaluation_bindings", _id(), _created(),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("project_id", sa.Uuid(), nullable=False),
sa.Column("adapter_kind", sa.String(64), nullable=False),
sa.Column("endpoint", sa.Text(), nullable=False),
sa.Column("current_target", sa.String(128), nullable=False),
sa.Column("shadow_target", sa.String(128)),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("configuration", sa.JSON(), nullable=False),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("project_id", name="uq_project_evaluation_binding"),
)
op.create_index("ix_project_evaluation_bindings_project_id", "project_evaluation_bindings", ["project_id"])
op.create_table(
"embedding_migrations", _id(), _created(),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("project_id", sa.Uuid(), nullable=False),
sa.Column("source_embedding_space", sa.String(255), nullable=False),
sa.Column("target_embedding_space_id", sa.Uuid(), nullable=False),
sa.Column("source_index_ref", sa.Text(), nullable=False),
sa.Column("target_index_ref", sa.Text(), nullable=False, unique=True),
sa.Column("corpus_revision", sa.String(128), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="planned"),
sa.Column("total_chunks", sa.Integer(), nullable=False, server_default="0"),
sa.Column("completed_chunks", sa.Integer(), nullable=False, server_default="0"),
sa.Column("failed_chunks", sa.Integer(), nullable=False, server_default="0"),
sa.Column("retried_chunks", sa.Integer(), nullable=False, server_default="0"),
sa.Column("batch_size", sa.Integer(), nullable=False),
sa.Column("concurrency", sa.Integer(), nullable=False),
sa.Column("priority", sa.String(32), nullable=False, server_default="background"),
sa.Column("preflight_evidence", sa.JSON(), nullable=False),
sa.Column("progress_evidence", sa.JSON(), nullable=False),
sa.Column("validation_evidence", sa.JSON(), nullable=False),
sa.Column("operational_metrics", sa.JSON(), nullable=False),
sa.Column("evaluation_eligibility", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("cancel_requested", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("failure_code", sa.String(64)), sa.Column("failure_message", sa.Text()),
sa.Column("started_at", sa.DateTime(timezone=True)),
sa.Column("finished_at", sa.DateTime(timezone=True)),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["target_embedding_space_id"], ["embedding_spaces.id"], ondelete="RESTRICT"),
)
for column in ("project_id", "target_embedding_space_id", "status"):
op.create_index(f"ix_embedding_migrations_{column}", "embedding_migrations", [column])
op.create_table(
"evaluation_suites", _id(), _created(),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("project_id", sa.Uuid(), nullable=False), sa.Column("key", sa.String(128), nullable=False),
sa.Column("name", sa.String(255), nullable=False), sa.Column("description", sa.Text(), nullable=False),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("project_id", "key", name="uq_evaluation_suite_key"),
)
op.create_index("ix_evaluation_suites_project_id", "evaluation_suites", ["project_id"])
op.create_table(
"evaluation_suite_revisions", _id(), _created(),
sa.Column("suite_id", sa.Uuid(), nullable=False), sa.Column("revision", sa.String(128), nullable=False),
sa.Column("dataset_revision", sa.String(128), nullable=False),
sa.Column("definition_digest", sa.String(64), nullable=False), sa.Column("metrics", sa.JSON(), nullable=False),
sa.Column("top_k", sa.Integer(), nullable=False), sa.Column("retrieval_settings", sa.JSON(), nullable=False),
sa.Column("thresholds", sa.JSON(), nullable=False),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.ForeignKeyConstraint(["suite_id"], ["evaluation_suites.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("suite_id", "revision", name="uq_evaluation_suite_revision"),
sa.UniqueConstraint("definition_digest", name="uq_evaluation_revision_digest"),
)
op.create_index("ix_evaluation_suite_revisions_suite_id", "evaluation_suite_revisions", ["suite_id"])
op.create_table(
"evaluation_cases", _id(), _created(),
sa.Column("suite_revision_id", sa.Uuid(), nullable=False), sa.Column("case_key", sa.String(128), nullable=False),
sa.Column("query", sa.Text(), nullable=False), sa.Column("relevant_chunk_ids", sa.JSON(), nullable=False),
sa.Column("relevant_document_ids", sa.JSON(), nullable=False), sa.Column("relevance_grades", sa.JSON(), nullable=False),
sa.Column("label_provenance", sa.JSON(), nullable=False), sa.Column("critical", sa.Boolean(), nullable=False),
sa.Column("review_status", sa.String(32), nullable=False),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.ForeignKeyConstraint(["suite_revision_id"], ["evaluation_suite_revisions.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("suite_revision_id", "case_key", name="uq_evaluation_case_key"),
)
op.create_index("ix_evaluation_cases_suite_revision_id", "evaluation_cases", ["suite_revision_id"])
op.create_index("ix_evaluation_cases_critical", "evaluation_cases", ["critical"])
op.create_table(
"evaluation_runs", _id(), _created(),
sa.Column("project_id", sa.Uuid(), nullable=False), sa.Column("suite_revision_id", sa.Uuid(), nullable=False),
sa.Column("target_kind", sa.String(32), nullable=False), sa.Column("target_index_ref", sa.Text(), nullable=False),
sa.Column("embedding_space_ref", sa.String(255), nullable=False), sa.Column("capability_deployment_id", sa.Uuid()),
sa.Column("status", sa.String(32), nullable=False), sa.Column("corpus_revision", sa.String(128), nullable=False),
sa.Column("retrieval_config_digest", sa.String(64), nullable=False),
sa.Column("environment_fingerprint", sa.JSON(), nullable=False), sa.Column("environment_digest", sa.String(64), nullable=False),
sa.Column("expected_cases", sa.Integer(), nullable=False), sa.Column("completed_cases", sa.Integer(), nullable=False),
sa.Column("error_count", sa.Integer(), nullable=False), sa.Column("aggregate_metrics", sa.JSON(), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True)), sa.Column("completed_at", sa.DateTime(timezone=True)),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["suite_revision_id"], ["evaluation_suite_revisions.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["capability_deployment_id"], ["capability_deployments.id"], ondelete="RESTRICT"),
)
for column in ("project_id", "suite_revision_id", "capability_deployment_id", "status", "environment_digest"):
op.create_index(f"ix_evaluation_runs_{column}", "evaluation_runs", [column])
op.create_table(
"evaluation_case_results", _id(), _created(),
sa.Column("run_id", sa.Uuid(), nullable=False), sa.Column("case_id", sa.Uuid(), nullable=False),
sa.Column("ranked_results", sa.JSON(), nullable=False), sa.Column("relevant_results", sa.JSON(), nullable=False),
sa.Column("first_relevant_rank", sa.Integer()), sa.Column("metrics", sa.JSON(), nullable=False),
sa.Column("latency_ms", sa.Float(), nullable=False), sa.Column("error_code", sa.String(64)),
sa.ForeignKeyConstraint(["run_id"], ["evaluation_runs.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["case_id"], ["evaluation_cases.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("run_id", "case_id", name="uq_evaluation_case_result"),
)
op.create_index("ix_evaluation_case_results_run_id", "evaluation_case_results", ["run_id"])
op.create_index("ix_evaluation_case_results_case_id", "evaluation_case_results", ["case_id"])
op.create_table(
"evaluation_comparisons", _id(), _created(),
sa.Column("project_id", sa.Uuid(), nullable=False), sa.Column("baseline_run_id", sa.Uuid(), nullable=False),
sa.Column("candidate_run_id", sa.Uuid(), nullable=False), sa.Column("comparability", sa.String(32), nullable=False),
sa.Column("comparability_evidence", sa.JSON(), nullable=False), sa.Column("metric_deltas", sa.JSON(), nullable=False),
sa.Column("improved_cases", sa.Integer(), nullable=False), sa.Column("unchanged_cases", sa.Integer(), nullable=False),
sa.Column("regressed_cases", sa.Integer(), nullable=False), sa.Column("critical_regressions", sa.Integer(), nullable=False),
sa.Column("case_comparisons", sa.JSON(), nullable=False), sa.Column("promotion_eligibility", sa.String(32), nullable=False),
sa.Column("eligibility_evidence", sa.JSON(), nullable=False),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["baseline_run_id"], ["evaluation_runs.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["candidate_run_id"], ["evaluation_runs.id"], ondelete="RESTRICT"),
sa.UniqueConstraint("baseline_run_id", "candidate_run_id", name="uq_evaluation_comparison"),
)
for column in ("project_id", "baseline_run_id", "candidate_run_id"):
op.create_index(f"ix_evaluation_comparisons_{column}", "evaluation_comparisons", [column])
def downgrade() -> None:
for table in (
"evaluation_comparisons", "evaluation_case_results", "evaluation_runs", "evaluation_cases",
"evaluation_suite_revisions", "evaluation_suites", "embedding_migrations", "project_evaluation_bindings",
):
op.drop_table(table)
op.drop_column("service_clients", "workload_priority")
@@ -0,0 +1,103 @@
"""M7 candidate capability routes and lab approval provenance.
Revision ID: 20260825_0010
Revises: 20260825_0009
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260825_0010"
down_revision: str | None = "20260825_0009"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.alter_column(
"capability_deployments",
"production_approval_id",
existing_type=sa.Uuid(),
nullable=True,
)
op.add_column(
"capability_deployments",
sa.Column("execution_approval_id", sa.Uuid(), nullable=True),
)
op.create_index(
"ix_capability_deployments_execution_approval_id",
"capability_deployments",
["execution_approval_id"],
)
op.create_foreign_key(
"fk_capability_deployments_execution_approval_id",
"capability_deployments",
"execution_approvals",
["execution_approval_id"],
["id"],
ondelete="RESTRICT",
)
op.create_table(
"capability_experiment_routes",
sa.Column("route_key", sa.String(length=128), nullable=False),
sa.Column("capability_contract_id", sa.Uuid(), nullable=False),
sa.Column("capability_deployment_id", sa.Uuid(), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False),
sa.Column("purpose", sa.Text(), nullable=False),
sa.Column("evidence", sa.JSON(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(
["capability_contract_id"], ["capability_contracts.id"], ondelete="RESTRICT"
),
sa.ForeignKeyConstraint(
["capability_deployment_id"], ["capability_deployments.id"], ondelete="RESTRICT"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("route_key"),
)
op.create_index(
"ix_capability_experiment_routes_route_key",
"capability_experiment_routes",
["route_key"],
unique=True,
)
op.create_index(
"ix_capability_experiment_routes_capability_contract_id",
"capability_experiment_routes",
["capability_contract_id"],
)
op.create_index(
"ix_capability_experiment_routes_capability_deployment_id",
"capability_experiment_routes",
["capability_deployment_id"],
)
op.create_index(
"ix_capability_experiment_routes_status",
"capability_experiment_routes",
["status"],
)
def downgrade() -> None:
op.drop_table("capability_experiment_routes")
op.drop_constraint(
"fk_capability_deployments_execution_approval_id",
"capability_deployments",
type_="foreignkey",
)
op.drop_index(
"ix_capability_deployments_execution_approval_id",
table_name="capability_deployments",
)
op.drop_column("capability_deployments", "execution_approval_id")
op.alter_column(
"capability_deployments",
"production_approval_id",
existing_type=sa.Uuid(),
nullable=False,
)
@@ -0,0 +1,109 @@
"""M7 first-class model comparisons and deterministic advisor evidence.
Revision ID: 20260825_0011
Revises: 20260825_0010
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260825_0011"
down_revision: str | None = "20260825_0010"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"model_comparisons",
sa.Column("project_id", sa.Uuid(), nullable=False),
sa.Column("capability_contract_id", sa.Uuid(), nullable=False),
sa.Column("suite_revision_id", sa.Uuid(), nullable=False),
sa.Column("current_run_id", sa.Uuid(), nullable=False),
sa.Column("title", sa.String(length=255), nullable=False),
sa.Column("candidates", sa.JSON(), nullable=False),
sa.Column("comparability", sa.String(length=32), nullable=False),
sa.Column("evidence_fingerprint", sa.String(length=64), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["capability_contract_id"], ["capability_contracts.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["suite_revision_id"], ["evaluation_suite_revisions.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["current_run_id"], ["evaluation_runs.id"], ondelete="RESTRICT"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("evidence_fingerprint"),
)
op.create_index("ix_model_comparisons_project_id", "model_comparisons", ["project_id"])
op.create_index("ix_model_comparisons_capability_contract_id", "model_comparisons", ["capability_contract_id"])
op.create_index("ix_model_comparisons_suite_revision_id", "model_comparisons", ["suite_revision_id"])
op.create_table(
"advisor_policies",
sa.Column("key", sa.String(length=128), nullable=False),
sa.Column("required_evidence_level", sa.String(length=8), nullable=False),
sa.Column("critical_regression_hard_block", sa.Boolean(), nullable=False),
sa.Column("maximum_latency_p95_regression_ratio", sa.Float(), nullable=False),
sa.Column("minimum_metric_deltas", sa.JSON(), nullable=False),
sa.Column("require_verified_supply_chain", sa.Boolean(), nullable=False),
sa.Column("require_runtime_fit", sa.Boolean(), nullable=False),
sa.Column("rationale", sa.Text(), nullable=False),
sa.Column("version", sa.Integer(), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("key"),
)
op.create_table(
"advisor_recommendations",
sa.Column("comparison_id", sa.Uuid(), nullable=False),
sa.Column("project_id", sa.Uuid(), nullable=False),
sa.Column("capability_contract_id", sa.Uuid(), nullable=False),
sa.Column("policy_id", sa.Uuid(), nullable=False),
sa.Column("candidate_key", sa.String(length=128), nullable=False),
sa.Column("current_deployment_id", sa.Uuid(), nullable=True),
sa.Column("candidate_deployment_id", sa.Uuid(), nullable=True),
sa.Column("current_embedding_space", sa.String(length=255), nullable=False),
sa.Column("candidate_embedding_space", sa.String(length=255), nullable=True),
sa.Column("verdict", sa.String(length=64), nullable=False),
sa.Column("confidence", sa.String(length=16), nullable=False),
sa.Column("evidence_level", sa.String(length=8), nullable=False),
sa.Column("quality_deltas", sa.JSON(), nullable=False),
sa.Column("latency_deltas", sa.JSON(), nullable=False),
sa.Column("resource_deltas", sa.JSON(), nullable=False),
sa.Column("migration_impact", sa.JSON(), nullable=False),
sa.Column("security_state", sa.JSON(), nullable=False),
sa.Column("key_improvements", sa.JSON(), nullable=False),
sa.Column("blockers", sa.JSON(), nullable=False),
sa.Column("policy_snapshot", sa.JSON(), nullable=False),
sa.Column("evidence_fingerprint", sa.String(length=64), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False),
sa.Column("generated_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("dismissed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("dismissed_by", sa.String(length=255), nullable=True),
sa.Column("dismissal_reason", sa.Text(), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.ForeignKeyConstraint(["comparison_id"], ["model_comparisons.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["capability_contract_id"], ["capability_contracts.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["policy_id"], ["advisor_policies.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["current_deployment_id"], ["capability_deployments.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["candidate_deployment_id"], ["capability_deployments.id"], ondelete="RESTRICT"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("evidence_fingerprint"),
)
op.create_index("ix_advisor_recommendations_comparison_id", "advisor_recommendations", ["comparison_id"])
op.create_index("ix_advisor_recommendations_project_id", "advisor_recommendations", ["project_id"])
op.create_index("ix_advisor_recommendations_capability_contract_id", "advisor_recommendations", ["capability_contract_id"])
op.create_index("ix_advisor_recommendations_verdict", "advisor_recommendations", ["verdict"])
op.create_index("ix_advisor_recommendations_status", "advisor_recommendations", ["status"])
def downgrade() -> None:
op.drop_table("advisor_recommendations")
op.drop_table("advisor_policies")
op.drop_table("model_comparisons")
@@ -0,0 +1,276 @@
"""M8 frozen reranking pools and retrieval pipeline evidence.
Revision ID: 20260825_0012
Revises: 20260825_0011
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260825_0012"
down_revision: str | None = "20260825_0011"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.alter_column("capability_deployments", "embedding_space_id", nullable=True)
op.create_table(
"retrieval_candidate_pools",
sa.Column("project_id", sa.Uuid(), nullable=False),
sa.Column("suite_revision_id", sa.Uuid(), nullable=False),
sa.Column("evaluation_case_id", sa.Uuid(), nullable=False),
sa.Column("source_embedding_space", sa.String(length=255), nullable=False),
sa.Column("source_index_ref", sa.Text(), nullable=False),
sa.Column("corpus_revision", sa.String(length=128), nullable=False),
sa.Column("retrieval_config_digest", sa.String(length=64), nullable=False),
sa.Column("candidate_count", sa.Integer(), nullable=False),
sa.Column("ordered_candidates", sa.JSON(), nullable=False),
sa.Column("fingerprint", sa.String(length=64), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.Column(
"immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.CheckConstraint(
"candidate_count > 0 AND candidate_count <= 40", name="ck_candidate_pool_count"
),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(
["suite_revision_id"], ["evaluation_suite_revisions.id"], ondelete="RESTRICT"
),
sa.ForeignKeyConstraint(
["evaluation_case_id"], ["evaluation_cases.id"], ondelete="RESTRICT"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"evaluation_case_id",
"source_index_ref",
"fingerprint",
name="uq_candidate_pool_case_source_fingerprint",
),
sa.UniqueConstraint("fingerprint"),
)
op.create_index("ix_candidate_pools_project_id", "retrieval_candidate_pools", ["project_id"])
op.create_index(
"ix_candidate_pools_suite_revision_id", "retrieval_candidate_pools", ["suite_revision_id"]
)
op.create_index(
"ix_candidate_pools_evaluation_case_id", "retrieval_candidate_pools", ["evaluation_case_id"]
)
op.create_table(
"retrieval_pipeline_identities",
sa.Column("project_id", sa.Uuid(), nullable=False),
sa.Column("embedding_space_ref", sa.String(length=255), nullable=False),
sa.Column("sparse_config_digest", sa.String(length=64), nullable=False),
sa.Column("fusion_config_digest", sa.String(length=64), nullable=False),
sa.Column("reranker_deployment_id", sa.Uuid(), nullable=True),
sa.Column("reranker_config_digest", sa.String(length=64), nullable=True),
sa.Column("candidate_k", sa.Integer(), nullable=False),
sa.Column("output_k", sa.Integer(), nullable=False),
sa.Column("identity_digest", sa.String(length=64), nullable=False),
sa.Column("configuration", sa.JSON(), nullable=False),
sa.Column("migration_class", sa.String(length=32), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.Column(
"immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.CheckConstraint("candidate_k > 0 AND candidate_k <= 40", name="ck_pipeline_candidate_k"),
sa.CheckConstraint("output_k > 0 AND output_k <= candidate_k", name="ck_pipeline_output_k"),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(
["reranker_deployment_id"], ["capability_deployments.id"], ondelete="RESTRICT"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("identity_digest"),
)
op.create_index(
"ix_pipeline_identities_project_id", "retrieval_pipeline_identities", ["project_id"]
)
op.create_index(
"ix_pipeline_identities_reranker_deployment_id",
"retrieval_pipeline_identities",
["reranker_deployment_id"],
)
op.create_table(
"reranking_evaluation_runs",
sa.Column("project_id", sa.Uuid(), nullable=False),
sa.Column("suite_revision_id", sa.Uuid(), nullable=False),
sa.Column("pipeline_identity_id", sa.Uuid(), nullable=False),
sa.Column("control_pipeline_identity_id", sa.Uuid(), nullable=False),
sa.Column("reranker_deployment_id", sa.Uuid(), nullable=True),
sa.Column("status", sa.String(length=32), nullable=False),
sa.Column("corpus_revision", sa.String(length=128), nullable=False),
sa.Column("candidate_pool_set_fingerprint", sa.String(length=64), nullable=False),
sa.Column("environment_fingerprint", sa.JSON(), nullable=False),
sa.Column("environment_digest", sa.String(length=64), nullable=False),
sa.Column("expected_cases", sa.Integer(), nullable=False),
sa.Column("completed_cases", sa.Integer(), nullable=False),
sa.Column("error_count", sa.Integer(), nullable=False),
sa.Column("aggregate_metrics", sa.JSON(), nullable=False),
sa.Column("latency_metrics", sa.JSON(), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(
["suite_revision_id"], ["evaluation_suite_revisions.id"], ondelete="RESTRICT"
),
sa.ForeignKeyConstraint(
["pipeline_identity_id"], ["retrieval_pipeline_identities.id"], ondelete="RESTRICT"
),
sa.ForeignKeyConstraint(
["control_pipeline_identity_id"],
["retrieval_pipeline_identities.id"],
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["reranker_deployment_id"], ["capability_deployments.id"], ondelete="RESTRICT"
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_reranking_runs_project_id", "reranking_evaluation_runs", ["project_id"])
op.create_index(
"ix_reranking_runs_suite_revision_id", "reranking_evaluation_runs", ["suite_revision_id"]
)
op.create_index(
"ix_reranking_runs_pipeline_identity_id",
"reranking_evaluation_runs",
["pipeline_identity_id"],
)
op.create_index("ix_reranking_runs_status", "reranking_evaluation_runs", ["status"])
op.create_table(
"reranking_case_results",
sa.Column("run_id", sa.Uuid(), nullable=False),
sa.Column("case_id", sa.Uuid(), nullable=False),
sa.Column("candidate_pool_id", sa.Uuid(), nullable=False),
sa.Column("ranked_results", sa.JSON(), nullable=False),
sa.Column("relevant_results", sa.JSON(), nullable=False),
sa.Column("first_relevant_rank", sa.Integer(), nullable=True),
sa.Column("metrics", sa.JSON(), nullable=False),
sa.Column("retrieval_latency_ms", sa.Float(), nullable=False),
sa.Column("rerank_latency_ms", sa.Float(), nullable=False),
sa.Column("total_latency_ms", sa.Float(), nullable=False),
sa.Column("error_code", sa.String(length=64), nullable=True),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.ForeignKeyConstraint(["run_id"], ["reranking_evaluation_runs.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["case_id"], ["evaluation_cases.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(
["candidate_pool_id"], ["retrieval_candidate_pools.id"], ondelete="RESTRICT"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("run_id", "case_id", name="uq_reranking_case_result"),
)
op.create_index("ix_reranking_case_results_run_id", "reranking_case_results", ["run_id"])
op.create_index("ix_reranking_case_results_case_id", "reranking_case_results", ["case_id"])
op.create_index(
"ix_reranking_case_results_pool_id", "reranking_case_results", ["candidate_pool_id"]
)
op.create_table(
"discovery_candidate_assessments",
sa.Column("model_id", sa.Uuid(), nullable=True),
sa.Column("upstream_snapshot_id", sa.Uuid(), nullable=False),
sa.Column("candidate_key", sa.String(length=128), nullable=False),
sa.Column("repository_id", sa.String(length=255), nullable=False),
sa.Column("resolved_commit_sha", sa.String(length=64), nullable=False),
sa.Column("artifact_evidence", sa.JSON(), nullable=False),
sa.Column("security_state", sa.JSON(), nullable=False),
sa.Column("license_state", sa.JSON(), nullable=False),
sa.Column("gpu_fit", sa.JSON(), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False),
sa.Column("rationale", sa.Text(), nullable=False),
sa.Column("evidence_fingerprint", sa.String(length=64), nullable=False),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column(
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.Column(
"immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.ForeignKeyConstraint(["model_id"], ["models.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(
["upstream_snapshot_id"], ["upstream_snapshots.id"], ondelete="RESTRICT"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("evidence_fingerprint"),
)
op.create_index(
"ix_discovery_assessments_model_id", "discovery_candidate_assessments", ["model_id"]
)
op.create_index(
"ix_discovery_assessments_snapshot_id",
"discovery_candidate_assessments",
["upstream_snapshot_id"],
)
op.create_index(
"ix_discovery_assessments_status", "discovery_candidate_assessments", ["status"]
)
op.add_column(
"advisor_recommendations",
sa.Column(
"target_kind",
sa.String(length=32),
nullable=False,
server_default="embedding_deployment",
),
)
op.add_column(
"advisor_recommendations",
sa.Column("current_pipeline_identity_id", sa.Uuid(), nullable=True),
)
op.add_column(
"advisor_recommendations",
sa.Column("candidate_pipeline_identity_id", sa.Uuid(), nullable=True),
)
op.create_foreign_key(
"fk_advisor_current_pipeline",
"advisor_recommendations",
"retrieval_pipeline_identities",
["current_pipeline_identity_id"],
["id"],
ondelete="RESTRICT",
)
op.create_foreign_key(
"fk_advisor_candidate_pipeline",
"advisor_recommendations",
"retrieval_pipeline_identities",
["candidate_pipeline_identity_id"],
["id"],
ondelete="RESTRICT",
)
def downgrade() -> None:
op.drop_constraint(
"fk_advisor_candidate_pipeline", "advisor_recommendations", type_="foreignkey"
)
op.drop_constraint("fk_advisor_current_pipeline", "advisor_recommendations", type_="foreignkey")
op.drop_column("advisor_recommendations", "candidate_pipeline_identity_id")
op.drop_column("advisor_recommendations", "current_pipeline_identity_id")
op.drop_column("advisor_recommendations", "target_kind")
op.drop_table("discovery_candidate_assessments")
op.drop_table("reranking_case_results")
op.drop_table("reranking_evaluation_runs")
op.drop_table("retrieval_pipeline_identities")
op.drop_table("retrieval_candidate_pools")
op.alter_column("capability_deployments", "embedding_space_id", nullable=False)
@@ -0,0 +1,74 @@
"""M9 multimodal capability evaluation foundation.
Revision ID: 20260826_0013
Revises: 20260825_0012
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260826_0013"
down_revision: str | None = "20260825_0012"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"capability_evaluation_suites",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("capability_contract_id", sa.Uuid(), nullable=False),
sa.Column("key", sa.String(length=128), nullable=False),
sa.Column("evaluation_type", sa.String(length=32), nullable=False),
sa.Column("revision", sa.String(length=128), nullable=False),
sa.Column("dataset_revision", sa.String(length=128), nullable=False),
sa.Column("definition_digest", sa.String(length=64), nullable=False),
sa.Column("metric_definitions", sa.JSON(), nullable=False),
sa.Column("case_definitions", sa.JSON(), nullable=False),
sa.Column("thresholds", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["capability_contract_id"], ["capability_contracts.id"], ondelete="RESTRICT"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("capability_contract_id", "key", "revision", name="uq_capability_eval_suite"),
sa.UniqueConstraint("definition_digest", name="uq_capability_eval_definition_digest"),
)
op.create_index("ix_capability_evaluation_suites_capability_contract_id", "capability_evaluation_suites", ["capability_contract_id"])
op.create_index("ix_capability_evaluation_suites_evaluation_type", "capability_evaluation_suites", ["evaluation_type"])
op.create_table(
"capability_evaluation_runs",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("suite_id", sa.Uuid(), nullable=False),
sa.Column("capability_deployment_id", sa.Uuid(), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False),
sa.Column("metric_values", sa.JSON(), nullable=False),
sa.Column("case_results", sa.JSON(), nullable=False),
sa.Column("resource_metrics", sa.JSON(), nullable=False),
sa.Column("environment_fingerprint", sa.String(length=64), nullable=False),
sa.Column("evidence_digest", sa.String(length=64), nullable=False),
sa.Column("evidence", sa.JSON(), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["suite_id"], ["capability_evaluation_suites.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["capability_deployment_id"], ["capability_deployments.id"], ondelete="RESTRICT"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("evidence_digest", name="uq_capability_eval_evidence"),
)
op.create_index("ix_capability_evaluation_runs_suite_id", "capability_evaluation_runs", ["suite_id"])
op.create_index("ix_capability_evaluation_runs_capability_deployment_id", "capability_evaluation_runs", ["capability_deployment_id"])
op.create_index("ix_capability_evaluation_runs_status", "capability_evaluation_runs", ["status"])
op.create_index("ix_capability_evaluation_runs_environment_fingerprint", "capability_evaluation_runs", ["environment_fingerprint"])
def downgrade() -> None:
op.drop_index("ix_capability_evaluation_runs_environment_fingerprint", table_name="capability_evaluation_runs")
op.drop_index("ix_capability_evaluation_runs_status", table_name="capability_evaluation_runs")
op.drop_index("ix_capability_evaluation_runs_capability_deployment_id", table_name="capability_evaluation_runs")
op.drop_index("ix_capability_evaluation_runs_suite_id", table_name="capability_evaluation_runs")
op.drop_table("capability_evaluation_runs")
op.drop_index("ix_capability_evaluation_suites_evaluation_type", table_name="capability_evaluation_suites")
op.drop_index("ix_capability_evaluation_suites_capability_contract_id", table_name="capability_evaluation_suites")
op.drop_table("capability_evaluation_suites")
@@ -0,0 +1,232 @@
"""M10 advanced residency and GPU scheduling.
Revision ID: 20260826_0014
Revises: 20260826_0013
"""
import uuid
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260826_0014"
down_revision: str | None = "20260826_0013"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.add_column(
"residency_allocations",
sa.Column("generation", sa.Integer(), server_default="1", nullable=False),
)
op.add_column(
"residency_allocations", sa.Column("transition_reason", sa.String(length=64), nullable=True)
)
op.add_column(
"serving_gpu_leases",
sa.Column(
"lease_type", sa.String(length=32), server_default="request_execution", nullable=False
),
)
op.add_column(
"serving_gpu_leases",
sa.Column("materialized_vram_bytes", sa.BigInteger(), server_default="0", nullable=False),
)
op.add_column("serving_gpu_leases", sa.Column("serving_job_id", sa.Uuid(), nullable=True))
op.add_column(
"serving_gpu_leases",
sa.Column("generation", sa.Integer(), server_default="1", nullable=False),
)
op.create_foreign_key(
"fk_serving_gpu_leases_job",
"serving_gpu_leases",
"serving_jobs",
["serving_job_id"],
["id"],
ondelete="SET NULL",
)
op.create_index(
"ix_serving_gpu_leases_serving_job_id", "serving_gpu_leases", ["serving_job_id"]
)
op.create_table(
"scheduler_policy_revisions",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("revision", sa.String(length=64), nullable=False),
sa.Column("configuration", sa.JSON(), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False),
sa.Column(
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("revision"),
)
op.create_index(
"ix_scheduler_policy_revisions_active", "scheduler_policy_revisions", ["active"]
)
policy_table = sa.table(
"scheduler_policy_revisions",
sa.column("id", sa.Uuid()),
sa.column("revision", sa.String()),
sa.column("configuration", sa.JSON()),
sa.column("active", sa.Boolean()),
)
op.bulk_insert(
policy_table,
[
{
"id": uuid.UUID("8e4d6a85-8e50-4f78-a182-000000000010"),
"revision": "m10-v1",
"configuration": {
"reserve_minimum_bytes": 1073741824,
"reserve_percentage": 0.05,
"runtime_margin_bytes": 268435456,
"deployment_margin_minimum_bytes": 134217728,
"deployment_margin_percentage": 0.1,
"request_execution_floor_bytes": 67108864,
"pressure_stable_seconds": 30,
"eviction_cooldown_seconds": 60,
"global_queue_limit": 128,
"placement_history_limit": 500,
"lab_paused": False,
},
"active": True,
}
],
)
op.create_table(
"scheduler_accelerator_states",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("accelerator_id", sa.Uuid(), nullable=False),
sa.Column("pressure_state", sa.String(length=16), nullable=False),
sa.Column("recovery_candidate", sa.String(length=16), nullable=True),
sa.Column("recovery_candidate_since", sa.DateTime(timezone=True), nullable=True),
sa.Column(
"pressure_changed_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
sa.Column(
"last_observed_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
sa.ForeignKeyConstraint(["accelerator_id"], ["accelerators.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("accelerator_id"),
)
op.create_index(
"ix_scheduler_accelerator_states_accelerator_id",
"scheduler_accelerator_states",
["accelerator_id"],
unique=True,
)
op.create_table(
"placement_plans",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("capability_deployment_id", sa.Uuid(), nullable=False),
sa.Column("request_id", sa.Uuid(), nullable=True),
sa.Column("policy_revision", sa.String(length=64), nullable=False),
sa.Column("verdict", sa.String(length=32), nullable=False),
sa.Column("reason_codes", sa.JSON(), nullable=False),
sa.Column("evidence", sa.JSON(), nullable=False),
sa.Column("evidence_fingerprint", sa.String(length=64), nullable=False),
sa.Column("dry_run", sa.Boolean(), nullable=False),
sa.Column(
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.ForeignKeyConstraint(
["capability_deployment_id"], ["capability_deployments.id"], ondelete="RESTRICT"
),
sa.PrimaryKeyConstraint("id"),
)
for column in (
"capability_deployment_id",
"request_id",
"verdict",
"evidence_fingerprint",
"created_at",
):
op.create_index(f"ix_placement_plans_{column}", "placement_plans", [column])
op.create_table(
"co_residency_evidence",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("left_deployment_id", sa.Uuid(), nullable=False),
sa.Column("right_deployment_id", sa.Uuid(), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False),
sa.Column("expected_combined_bytes", sa.BigInteger(), nullable=False),
sa.Column("measured_combined_bytes", sa.BigInteger(), nullable=True),
sa.Column("evidence", sa.JSON(), nullable=False),
sa.Column("measured_at", sa.DateTime(timezone=True), nullable=True),
sa.Column(
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.ForeignKeyConstraint(
["left_deployment_id"], ["capability_deployments.id"], ondelete="RESTRICT"
),
sa.ForeignKeyConstraint(
["right_deployment_id"], ["capability_deployments.id"], ondelete="RESTRICT"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"left_deployment_id", "right_deployment_id", name="uq_co_residency_pair"
),
)
for column in ("left_deployment_id", "right_deployment_id", "status"):
op.create_index(f"ix_co_residency_evidence_{column}", "co_residency_evidence", [column])
op.create_table(
"scheduler_evictions",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("capability_deployment_id", sa.Uuid(), nullable=False),
sa.Column("requested_deployment_id", sa.Uuid(), nullable=True),
sa.Column("reason_code", sa.String(length=64), nullable=False),
sa.Column("expected_reclaimed_bytes", sa.BigInteger(), nullable=False),
sa.Column("actual_reclaimed_bytes", sa.BigInteger(), nullable=True),
sa.Column("state", sa.String(length=32), nullable=False),
sa.Column(
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.ForeignKeyConstraint(
["capability_deployment_id"], ["capability_deployments.id"], ondelete="RESTRICT"
),
sa.ForeignKeyConstraint(
["requested_deployment_id"], ["capability_deployments.id"], ondelete="SET NULL"
),
sa.PrimaryKeyConstraint("id"),
)
for column in ("capability_deployment_id", "requested_deployment_id", "reason_code"):
op.create_index(f"ix_scheduler_evictions_{column}", "scheduler_evictions", [column])
def downgrade() -> None:
op.drop_index(
"ix_scheduler_accelerator_states_accelerator_id", table_name="scheduler_accelerator_states"
)
op.drop_table("scheduler_accelerator_states")
for column in ("capability_deployment_id", "requested_deployment_id", "reason_code"):
op.drop_index(f"ix_scheduler_evictions_{column}", table_name="scheduler_evictions")
op.drop_table("scheduler_evictions")
for column in ("left_deployment_id", "right_deployment_id", "status"):
op.drop_index(f"ix_co_residency_evidence_{column}", table_name="co_residency_evidence")
op.drop_table("co_residency_evidence")
for column in (
"capability_deployment_id",
"request_id",
"verdict",
"evidence_fingerprint",
"created_at",
):
op.drop_index(f"ix_placement_plans_{column}", table_name="placement_plans")
op.drop_table("placement_plans")
op.drop_index("ix_scheduler_policy_revisions_active", table_name="scheduler_policy_revisions")
op.drop_table("scheduler_policy_revisions")
op.drop_index("ix_serving_gpu_leases_serving_job_id", table_name="serving_gpu_leases")
op.drop_constraint("fk_serving_gpu_leases_job", "serving_gpu_leases", type_="foreignkey")
for column in ("generation", "serving_job_id", "materialized_vram_bytes", "lease_type"):
op.drop_column("serving_gpu_leases", column)
op.drop_column("residency_allocations", "transition_reason")
op.drop_column("residency_allocations", "generation")
@@ -0,0 +1,54 @@
"""Use 64-bit scheduler and worker generations.
Revision ID: 20260826_0015
Revises: 20260826_0014
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260826_0015"
down_revision: str | None = "20260826_0014"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.alter_column(
"residency_allocations",
"generation",
existing_type=sa.Integer(),
type_=sa.BigInteger(),
existing_nullable=False,
existing_server_default="1",
)
op.alter_column(
"serving_gpu_leases",
"generation",
existing_type=sa.Integer(),
type_=sa.BigInteger(),
existing_nullable=False,
existing_server_default="1",
)
def downgrade() -> None:
op.alter_column(
"serving_gpu_leases",
"generation",
existing_type=sa.BigInteger(),
type_=sa.Integer(),
existing_nullable=False,
existing_server_default="1",
)
op.alter_column(
"residency_allocations",
"generation",
existing_type=sa.BigInteger(),
type_=sa.Integer(),
existing_nullable=False,
existing_server_default="1",
)
@@ -0,0 +1,65 @@
"""Add operational project integrations and project-fit evidence.
Revision ID: 20260826_0016
Revises: 20260826_0015
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260826_0016"
down_revision: str | None = "20260826_0015"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.add_column("service_clients", sa.Column("project_binding_id", sa.Uuid(), nullable=True))
op.add_column("service_clients", sa.Column("integration_environment", sa.String(32), nullable=True))
op.add_column("service_clients", sa.Column("purpose", sa.Text(), nullable=True))
op.create_foreign_key(
"fk_service_clients_project_binding",
"service_clients",
"project_bindings",
["project_binding_id"],
["id"],
ondelete="RESTRICT",
)
op.create_index("ix_service_clients_project_binding_id", "service_clients", ["project_binding_id"])
op.create_table(
"project_fit_evidence",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("project_binding_id", sa.Uuid(), nullable=False),
sa.Column("capability_deployment_id", sa.Uuid(), nullable=True),
sa.Column("environment", sa.String(32), nullable=False),
sa.Column("recommendation", sa.String(32), nullable=False),
sa.Column("case_count", sa.Integer(), nullable=False),
sa.Column("metric_values", sa.JSON(), nullable=False),
sa.Column("critical_errors", sa.Integer(), nullable=False),
sa.Column("blockers", sa.JSON(), nullable=False),
sa.Column("evidence_digest", sa.String(64), nullable=False),
sa.Column("evidence", sa.JSON(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["project_binding_id"], ["project_bindings.id"], ondelete="RESTRICT"),
sa.ForeignKeyConstraint(["capability_deployment_id"], ["capability_deployments.id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("evidence_digest", name="uq_project_fit_evidence_digest"),
)
op.create_index("ix_project_fit_evidence_project_binding_id", "project_fit_evidence", ["project_binding_id"])
op.create_index("ix_project_fit_evidence_capability_deployment_id", "project_fit_evidence", ["capability_deployment_id"])
op.create_index("ix_project_fit_evidence_recommendation", "project_fit_evidence", ["recommendation"])
def downgrade() -> None:
op.drop_index("ix_project_fit_evidence_recommendation", table_name="project_fit_evidence")
op.drop_index("ix_project_fit_evidence_capability_deployment_id", table_name="project_fit_evidence")
op.drop_index("ix_project_fit_evidence_project_binding_id", table_name="project_fit_evidence")
op.drop_table("project_fit_evidence")
op.drop_index("ix_service_clients_project_binding_id", table_name="service_clients")
op.drop_constraint("fk_service_clients_project_binding", "service_clients", type_="foreignkey")
op.drop_column("service_clients", "purpose")
op.drop_column("service_clients", "integration_environment")
op.drop_column("service_clients", "project_binding_id")
@@ -0,0 +1,82 @@
"""Classify project-fit evidence and defer production validation explicitly.
Revision ID: 20260826_0017
Revises: 20260826_0016
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260826_0017"
down_revision: str | None = "20260826_0016"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.add_column(
"project_fit_evidence",
sa.Column(
"evidence_class",
sa.String(32),
server_default="CATALOG_REFERENCE",
nullable=False,
),
)
op.add_column(
"project_fit_evidence",
sa.Column(
"engineering_integration",
sa.String(32),
server_default="PASS",
nullable=False,
),
)
op.add_column(
"project_fit_evidence",
sa.Column(
"production_validation",
sa.String(48),
server_default="DEFERRED_EXTERNAL_VALIDATION",
nullable=False,
),
)
op.add_column(
"project_fit_evidence",
sa.Column("production_action", sa.String(32), server_default="NONE", nullable=False),
)
op.create_index(
"ix_project_fit_evidence_evidence_class",
"project_fit_evidence",
["evidence_class"],
)
op.create_index(
"ix_project_fit_evidence_production_validation",
"project_fit_evidence",
["production_validation"],
)
for column in (
"evidence_class",
"engineering_integration",
"production_validation",
"production_action",
):
op.alter_column("project_fit_evidence", column, server_default=None)
def downgrade() -> None:
op.drop_index(
"ix_project_fit_evidence_production_validation",
table_name="project_fit_evidence",
)
op.drop_index(
"ix_project_fit_evidence_evidence_class",
table_name="project_fit_evidence",
)
op.drop_column("project_fit_evidence", "production_action")
op.drop_column("project_fit_evidence", "production_validation")
op.drop_column("project_fit_evidence", "engineering_integration")
op.drop_column("project_fit_evidence", "evidence_class")
@@ -0,0 +1,431 @@
"""Add evidence-bound lifecycle, rollback, retention and cleanup records.
Revision ID: 20260826_0018
Revises: 20260826_0017
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260826_0018"
down_revision: str | None = "20260826_0017"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def id_column() -> sa.Column[object]:
return sa.Column("id", sa.Uuid(), primary_key=True, nullable=False)
def created_at() -> sa.Column[object]:
return sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now())
def upgrade() -> None:
op.create_table(
"lifecycle_subjects",
id_column(),
sa.Column("target_type", sa.String(48), nullable=False),
sa.Column("target_ref", sa.String(255), nullable=False),
sa.Column("environment", sa.String(32), nullable=False),
sa.Column("state", sa.String(48), nullable=False),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.Column("superseded_by_ref", sa.String(255)),
created_at(),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.CheckConstraint("version >= 1", name="ck_lifecycle_subject_version"),
sa.UniqueConstraint(
"target_type", "target_ref", "environment", name="uq_lifecycle_subject_target"
),
)
op.create_index("ix_lifecycle_subjects_target_type", "lifecycle_subjects", ["target_type"])
op.create_index("ix_lifecycle_subjects_target_ref", "lifecycle_subjects", ["target_ref"])
op.create_index("ix_lifecycle_subjects_environment", "lifecycle_subjects", ["environment"])
op.create_index("ix_lifecycle_subjects_state", "lifecycle_subjects", ["state"])
op.create_table(
"lifecycle_policy_revisions",
id_column(),
sa.Column("key", sa.String(128), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("scope", sa.String(48), nullable=False),
sa.Column("requirements", sa.JSON(), nullable=False),
sa.Column("fingerprint", sa.String(64), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("created_by", sa.String(255), nullable=False),
created_at(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("key", "revision", name="uq_lifecycle_policy_revision"),
sa.UniqueConstraint("fingerprint", name="uq_lifecycle_policy_fingerprint"),
)
op.create_index("ix_lifecycle_policy_revisions_key", "lifecycle_policy_revisions", ["key"])
op.create_index("ix_lifecycle_policy_revisions_scope", "lifecycle_policy_revisions", ["scope"])
op.create_index(
"ix_lifecycle_policy_revisions_active", "lifecycle_policy_revisions", ["active"]
)
op.create_table(
"lifecycle_approval_requests",
id_column(),
sa.Column(
"policy_revision_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_policy_revisions.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"subject_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_subjects.id", ondelete="RESTRICT"),
),
sa.Column("target_type", sa.String(48), nullable=False),
sa.Column("target_ref", sa.String(255), nullable=False),
sa.Column("environment", sa.String(32), nullable=False),
sa.Column("requested_transition", sa.String(48), nullable=False),
sa.Column("evidence_snapshot", sa.JSON(), nullable=False),
sa.Column("evidence_fingerprint", sa.String(64), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("blockers", sa.JSON(), nullable=False),
sa.Column("warnings", sa.JSON(), nullable=False),
sa.Column("requested_by", sa.String(255), nullable=False),
sa.Column("approved_by", sa.String(255)),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True)),
sa.Column("stale_at", sa.DateTime(timezone=True)),
sa.Column("decided_at", sa.DateTime(timezone=True)),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
created_at(),
sa.CheckConstraint("version >= 1", name="ck_lifecycle_approval_version"),
)
for column in (
"policy_revision_id",
"subject_id",
"target_type",
"target_ref",
"environment",
"evidence_fingerprint",
"status",
"expires_at",
"created_at",
):
op.create_index(
f"ix_lifecycle_approval_requests_{column}", "lifecycle_approval_requests", [column]
)
op.create_table(
"lifecycle_approval_evidence",
sa.Column(
"approval_request_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_approval_requests.id", ondelete="RESTRICT"),
primary_key=True,
),
sa.Column("evidence_type", sa.String(64), primary_key=True),
sa.Column("evidence_ref", sa.String(255), primary_key=True),
sa.Column("evidence_digest", sa.String(64), nullable=False),
)
op.create_index(
"ix_lifecycle_approval_evidence_evidence_ref",
"lifecycle_approval_evidence",
["evidence_ref"],
)
op.create_table(
"lifecycle_promotion_plans",
id_column(),
sa.Column(
"approval_request_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_approval_requests.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"subject_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_subjects.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("current_state", sa.String(48), nullable=False),
sa.Column("desired_state", sa.String(48), nullable=False),
sa.Column("migration_class", sa.String(32), nullable=False),
sa.Column(
"candidate_deployment_id",
sa.Uuid(),
sa.ForeignKey("capability_deployments.id", ondelete="RESTRICT"),
),
sa.Column("rollback_target_ref", sa.String(255), nullable=False),
sa.Column("project_consumers", sa.JSON(), nullable=False),
sa.Column("affected_identities", sa.JSON(), nullable=False),
sa.Column("impact_analysis", sa.JSON(), nullable=False),
sa.Column(
"migration_id",
sa.Uuid(),
sa.ForeignKey("embedding_migrations.id", ondelete="RESTRICT"),
),
sa.Column("canary_strategy", sa.JSON(), nullable=False),
sa.Column("drain_strategy", sa.JSON(), nullable=False),
sa.Column("health_gates", sa.JSON(), nullable=False),
sa.Column("automatic_abort_conditions", sa.JSON(), nullable=False),
sa.Column("plan_fingerprint", sa.String(64), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="DRAFT"),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.Column("created_by", sa.String(255), nullable=False),
sa.Column("approved_by", sa.String(255)),
sa.Column("immutable_at", sa.DateTime(timezone=True)),
created_at(),
sa.CheckConstraint("version >= 1", name="ck_lifecycle_plan_version"),
sa.UniqueConstraint("plan_fingerprint", name="uq_lifecycle_plan_fingerprint"),
)
for column in (
"approval_request_id",
"subject_id",
"migration_class",
"candidate_deployment_id",
"migration_id",
"status",
"created_at",
):
op.create_index(
f"ix_lifecycle_promotion_plans_{column}", "lifecycle_promotion_plans", [column]
)
op.create_table(
"lifecycle_rollback_snapshots",
id_column(),
sa.Column(
"promotion_plan_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_promotion_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("snapshot", sa.JSON(), nullable=False),
sa.Column("snapshot_digest", sa.String(64), nullable=False),
created_at(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("promotion_plan_id", name="uq_lifecycle_rollback_plan"),
sa.UniqueConstraint("snapshot_digest", name="uq_lifecycle_rollback_digest"),
)
op.create_index(
"ix_lifecycle_rollback_snapshots_promotion_plan_id",
"lifecycle_rollback_snapshots",
["promotion_plan_id"],
)
op.create_table(
"lifecycle_operations",
id_column(),
sa.Column(
"promotion_plan_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_promotion_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("stage", sa.String(32), nullable=False),
sa.Column("idempotency_key", sa.String(128), nullable=False),
sa.Column("expected_subject_version", sa.Integer(), nullable=False),
sa.Column("requester", sa.String(255), nullable=False),
sa.Column("approver", sa.String(255), nullable=False),
sa.Column("executor", sa.String(255), nullable=False),
sa.Column("failure_code", sa.String(64)),
sa.Column("failure_details", sa.JSON(), nullable=False),
sa.Column("rollback_duration_ms", sa.Float()),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.Column("started_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("finished_at", sa.DateTime(timezone=True)),
sa.CheckConstraint("version >= 1", name="ck_lifecycle_operation_version"),
sa.UniqueConstraint("idempotency_key", name="uq_lifecycle_operation_idempotency"),
)
op.create_index(
"ix_lifecycle_operations_promotion_plan_id", "lifecycle_operations", ["promotion_plan_id"]
)
op.create_index("ix_lifecycle_operations_stage", "lifecycle_operations", ["stage"])
op.create_table(
"lifecycle_canary_runs",
id_column(),
sa.Column(
"operation_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_operations.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("mode", sa.String(48), nullable=False),
sa.Column("traffic_percent", sa.Integer(), nullable=False, server_default="0"),
sa.Column("eligible_clients", sa.JSON(), nullable=False),
sa.Column("eligible_projects", sa.JSON(), nullable=False),
sa.Column("target_request_count", sa.Integer(), nullable=False),
sa.Column("thresholds", sa.JSON(), nullable=False),
sa.Column("status", sa.String(48), nullable=False),
sa.Column("request_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column("error_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column("latency_p95_ms", sa.Float()),
sa.Column("abort_trigger", sa.String(64)),
sa.Column("result", sa.JSON(), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("finished_at", sa.DateTime(timezone=True)),
sa.UniqueConstraint("operation_id", name="uq_lifecycle_canary_operation"),
)
op.create_index(
"ix_lifecycle_canary_runs_operation_id", "lifecycle_canary_runs", ["operation_id"]
)
op.create_index("ix_lifecycle_canary_runs_status", "lifecycle_canary_runs", ["status"])
op.create_table(
"retention_policy_revisions",
id_column(),
sa.Column("key", sa.String(128), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("minimum_rollback_days", sa.Integer(), nullable=False),
sa.Column("requirements", sa.JSON(), nullable=False),
sa.Column("fingerprint", sa.String(64), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("created_by", sa.String(255), nullable=False),
created_at(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("key", "revision", name="uq_retention_policy_revision"),
sa.UniqueConstraint("fingerprint", name="uq_retention_policy_fingerprint"),
)
op.create_index("ix_retention_policy_revisions_key", "retention_policy_revisions", ["key"])
op.create_index(
"ix_retention_policy_revisions_active", "retention_policy_revisions", ["active"]
)
op.create_table(
"lifecycle_retention_records",
id_column(),
sa.Column(
"policy_revision_id",
sa.Uuid(),
sa.ForeignKey("retention_policy_revisions.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("target_type", sa.String(48), nullable=False),
sa.Column("target_ref", sa.String(255), nullable=False),
sa.Column("state", sa.String(32), nullable=False),
sa.Column("retained_until", sa.DateTime(timezone=True)),
sa.Column("legal_hold", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("reason", sa.Text(), nullable=False),
created_at(),
sa.UniqueConstraint("target_type", "target_ref", name="uq_lifecycle_retention_target"),
)
for column in ("policy_revision_id", "target_type", "target_ref", "state", "retained_until"):
op.create_index(
f"ix_lifecycle_retention_records_{column}", "lifecycle_retention_records", [column]
)
op.create_table(
"lifecycle_cleanup_plans",
id_column(),
sa.Column("target_type", sa.String(48), nullable=False),
sa.Column("target_ref", sa.String(255), nullable=False),
sa.Column("action", sa.String(48), nullable=False),
sa.Column("dependencies", sa.JSON(), nullable=False),
sa.Column("dependency_digest", sa.String(64), nullable=False),
sa.Column("reclaimable_bytes", sa.BigInteger(), nullable=False),
sa.Column("retention_state", sa.String(32), nullable=False),
sa.Column("blockers", sa.JSON(), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("created_by", sa.String(255), nullable=False),
created_at(),
sa.Column("executed_at", sa.DateTime(timezone=True)),
)
for column in ("target_type", "target_ref", "status", "created_at"):
op.create_index(f"ix_lifecycle_cleanup_plans_{column}", "lifecycle_cleanup_plans", [column])
op.create_table(
"artifact_location_removal_records",
id_column(),
sa.Column(
"artifact_location_id",
sa.Uuid(),
sa.ForeignKey("artifact_locations.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"cleanup_plan_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_cleanup_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("prior_status", sa.String(32), nullable=False),
sa.Column("prior_observed_sha256", sa.String(64)),
sa.Column("prior_size_bytes", sa.BigInteger()),
sa.Column("removed_by", sa.String(255), nullable=False),
sa.Column("removed_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index(
"ix_artifact_location_removal_records_artifact_location_id",
"artifact_location_removal_records",
["artifact_location_id"],
)
op.create_index(
"ix_artifact_location_removal_records_cleanup_plan_id",
"artifact_location_removal_records",
["cleanup_plan_id"],
)
op.create_table(
"lifecycle_events",
id_column(),
sa.Column("event_type", sa.String(64), nullable=False),
sa.Column("object_type", sa.String(48), nullable=False),
sa.Column("object_ref", sa.String(255), nullable=False),
sa.Column("from_state", sa.String(48)),
sa.Column("to_state", sa.String(48)),
sa.Column("actor", sa.String(255), nullable=False),
sa.Column("actor_role", sa.String(32), nullable=False),
sa.Column(
"policy_revision_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_policy_revisions.id", ondelete="RESTRICT"),
),
sa.Column("evidence_ids", sa.JSON(), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("change_id", sa.String(64), nullable=False),
sa.Column("details", sa.JSON(), nullable=False),
sa.Column("occurred_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
for column in (
"event_type",
"object_type",
"object_ref",
"policy_revision_id",
"change_id",
"occurred_at",
):
op.create_index(f"ix_lifecycle_events_{column}", "lifecycle_events", [column])
op.create_index(
"uq_capability_deployment_one_production_stable",
"capability_deployments",
["capability_contract_id"],
unique=True,
postgresql_where=sa.text("production = true AND status = 'stable'"),
sqlite_where=sa.text("production = 1 AND status = 'stable'"),
)
def downgrade() -> None:
op.drop_index(
"uq_capability_deployment_one_production_stable",
table_name="capability_deployments",
)
for table in (
"lifecycle_events",
"artifact_location_removal_records",
"lifecycle_cleanup_plans",
"lifecycle_retention_records",
"retention_policy_revisions",
"lifecycle_canary_runs",
"lifecycle_operations",
"lifecycle_rollback_snapshots",
"lifecycle_promotion_plans",
"lifecycle_approval_evidence",
"lifecycle_approval_requests",
"lifecycle_policy_revisions",
"lifecycle_subjects",
):
op.drop_table(table)
@@ -0,0 +1,415 @@
"""Add typed resumable migration-engine records.
Revision ID: 20260826_0019
Revises: 20260826_0018
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260826_0019"
down_revision: str | None = "20260826_0018"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _id() -> sa.Column[object]:
return sa.Column("id", sa.Uuid(), primary_key=True, nullable=False)
def _created() -> sa.Column[object]:
return sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now())
def upgrade() -> None:
op.create_table(
"migration_validation_policy_revisions",
_id(),
sa.Column("key", sa.String(128), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("required_completeness", sa.Float(), nullable=False),
sa.Column("allowed_failures", sa.Integer(), nullable=False),
sa.Column("required_evaluation", sa.Boolean(), nullable=False),
sa.Column("critical_regressions_allowed", sa.Integer(), nullable=False),
sa.Column("maximum_latency_regression_ratio", sa.Float()),
sa.Column("require_project_fit", sa.Boolean(), nullable=False),
sa.Column("require_external_validation", sa.Boolean(), nullable=False),
sa.Column("require_security_approved", sa.Boolean(), nullable=False),
sa.Column("allow_isolated_lab_cutover", sa.Boolean(), nullable=False),
sa.Column("fingerprint", sa.String(64), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("created_by", sa.String(255), nullable=False),
_created(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("key", "revision", name="uq_migration_validation_policy_revision"),
sa.UniqueConstraint("fingerprint", name="uq_migration_validation_policy_fingerprint"),
)
op.create_index(
"ix_migration_validation_policy_revisions_key",
"migration_validation_policy_revisions",
["key"],
)
op.create_index(
"ix_migration_validation_policy_revisions_active",
"migration_validation_policy_revisions",
["active"],
)
op.create_table(
"migration_plans",
_id(),
sa.Column(
"project_id",
sa.Uuid(),
sa.ForeignKey("projects.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"project_binding_id",
sa.Uuid(),
sa.ForeignKey("project_bindings.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"capability_contract_id",
sa.Uuid(),
sa.ForeignKey("capability_contracts.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("migration_class", sa.String(32), nullable=False),
sa.Column("environment", sa.String(32), nullable=False),
sa.Column("adapter", sa.JSON(), nullable=False),
sa.Column("source_identity", sa.JSON(), nullable=False),
sa.Column("target_identity", sa.JSON(), nullable=False),
sa.Column("source_data_target", sa.Text(), nullable=False),
sa.Column("target_shadow_target", sa.Text(), nullable=False),
sa.Column("source_space_ref", sa.String(255), nullable=False),
sa.Column(
"target_space_id",
sa.Uuid(),
sa.ForeignKey("embedding_spaces.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("corpus_revision", sa.String(128), nullable=False),
sa.Column("migration_policy_revision", sa.String(128), nullable=False),
sa.Column(
"validation_policy_revision_id",
sa.Uuid(),
sa.ForeignKey("migration_validation_policy_revisions.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"lifecycle_approval_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_approval_requests.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"promotion_plan_id",
sa.Uuid(),
sa.ForeignKey("lifecycle_promotion_plans.id", ondelete="RESTRICT"),
),
sa.Column("rollback_target_ref", sa.Text(), nullable=False),
sa.Column("total_expected_items", sa.Integer(), nullable=False),
sa.Column("batch_size", sa.Integer(), nullable=False),
sa.Column("max_in_flight_batches", sa.Integer(), nullable=False),
sa.Column("concurrency", sa.Integer(), nullable=False),
sa.Column("priority", sa.String(32), nullable=False),
sa.Column("target_storage", sa.JSON(), nullable=False),
sa.Column("shadow_policy", sa.JSON(), nullable=False),
sa.Column("cutover_policy", sa.JSON(), nullable=False),
sa.Column("rollback_retention_days", sa.Integer(), nullable=False),
sa.Column("environment_fingerprint", sa.String(64), nullable=False),
sa.Column("idempotency_key", sa.String(128), nullable=False),
sa.Column("created_by", sa.String(255), nullable=False),
sa.Column("irreversible", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("schema_steps", sa.JSON(), nullable=False),
sa.Column("state", sa.String(48), nullable=False, server_default="PLANNED"),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.Column("generation", sa.BigInteger(), nullable=False, server_default="1"),
sa.Column("plan_fingerprint", sa.String(64), nullable=False),
sa.Column("approval_fingerprint", sa.String(64), nullable=False),
sa.Column("completed_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("failed_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("retryable_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("permanent_failed_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("last_cursor", sa.String(255)),
sa.Column("cancel_requested", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("failure_code", sa.String(64)),
sa.Column("failure_details", sa.JSON(), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True)),
sa.Column("completed_at", sa.DateTime(timezone=True)),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
_created(),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("idempotency_key", name="uq_migration_plan_idempotency"),
sa.UniqueConstraint("plan_fingerprint", name="uq_migration_plan_fingerprint"),
sa.UniqueConstraint("target_shadow_target", name="uq_migration_plans_target_shadow_target"),
sa.CheckConstraint("version >= 1", name="ck_migration_plan_version"),
sa.CheckConstraint("generation >= 1", name="ck_migration_plan_generation"),
sa.CheckConstraint(
"completed_items >= 0 AND failed_items >= 0", name="ck_migration_plan_progress"
),
)
for column in (
"project_id",
"project_binding_id",
"capability_contract_id",
"migration_class",
"environment",
"target_space_id",
"corpus_revision",
"validation_policy_revision_id",
"lifecycle_approval_id",
"promotion_plan_id",
"state",
):
op.create_index(f"ix_migration_plans_{column}", "migration_plans", [column])
op.create_index(
"uq_migration_one_production_active",
"migration_plans",
["project_id", "capability_contract_id"],
unique=True,
postgresql_where=sa.text(
"environment = 'PRODUCTION' AND state NOT IN "
"('CUTOVER_COMMITTED','ROLLED_BACK','FAILED','CANCELLED')"
),
)
op.create_table(
"migration_batch_checkpoints",
_id(),
sa.Column(
"migration_plan_id",
sa.Uuid(),
sa.ForeignKey("migration_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("batch_number", sa.Integer(), nullable=False),
sa.Column("generation", sa.BigInteger(), nullable=False),
sa.Column("cursor_start", sa.String(255), nullable=False),
sa.Column("cursor_end", sa.String(255), nullable=False),
sa.Column("item_count", sa.Integer(), nullable=False),
sa.Column("completed_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("failed_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("retryable_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("permanent_failed_items", sa.Integer(), nullable=False, server_default="0"),
sa.Column("item_fingerprint", sa.String(64), nullable=False),
sa.Column("result_fingerprint", sa.String(64)),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("attempts", sa.Integer(), nullable=False, server_default="0"),
sa.Column("duration_ms", sa.Float()),
sa.Column("error_code", sa.String(64)),
sa.Column("bounded_errors", sa.JSON(), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True)),
sa.Column("completed_at", sa.DateTime(timezone=True)),
sa.UniqueConstraint(
"migration_plan_id", "batch_number", "generation", name="uq_migration_batch"
),
)
op.create_index(
"ix_migration_batch_checkpoints_migration_plan_id",
"migration_batch_checkpoints",
["migration_plan_id"],
)
op.create_index(
"ix_migration_batch_checkpoints_status", "migration_batch_checkpoints", ["status"]
)
op.create_table(
"migration_validation_snapshots",
_id(),
sa.Column(
"migration_plan_id",
sa.Uuid(),
sa.ForeignKey("migration_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"validation_policy_revision_id",
sa.Uuid(),
sa.ForeignKey("migration_validation_policy_revisions.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("generation", sa.BigInteger(), nullable=False),
sa.Column("expected_count", sa.Integer(), nullable=False),
sa.Column("actual_count", sa.Integer(), nullable=False),
sa.Column("missing_count", sa.Integer(), nullable=False),
sa.Column("duplicate_count", sa.Integer(), nullable=False),
sa.Column("malformed_count", sa.Integer(), nullable=False),
sa.Column("non_finite_count", sa.Integer(), nullable=False),
sa.Column("wrong_dimension_count", sa.Integer(), nullable=False),
sa.Column("content_hash_mismatch_count", sa.Integer(), nullable=False),
sa.Column("wrong_space_count", sa.Integer(), nullable=False),
sa.Column("index_schema_matches", sa.Boolean(), nullable=False),
sa.Column("distance_metric_matches", sa.Boolean(), nullable=False),
sa.Column("payload_integrity", sa.Boolean(), nullable=False),
sa.Column("target_fingerprint", sa.String(64), nullable=False),
sa.Column("evaluation_run_ids", sa.JSON(), nullable=False),
sa.Column("comparable", sa.Boolean(), nullable=False),
sa.Column("critical_regressions", sa.Integer(), nullable=False),
sa.Column("latency_regression_ratio", sa.Float()),
sa.Column("project_fit_eligible", sa.Boolean(), nullable=False),
sa.Column("external_validation_satisfied", sa.Boolean(), nullable=False),
sa.Column("security_approved", sa.Boolean(), nullable=False),
sa.Column("evidence", sa.JSON(), nullable=False),
sa.Column("snapshot_fingerprint", sa.String(64), nullable=False),
sa.Column("passed", sa.Boolean(), nullable=False),
sa.Column("technical_cutover_eligible", sa.Boolean(), nullable=False),
sa.Column("project_promotion_eligible", sa.Boolean(), nullable=False),
sa.Column("blockers", sa.JSON(), nullable=False),
_created(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("snapshot_fingerprint", name="uq_migration_validation_snapshot"),
)
op.create_index(
"ix_migration_validation_snapshots_migration_plan_id",
"migration_validation_snapshots",
["migration_plan_id"],
)
op.create_index(
"ix_migration_validation_snapshots_validation_policy_revision_id",
"migration_validation_snapshots",
["validation_policy_revision_id"],
)
op.create_index(
"ix_migration_validation_snapshots_passed", "migration_validation_snapshots", ["passed"]
)
op.create_table(
"migration_shadow_sessions",
_id(),
sa.Column(
"migration_plan_id",
sa.Uuid(),
sa.ForeignKey("migration_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("generation", sa.BigInteger(), nullable=False),
sa.Column("status", sa.String(32), nullable=False),
sa.Column("request_count", sa.Integer(), nullable=False),
sa.Column("source_error_count", sa.Integer(), nullable=False),
sa.Column("target_error_count", sa.Integer(), nullable=False),
sa.Column("source_latency_p95_ms", sa.Float(), nullable=False),
sa.Column("target_latency_p95_ms", sa.Float(), nullable=False),
sa.Column("critical_regressions", sa.Integer(), nullable=False),
sa.Column("metrics", sa.JSON(), nullable=False),
sa.Column("evidence_refs", sa.JSON(), nullable=False),
sa.Column("result_fingerprint", sa.String(64), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("completed_at", sa.DateTime(timezone=True)),
sa.UniqueConstraint("result_fingerprint", name="uq_migration_shadow_result_fingerprint"),
)
op.create_index(
"ix_migration_shadow_sessions_migration_plan_id",
"migration_shadow_sessions",
["migration_plan_id"],
)
op.create_index("ix_migration_shadow_sessions_status", "migration_shadow_sessions", ["status"])
op.create_table(
"migration_rollback_snapshots",
_id(),
sa.Column(
"migration_plan_id",
sa.Uuid(),
sa.ForeignKey("migration_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("snapshot", sa.JSON(), nullable=False),
sa.Column("snapshot_fingerprint", sa.String(64), nullable=False),
sa.Column("retain_until", sa.DateTime(timezone=True), nullable=False),
_created(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("migration_plan_id", name="uq_migration_rollback_plan"),
sa.UniqueConstraint("snapshot_fingerprint", name="uq_migration_rollback_fingerprint"),
)
op.create_index(
"ix_migration_rollback_snapshots_migration_plan_id",
"migration_rollback_snapshots",
["migration_plan_id"],
)
op.create_index(
"ix_migration_rollback_snapshots_retain_until",
"migration_rollback_snapshots",
["retain_until"],
)
op.create_table(
"migration_cutover_operations",
_id(),
sa.Column(
"migration_plan_id",
sa.Uuid(),
sa.ForeignKey("migration_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("stage", sa.String(32), nullable=False),
sa.Column("idempotency_key", sa.String(128), nullable=False),
sa.Column("generation", sa.BigInteger(), nullable=False),
sa.Column("expected_plan_version", sa.Integer(), nullable=False),
sa.Column("source_before", sa.Text(), nullable=False),
sa.Column("target_after", sa.Text(), nullable=False),
sa.Column("external_state_fingerprint", sa.String(64), nullable=False),
sa.Column("health_evidence", sa.JSON(), nullable=False),
sa.Column("failure_code", sa.String(64)),
sa.Column("failure_details", sa.JSON(), nullable=False),
sa.Column("switch_duration_ms", sa.Float()),
sa.Column("rollback_duration_ms", sa.Float()),
sa.Column("started_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("finished_at", sa.DateTime(timezone=True)),
sa.UniqueConstraint("idempotency_key", name="uq_migration_cutover_idempotency"),
)
op.create_index(
"ix_migration_cutover_operations_migration_plan_id",
"migration_cutover_operations",
["migration_plan_id"],
)
op.create_index(
"ix_migration_cutover_operations_stage", "migration_cutover_operations", ["stage"]
)
op.create_table(
"migration_events",
_id(),
sa.Column(
"migration_plan_id",
sa.Uuid(),
sa.ForeignKey("migration_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"operation_id",
sa.Uuid(),
sa.ForeignKey("migration_cutover_operations.id", ondelete="RESTRICT"),
),
sa.Column("event_type", sa.String(64), nullable=False),
sa.Column("before_state", sa.String(48)),
sa.Column("after_state", sa.String(48)),
sa.Column("actor", sa.String(255), nullable=False),
sa.Column("policy_revision", sa.String(128), nullable=False),
sa.Column("evidence_refs", sa.JSON(), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("source_identity", sa.JSON(), nullable=False),
sa.Column("target_identity", sa.JSON(), nullable=False),
sa.Column("generation", sa.BigInteger(), nullable=False),
sa.Column("change_id", sa.String(128), nullable=False),
sa.Column("details", sa.JSON(), nullable=False),
sa.Column("occurred_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
for column in ("migration_plan_id", "operation_id", "event_type", "change_id", "occurred_at"):
op.create_index(f"ix_migration_events_{column}", "migration_events", [column])
def downgrade() -> None:
op.drop_table("migration_events")
op.drop_table("migration_cutover_operations")
op.drop_table("migration_rollback_snapshots")
op.drop_table("migration_shadow_sessions")
op.drop_table("migration_validation_snapshots")
op.drop_table("migration_batch_checkpoints")
op.drop_table("migration_plans")
op.drop_table("migration_validation_policy_revisions")
@@ -0,0 +1,257 @@
"""Add operational SLO, alert and bounded capacity history records.
Revision ID: 20260827_0020
Revises: 20260826_0019
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260827_0020"
down_revision: str | None = "20260826_0019"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _id() -> sa.Column[object]:
return sa.Column("id", sa.Uuid(), primary_key=True, nullable=False)
def _created() -> sa.Column[object]:
return sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now())
def upgrade() -> None:
op.create_table(
"service_level_indicators",
_id(),
sa.Column("key", sa.String(128), nullable=False, unique=True),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("service", sa.String(128), nullable=False),
sa.Column("capability", sa.String(128)),
sa.Column("measurement", sa.String(32), nullable=False),
sa.Column("valid_population", sa.JSON(), nullable=False),
sa.Column("success_condition", sa.JSON(), nullable=False),
sa.Column("default_window_seconds", sa.Integer(), nullable=False),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.true()),
_created(),
)
for column in ("key", "service", "capability", "enabled"):
op.create_index(f"ix_service_level_indicators_{column}", "service_level_indicators", [column])
op.create_table(
"slo_policy_revisions",
_id(),
sa.Column("key", sa.String(128), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("sli_definition_id", sa.Uuid(), sa.ForeignKey("service_level_indicators.id", ondelete="RESTRICT"), nullable=False),
sa.Column("objective", sa.Float(), nullable=False),
sa.Column("threshold_ms", sa.Float()),
sa.Column("rolling_window_seconds", sa.Integer(), nullable=False),
sa.Column("minimum_sample_count", sa.Integer(), nullable=False),
sa.Column("severity", sa.String(16), nullable=False),
sa.Column("environment", sa.String(16), nullable=False),
sa.Column("rationale", sa.Text(), nullable=False),
sa.Column("effective_from", sa.DateTime(timezone=True), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("fingerprint", sa.String(64), nullable=False, unique=True),
sa.Column("created_by", sa.String(255), nullable=False),
_created(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("key", "revision", name="uq_slo_policy_key_revision"),
)
for column in ("key", "sli_definition_id", "environment", "active"):
op.create_index(f"ix_slo_policy_revisions_{column}", "slo_policy_revisions", [column])
op.create_table(
"slo_evaluations",
_id(),
sa.Column("policy_id", sa.Uuid(), sa.ForeignKey("slo_policy_revisions.id", ondelete="RESTRICT"), nullable=False),
sa.Column("window_start", sa.DateTime(timezone=True), nullable=False),
sa.Column("window_end", sa.DateTime(timezone=True), nullable=False),
sa.Column("observed_value", sa.Float()),
sa.Column("sample_count", sa.Integer(), nullable=False),
sa.Column("good_count", sa.Integer(), nullable=False),
sa.Column("bad_count", sa.Integer(), nullable=False),
sa.Column("state", sa.String(32), nullable=False),
sa.Column("freshness_seconds", sa.Float()),
sa.Column("allowed_bad", sa.Float()),
sa.Column("consumed_bad", sa.Integer()),
sa.Column("remaining_bad", sa.Float()),
sa.Column("short_burn_rate", sa.Float()),
sa.Column("long_burn_rate", sa.Float()),
sa.Column("evidence", sa.JSON(), nullable=False),
sa.Column("observed_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("policy_id", "window_end", name="uq_slo_evaluation_window"),
)
for column in ("policy_id", "window_start", "window_end", "state"):
op.create_index(f"ix_slo_evaluations_{column}", "slo_evaluations", [column])
op.create_table(
"alert_rule_revisions",
_id(),
sa.Column("key", sa.String(128), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("alert_type", sa.String(64), nullable=False),
sa.Column("signal", sa.String(128), nullable=False),
sa.Column("slo_policy_id", sa.Uuid(), sa.ForeignKey("slo_policy_revisions.id", ondelete="RESTRICT")),
sa.Column("condition", sa.JSON(), nullable=False),
sa.Column("pending_seconds", sa.Integer(), nullable=False),
sa.Column("severity", sa.String(16), nullable=False),
sa.Column("labels", sa.JSON(), nullable=False),
sa.Column("cooldown_seconds", sa.Integer(), nullable=False),
sa.Column("recovery_condition", sa.JSON(), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("fingerprint", sa.String(64), nullable=False, unique=True),
sa.Column("created_by", sa.String(255), nullable=False),
_created(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("key", "revision", name="uq_alert_rule_key_revision"),
)
for column in ("key", "alert_type", "slo_policy_id", "severity", "active"):
op.create_index(f"ix_alert_rule_revisions_{column}", "alert_rule_revisions", [column])
op.create_table(
"maintenance_windows",
_id(),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("starts_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("ends_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("matcher", sa.JSON(), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("created_by", sa.String(255), nullable=False),
_created(),
)
for column in ("starts_at", "ends_at", "active"):
op.create_index(f"ix_maintenance_windows_{column}", "maintenance_windows", [column])
op.create_table(
"operational_incidents",
_id(),
sa.Column("fingerprint", sa.String(64), nullable=False, unique=True),
sa.Column("title", sa.String(255), nullable=False),
sa.Column("state", sa.String(32), nullable=False),
sa.Column("severity", sa.String(16), nullable=False),
sa.Column("root_subject_type", sa.String(64), nullable=False),
sa.Column("root_subject_ref", sa.String(255), nullable=False),
sa.Column("correlation", sa.String(32), nullable=False),
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("resolved_at", sa.DateTime(timezone=True)),
)
op.create_index("ix_operational_incidents_state", "operational_incidents", ["state"])
op.create_table(
"operational_alerts",
_id(),
sa.Column("rule_id", sa.Uuid(), sa.ForeignKey("alert_rule_revisions.id", ondelete="RESTRICT"), nullable=False),
sa.Column("fingerprint", sa.String(64), nullable=False, unique=True),
sa.Column("alert_type", sa.String(64), nullable=False),
sa.Column("severity", sa.String(16), nullable=False),
sa.Column("state", sa.String(32), nullable=False),
sa.Column("source", sa.String(128), nullable=False),
sa.Column("subject_type", sa.String(64), nullable=False),
sa.Column("subject_ref", sa.String(255), nullable=False),
sa.Column("summary", sa.Text(), nullable=False),
sa.Column("details", sa.JSON(), nullable=False),
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("firing_at", sa.DateTime(timezone=True)),
sa.Column("acknowledged_at", sa.DateTime(timezone=True)),
sa.Column("acknowledged_by", sa.String(255)),
sa.Column("acknowledgement_reason", sa.Text()),
sa.Column("resolved_at", sa.DateTime(timezone=True)),
sa.Column("suppressed_until", sa.DateTime(timezone=True)),
sa.Column("occurrence_count", sa.Integer(), nullable=False, server_default="1"),
sa.Column("cooldown_until", sa.DateTime(timezone=True)),
sa.Column("incident_id", sa.Uuid(), sa.ForeignKey("operational_incidents.id", ondelete="SET NULL")),
)
for column in ("rule_id", "alert_type", "severity", "state", "subject_ref", "first_seen_at", "last_seen_at", "incident_id"):
op.create_index(f"ix_operational_alerts_{column}", "operational_alerts", [column])
op.create_table(
"alert_history_events",
_id(),
sa.Column("alert_id", sa.Uuid(), sa.ForeignKey("operational_alerts.id", ondelete="RESTRICT"), nullable=False),
sa.Column("from_state", sa.String(32)),
sa.Column("to_state", sa.String(32), nullable=False),
sa.Column("actor", sa.String(255), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("evidence", sa.JSON(), nullable=False),
sa.Column("occurred_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
for column in ("alert_id", "to_state", "occurred_at"):
op.create_index(f"ix_alert_history_events_{column}", "alert_history_events", [column])
op.create_table(
"incident_timeline_events",
_id(),
sa.Column("incident_id", sa.Uuid(), sa.ForeignKey("operational_incidents.id", ondelete="RESTRICT"), nullable=False),
sa.Column("alert_id", sa.Uuid(), sa.ForeignKey("operational_alerts.id", ondelete="SET NULL")),
sa.Column("event_type", sa.String(64), nullable=False),
sa.Column("relation", sa.String(32), nullable=False),
sa.Column("summary", sa.Text(), nullable=False),
sa.Column("occurred_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
for column in ("incident_id", "alert_id", "occurred_at"):
op.create_index(f"ix_incident_timeline_events_{column}", "incident_timeline_events", [column])
op.create_table(
"capacity_snapshots",
_id(),
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("received_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("node_id", sa.Uuid(), sa.ForeignKey("compute_nodes.id", ondelete="RESTRICT"), nullable=False),
sa.Column("node_name", sa.String(255), nullable=False),
sa.Column("accelerator_id", sa.Uuid(), sa.ForeignKey("accelerators.id", ondelete="RESTRICT")),
sa.Column("gpu_total_bytes", sa.BigInteger()),
sa.Column("gpu_observed_bytes", sa.BigInteger()),
sa.Column("gpu_external_bytes", sa.BigInteger()),
sa.Column("gpu_managed_resident_bytes", sa.BigInteger()),
sa.Column("gpu_leased_bytes", sa.BigInteger()),
sa.Column("gpu_reserve_bytes", sa.BigInteger()),
sa.Column("gpu_schedulable_bytes", sa.BigInteger()),
sa.Column("pressure_state", sa.String(16), nullable=False),
sa.Column("system_ram_total_bytes", sa.BigInteger()),
sa.Column("system_ram_available_bytes", sa.BigInteger()),
sa.Column("storage_total_bytes", sa.BigInteger()),
sa.Column("storage_free_bytes", sa.BigInteger()),
sa.Column("availability", sa.String(32), nullable=False),
sa.Column("freshness_seconds", sa.Float(), nullable=False),
sa.UniqueConstraint("node_id", "accelerator_id", "observed_at", name="uq_capacity_observation"),
)
for column in ("observed_at", "received_at", "node_id", "accelerator_id"):
op.create_index(f"ix_capacity_snapshots_{column}", "capacity_snapshots", [column])
op.create_table(
"capacity_aggregates",
_id(),
sa.Column("node_id", sa.Uuid(), sa.ForeignKey("compute_nodes.id", ondelete="RESTRICT"), nullable=False),
sa.Column("accelerator_id", sa.Uuid(), sa.ForeignKey("accelerators.id", ondelete="RESTRICT")),
sa.Column("bucket_start", sa.DateTime(timezone=True), nullable=False),
sa.Column("bucket_seconds", sa.Integer(), nullable=False),
sa.Column("sample_count", sa.Integer(), nullable=False),
sa.Column("metrics", sa.JSON(), nullable=False),
_created(),
sa.UniqueConstraint("node_id", "accelerator_id", "bucket_start", name="uq_capacity_bucket"),
)
for column in ("node_id", "accelerator_id", "bucket_start"):
op.create_index(f"ix_capacity_aggregates_{column}", "capacity_aggregates", [column])
def downgrade() -> None:
op.drop_table("capacity_aggregates")
op.drop_table("capacity_snapshots")
op.drop_table("incident_timeline_events")
op.drop_table("alert_history_events")
op.drop_table("operational_alerts")
op.drop_table("operational_incidents")
op.drop_table("maintenance_windows")
op.drop_table("alert_rule_revisions")
op.drop_table("slo_evaluations")
op.drop_table("slo_policy_revisions")
op.drop_table("service_level_indicators")
@@ -0,0 +1,333 @@
"""Add versioned recovery policies, immutable backup sets, restore journals and artifact recovery.
Revision ID: 20260827_0021
Revises: 20260827_0020
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260827_0021"
down_revision: str | None = "20260827_0020"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _id() -> sa.Column[object]:
return sa.Column("id", sa.Uuid(), primary_key=True, nullable=False)
def _created() -> sa.Column[object]:
return sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now())
def upgrade() -> None:
op.create_table(
"recovery_policy_revisions",
_id(),
sa.Column("key", sa.String(128), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("asset_class", sa.String(32), nullable=False),
sa.Column("backup_method", sa.String(48), nullable=False),
sa.Column("retention_days", sa.Integer(), nullable=False),
sa.Column("minimum_verified_backups", sa.Integer(), nullable=False),
sa.Column("rpo_seconds", sa.Integer()),
sa.Column("rto_target_seconds", sa.Integer()),
sa.Column("restore_verification", sa.String(32), nullable=False),
sa.Column("encryption_required", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("external_dependency", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("rehydration_allowed", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("secret_class", sa.String(32)),
sa.Column("rationale", sa.Text(), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("fingerprint", sa.String(64), nullable=False, unique=True),
sa.Column("created_by", sa.String(255), nullable=False),
_created(),
sa.Column("immutable_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("key", "revision", name="uq_recovery_policy_key_revision"),
sa.CheckConstraint("retention_days >= 1", name="ck_recovery_policy_retention"),
sa.CheckConstraint(
"minimum_verified_backups >= 1", name="ck_recovery_policy_minimum_verified"
),
)
for column in ("key", "asset_class", "active"):
op.create_index(
f"ix_recovery_policy_revisions_{column}", "recovery_policy_revisions", [column]
)
op.create_table(
"recovery_asset_records",
_id(),
sa.Column("key", sa.String(128), nullable=False, unique=True),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("asset_class", sa.String(32), nullable=False),
sa.Column("owner", sa.String(128), nullable=False),
sa.Column("location", sa.Text(), nullable=False),
sa.Column("backup_method", sa.String(48), nullable=False),
sa.Column("restore_method", sa.Text(), nullable=False),
sa.Column("rebuild_method", sa.Text()),
sa.Column("rpo_seconds", sa.Integer()),
sa.Column("readiness", sa.String(32), nullable=False),
sa.Column("dependencies", sa.JSON(), nullable=False),
sa.Column("notes", sa.Text(), nullable=False, server_default=""),
sa.Column(
"policy_revision_id",
sa.Uuid(),
sa.ForeignKey("recovery_policy_revisions.id", ondelete="RESTRICT"),
nullable=False,
),
_created(),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
for column in ("key", "asset_class", "readiness", "policy_revision_id"):
op.create_index(f"ix_recovery_asset_records_{column}", "recovery_asset_records", [column])
op.create_table(
"backup_sets",
_id(),
sa.Column("backup_id", sa.String(64), nullable=False),
sa.Column("state", sa.String(32), nullable=False, server_default="PLANNED"),
sa.Column(
"policy_revision_id",
sa.Uuid(),
sa.ForeignKey("recovery_policy_revisions.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("modelforge_version", sa.String(64), nullable=False),
sa.Column("modelforge_commit", sa.String(64)),
sa.Column("source_repository", sa.Text()),
sa.Column("source_reference", sa.String(255)),
sa.Column("schema_revision", sa.String(64)),
sa.Column("environment_fingerprint", sa.JSON(), nullable=False),
sa.Column("database_identity", sa.JSON(), nullable=False),
sa.Column("destination_root", sa.Text(), nullable=False),
sa.Column("payload_relative_path", sa.Text()),
sa.Column("payload_sha256", sa.String(64)),
sa.Column("manifest_relative_path", sa.Text()),
sa.Column("manifest_sha256", sa.String(64)),
sa.Column("included_asset_classes", sa.JSON(), nullable=False),
sa.Column("excluded_asset_classes", sa.JSON(), nullable=False),
sa.Column("payload_bytes", sa.BigInteger(), nullable=False, server_default="0"),
sa.Column("encrypted", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("encryption_algorithm", sa.String(64)),
sa.Column("encryption_key_id", sa.String(128)),
sa.Column("verification_details", sa.JSON(), nullable=False),
sa.Column("verified_at", sa.DateTime(timezone=True)),
sa.Column("failure_code", sa.String(64)),
sa.Column("failure_reason", sa.Text()),
sa.Column("milestone", sa.String(64)),
sa.Column("legal_hold", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("created_by", sa.String(255), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True)),
sa.Column("completed_at", sa.DateTime(timezone=True)),
sa.Column("expires_at", sa.DateTime(timezone=True)),
_created(),
sa.Column("immutable_at", sa.DateTime(timezone=True)),
sa.UniqueConstraint("backup_id", name="uq_backup_set_backup_id"),
sa.CheckConstraint("payload_bytes >= 0", name="ck_backup_set_payload_bytes"),
)
for column in (
"backup_id",
"state",
"policy_revision_id",
"schema_revision",
"manifest_sha256",
"verified_at",
"milestone",
"legal_hold",
"expires_at",
):
op.create_index(f"ix_backup_sets_{column}", "backup_sets", [column])
op.create_table(
"backup_manifest_entries",
_id(),
sa.Column(
"backup_set_id",
sa.Uuid(),
sa.ForeignKey("backup_sets.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("logical_asset_type", sa.String(64), nullable=False),
sa.Column("object_name", sa.String(255), nullable=False),
sa.Column("relative_path", sa.Text(), nullable=False),
sa.Column("size_bytes", sa.BigInteger(), nullable=False),
sa.Column("sha256", sa.String(64), nullable=False),
sa.Column("source_generation", sa.String(128), nullable=False),
sa.Column("schema_version", sa.String(64)),
sa.Column("dependency_refs", sa.JSON(), nullable=False),
_created(),
sa.UniqueConstraint("backup_set_id", "object_name", name="uq_backup_manifest_object"),
sa.CheckConstraint("length(sha256) = 64", name="ck_backup_manifest_sha256_length"),
sa.CheckConstraint("size_bytes >= 0", name="ck_backup_manifest_size"),
)
for column in ("backup_set_id", "logical_asset_type"):
op.create_index(
f"ix_backup_manifest_entries_{column}", "backup_manifest_entries", [column]
)
op.create_table(
"restore_plans",
_id(),
sa.Column(
"backup_set_id",
sa.Uuid(),
sa.ForeignKey("backup_sets.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("mode", sa.String(32), nullable=False),
sa.Column("state", sa.String(32), nullable=False, server_default="DRAFT"),
sa.Column("target_environment", sa.String(32), nullable=False),
sa.Column("target_label", sa.String(128), nullable=False),
sa.Column("database_destination", sa.Text(), nullable=False),
sa.Column("artifact_strategy", sa.String(32), nullable=False),
sa.Column("secret_strategy", sa.String(32), nullable=False),
sa.Column("node_strategy", sa.String(32), nullable=False),
sa.Column("expected_modelforge_version", sa.String(64)),
sa.Column("preflight", sa.JSON(), nullable=False),
sa.Column("validation_requirements", sa.JSON(), nullable=False),
sa.Column("fingerprint", sa.String(64), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("created_by", sa.String(255), nullable=False),
_created(),
sa.UniqueConstraint("fingerprint", name="uq_restore_plan_fingerprint"),
)
for column in ("backup_set_id", "mode", "state", "target_environment"):
op.create_index(f"ix_restore_plans_{column}", "restore_plans", [column])
op.create_table(
"restore_operations",
_id(),
sa.Column(
"plan_id",
sa.Uuid(),
sa.ForeignKey("restore_plans.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"backup_set_id",
sa.Uuid(),
sa.ForeignKey("backup_sets.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("state", sa.String(48), nullable=False, server_default="PLANNED"),
sa.Column("attempt", sa.Integer(), nullable=False, server_default="1"),
sa.Column("idempotency_key", sa.String(64), nullable=False),
sa.Column("preflight_result", sa.JSON(), nullable=False),
sa.Column("phase_durations", sa.JSON(), nullable=False),
sa.Column("source_fingerprint", sa.JSON(), nullable=False),
sa.Column("restored_fingerprint", sa.JSON(), nullable=False),
sa.Column("fingerprint_diff", sa.JSON(), nullable=False),
sa.Column("validation_result", sa.JSON(), nullable=False),
sa.Column("rpo_seconds", sa.Float()),
sa.Column("rto_seconds", sa.Float()),
sa.Column("failure_code", sa.String(64)),
sa.Column("failure_reason", sa.Text()),
sa.Column("started_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("ready_at", sa.DateTime(timezone=True)),
sa.UniqueConstraint("idempotency_key", name="uq_restore_operation_idempotency"),
)
for column in ("plan_id", "backup_set_id", "state"):
op.create_index(f"ix_restore_operations_{column}", "restore_operations", [column])
op.create_table(
"restore_operation_events",
_id(),
sa.Column(
"restore_operation_id",
sa.Uuid(),
sa.ForeignKey("restore_operations.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("from_state", sa.String(48)),
sa.Column("to_state", sa.String(48), nullable=False),
sa.Column("phase", sa.String(48), nullable=False),
sa.Column("actor", sa.String(255), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("evidence", sa.JSON(), nullable=False),
sa.Column("occurred_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
for column in ("restore_operation_id", "to_state", "occurred_at"):
op.create_index(
f"ix_restore_operation_events_{column}", "restore_operation_events", [column]
)
op.create_table(
"artifact_recovery_operations",
_id(),
sa.Column(
"restore_operation_id",
sa.Uuid(),
sa.ForeignKey("restore_operations.id", ondelete="SET NULL"),
),
sa.Column(
"artifact_set_id",
sa.Uuid(),
sa.ForeignKey("artifact_sets.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"model_revision_id",
sa.Uuid(),
sa.ForeignKey("model_revisions.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("recovery_class", sa.String(32), nullable=False),
sa.Column("state", sa.String(32), nullable=False, server_default="PLANNED"),
sa.Column("upstream_repository", sa.Text()),
sa.Column("upstream_commit_sha", sa.String(64)),
sa.Column(
"target_storage_root_id",
sa.Uuid(),
sa.ForeignKey("storage_roots.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("expected_files", sa.JSON(), nullable=False),
sa.Column("verified_files", sa.JSON(), nullable=False),
sa.Column("bytes_total", sa.BigInteger(), nullable=False, server_default="0"),
sa.Column("bytes_recovered", sa.BigInteger(), nullable=False, server_default="0"),
sa.Column(
"download_plan_id", sa.Uuid(), sa.ForeignKey("download_plans.id", ondelete="SET NULL")
),
sa.Column(
"artifact_job_id", sa.Uuid(), sa.ForeignKey("artifact_jobs.id", ondelete="SET NULL")
),
sa.Column("lineage", sa.JSON(), nullable=False),
sa.Column("duration_seconds", sa.Float()),
sa.Column("failure_code", sa.String(64)),
sa.Column("failure_reason", sa.Text()),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("created_by", sa.String(255), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("completed_at", sa.DateTime(timezone=True)),
)
for column in (
"restore_operation_id",
"artifact_set_id",
"model_revision_id",
"recovery_class",
"state",
"target_storage_root_id",
"download_plan_id",
"artifact_job_id",
):
op.create_index(
f"ix_artifact_recovery_operations_{column}", "artifact_recovery_operations", [column]
)
def downgrade() -> None:
op.drop_table("artifact_recovery_operations")
op.drop_table("restore_operation_events")
op.drop_table("restore_operations")
op.drop_table("restore_plans")
op.drop_table("backup_manifest_entries")
op.drop_table("backup_sets")
op.drop_table("recovery_asset_records")
op.drop_table("recovery_policy_revisions")
@@ -0,0 +1,71 @@
"""Add audited compute-node decommission tombstones and operation records.
Revision ID: 20260828_0022
Revises: 20260827_0021
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260828_0022"
down_revision: str | None = "20260827_0021"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.add_column(
"compute_nodes",
sa.Column("generation", sa.BigInteger(), nullable=False, server_default="1"),
)
op.add_column("compute_nodes", sa.Column("decommissioned_at", sa.DateTime(timezone=True)))
op.add_column("compute_nodes", sa.Column("decommission_reason", sa.Text()))
op.add_column("compute_nodes", sa.Column("decommissioned_by", sa.String(255)))
op.create_index("ix_compute_nodes_decommissioned_at", "compute_nodes", ["decommissioned_at"])
op.create_table(
"node_decommission_operations",
sa.Column("id", sa.Uuid(), primary_key=True, nullable=False),
sa.Column(
"compute_node_id",
sa.Uuid(),
sa.ForeignKey("compute_nodes.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column("persisted_identity", sa.String(128), nullable=False),
sa.Column("idempotency_key", sa.String(128), nullable=False),
sa.Column("expected_generation", sa.BigInteger(), nullable=False),
sa.Column("preview_digest", sa.String(64), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("operator", sa.String(255), nullable=False),
sa.Column("previous_state", sa.JSON(), nullable=False),
sa.Column("cleanup_summary", sa.JSON(), nullable=False),
sa.Column("status", sa.String(32), nullable=False, server_default="completed"),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
sa.Column("decommissioned_at", sa.DateTime(timezone=True), nullable=False),
sa.UniqueConstraint("compute_node_id", name="uq_node_decommission_node"),
sa.UniqueConstraint("idempotency_key", name="uq_node_decommission_idempotency"),
sa.CheckConstraint("expected_generation >= 1", name="ck_node_decommission_generation"),
)
for column in ("compute_node_id", "persisted_identity", "status", "decommissioned_at"):
op.create_index(
f"ix_node_decommission_operations_{column}",
"node_decommission_operations",
[column],
)
def downgrade() -> None:
op.drop_table("node_decommission_operations")
op.drop_index("ix_compute_nodes_decommissioned_at", table_name="compute_nodes")
op.drop_column("compute_nodes", "decommissioned_by")
op.drop_column("compute_nodes", "decommission_reason")
op.drop_column("compute_nodes", "decommissioned_at")
op.drop_column("compute_nodes", "generation")
@@ -0,0 +1,56 @@
"""Enforce the two fixed Node Agent credential scopes.
Revision ID: 20260830_0023
Revises: 20260828_0022
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260830_0023"
down_revision: str | None = "20260828_0022"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
_SCOPE_CONSTRAINTS = (
("node_enrollments", "node.enroll", "ck_node_enrollment_scope"),
("node_credentials", "node.publish", "ck_node_credential_scope"),
)
def _validate_existing_scopes() -> None:
connection = op.get_bind()
for table_name, expected_scope, _constraint_name in _SCOPE_CONSTRAINTS:
table = sa.table(table_name, sa.column("scope", sa.String(64)))
invalid_rows = connection.scalar(
sa.select(sa.func.count())
.select_from(table)
.where(sa.or_(table.c.scope.is_(None), table.c.scope != expected_scope))
)
if invalid_rows:
raise RuntimeError(
f"refusing node-scope migration: {table_name} contains "
f"{invalid_rows} row(s) outside {expected_scope!r}"
)
def upgrade() -> None:
# Validate every table before changing either one. A malformed production row therefore aborts
# the migration without leaving a partially hardened schema.
_validate_existing_scopes()
for table_name, expected_scope, constraint_name in _SCOPE_CONSTRAINTS:
with op.batch_alter_table(table_name) as batch_op:
batch_op.create_check_constraint(
constraint_name,
f"scope = '{expected_scope}'",
)
def downgrade() -> None:
for table_name, _expected_scope, constraint_name in reversed(_SCOPE_CONSTRAINTS):
with op.batch_alter_table(table_name) as batch_op:
batch_op.drop_constraint(constraint_name, type_="check")
@@ -0,0 +1,717 @@
"""Version and checkpoint the tamper-evident audit chain.
Revision ID: 20260830_0024
Revises: 20260830_0023
"""
from __future__ import annotations
import hashlib
import json
import re
import uuid
from datetime import UTC, datetime
from typing import Any
import sqlalchemy as sa
from sqlalchemy.engine import Connection
from alembic import op
revision = "20260830_0024"
down_revision = "20260830_0023"
branch_labels = None
depends_on = None
_LEGACY_PREFIX_DOMAIN = b"modelforge:audit:legacy-prefix:v1\n"
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
_AUDIT_CHAIN_LOCK_KEY = int.from_bytes(b"MF_AUDIT", byteorder="big", signed=False)
_AUDIT_OWNER_ROLE = "modelforge"
_AUDIT_RUNTIME_ROLE = "modelforge_runtime"
_POSTGRES_AUDIT_BOUNDARY_SQL = r"""
create schema if not exists modelforge_audit authorization modelforge;
alter schema modelforge_audit owner to modelforge;
revoke all on schema modelforge_audit from public;
revoke create on schema public from modelforge_runtime;
grant usage on schema public, modelforge_audit to modelforge_runtime;
create or replace function modelforge_audit.enforce_owner_mutation()
returns trigger
language plpgsql
security invoker
set search_path = pg_catalog
as $guard$
begin
if current_user <> 'modelforge' then
raise exception 'audit tables are writable only through the canonical append function'
using errcode = '42501';
end if;
if tg_op = 'DELETE' then
return old;
elsif tg_op = 'TRUNCATE' then
return null;
end if;
return new;
end
$guard$;
alter function modelforge_audit.enforce_owner_mutation() owner to modelforge;
revoke all on function modelforge_audit.enforce_owner_mutation() from public;
revoke all on function modelforge_audit.enforce_owner_mutation() from modelforge_runtime;
drop trigger if exists trg_modelforge_audit_events_owner on public.audit_events;
create trigger trg_modelforge_audit_events_owner
before insert or update or delete on public.audit_events
for each row execute function modelforge_audit.enforce_owner_mutation();
drop trigger if exists trg_modelforge_audit_events_truncate_owner on public.audit_events;
create trigger trg_modelforge_audit_events_truncate_owner
before truncate on public.audit_events
for each statement execute function modelforge_audit.enforce_owner_mutation();
drop trigger if exists trg_modelforge_audit_head_owner on public.audit_chain_heads;
create trigger trg_modelforge_audit_head_owner
before insert or update or delete on public.audit_chain_heads
for each row execute function modelforge_audit.enforce_owner_mutation();
drop trigger if exists trg_modelforge_audit_head_truncate_owner on public.audit_chain_heads;
create trigger trg_modelforge_audit_head_truncate_owner
before truncate on public.audit_chain_heads
for each statement execute function modelforge_audit.enforce_owner_mutation();
create or replace function modelforge_audit.append_event_v2(
p_event_id uuid,
p_occurred_at timestamptz,
p_correlation_id text,
p_actor_type text,
p_actor_id text,
p_action text,
p_resource_type text,
p_resource_id text,
p_outcome text,
p_details jsonb,
p_expected_event_count bigint,
p_expected_last_sequence bigint,
p_expected_last_event_hash text,
p_expected_hash_format text,
p_expected_v2_start_sequence bigint,
p_expected_legacy_prefix_count bigint,
p_expected_legacy_prefix_seal text
)
returns table(event_id uuid, sequence bigint, event_hash text, occurred_at timestamptz)
language plpgsql
security definer
set search_path = pg_catalog
as $append$
declare
v_head public.audit_chain_heads%rowtype;
v_tail public.audit_events%rowtype;
v_max_sequence bigint;
v_predecessor_hash text;
v_sequence bigint;
v_payload text;
v_hash text;
v_updated bigint;
begin
perform pg_catalog.pg_advisory_xact_lock(5568242723498248532);
if p_event_id is null or p_occurred_at is null or p_correlation_id is null
or p_actor_type is null or p_actor_id is null or p_action is null
or p_resource_type is null or p_outcome is null or p_details is null then
raise exception 'canonical audit append arguments must not be null'
using errcode = '23502';
end if;
if not pg_catalog.isfinite(p_occurred_at) then
raise exception 'canonical audit occurred_at must be finite' using errcode = '22008';
end if;
if pg_catalog.jsonb_typeof(p_details) <> 'object' then
raise exception 'canonical audit details must be a JSON object'
using errcode = '22023';
end if;
select head.* into v_head
from public.audit_chain_heads as head
where head.singleton_id = 1
for update;
if not found then
raise exception 'audit checkpoint is missing' using errcode = '23514';
end if;
if v_head.event_count is distinct from p_expected_event_count
or v_head.last_sequence is distinct from p_expected_last_sequence
or v_head.last_event_hash is distinct from p_expected_last_event_hash
or v_head.hash_format is distinct from p_expected_hash_format
or v_head.v2_start_sequence is distinct from p_expected_v2_start_sequence
or v_head.legacy_prefix_count is distinct from p_expected_legacy_prefix_count
or v_head.legacy_prefix_seal is distinct from p_expected_legacy_prefix_seal then
raise exception 'audit checkpoint changed before canonical append'
using errcode = '40001';
end if;
if v_head.hash_format <> 'v2'
or v_head.event_count <> v_head.last_sequence
or v_head.event_count < 0
or v_head.v2_start_sequence < 1
or v_head.legacy_prefix_count <> v_head.v2_start_sequence - 1
or v_head.legacy_prefix_count > v_head.event_count
or v_head.legacy_prefix_seal !~ '^[0-9a-f]{64}$' then
raise exception 'audit checkpoint invariants are invalid' using errcode = '23514';
end if;
select events.sequence into v_max_sequence
from public.audit_events as events
order by events.sequence desc, events.id desc
limit 1;
if v_head.event_count = 0 then
if v_max_sequence is not null or v_head.last_event_hash is not null then
raise exception 'empty audit checkpoint has retained events' using errcode = '23514';
end if;
else
if v_max_sequence is distinct from v_head.last_sequence
or v_head.last_event_hash is null then
raise exception 'audit checkpoint does not identify the retained tail'
using errcode = '23514';
end if;
select events.* into v_tail
from public.audit_events as events
where events.sequence = v_head.last_sequence;
if not found or v_tail.event_hash is distinct from v_head.last_event_hash
or v_tail.event_hash !~ '^[0-9a-f]{64}$' then
raise exception 'audit retained tail is missing or does not match the checkpoint'
using errcode = '23514';
end if;
if v_tail.hash_format = 'v2' then
if v_tail.canonical_payload is null
or pg_catalog.encode(
pg_catalog.sha256(pg_catalog.convert_to(v_tail.canonical_payload, 'UTF8')),
'hex'
) <> v_tail.event_hash
or v_tail.canonical_payload::jsonb is distinct from pg_catalog.jsonb_build_object(
'hash_format', 'v2',
'id', v_tail.id::text,
'occurred_at', pg_catalog.to_char(
v_tail.occurred_at at time zone 'UTC',
'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
),
'correlation_id', v_tail.correlation_id,
'actor_type', v_tail.actor_type,
'actor_id', v_tail.actor_id,
'action', v_tail.action,
'resource_type', v_tail.resource_type,
'resource_id', v_tail.resource_id,
'outcome', v_tail.outcome,
'details', v_tail.details::jsonb,
'previous_event_hash', v_tail.previous_event_hash
) then
raise exception 'v2 audit retained tail payload is invalid'
using errcode = '23514';
end if;
elsif v_tail.hash_format <> 'v1'
or v_tail.sequence <> v_head.v2_start_sequence - 1 then
raise exception 'audit retained tail hash format is invalid' using errcode = '23514';
end if;
if v_tail.sequence = 1 then
if v_tail.previous_event_hash is not null then
raise exception 'first audit event has a previous hash' using errcode = '23514';
end if;
else
select events.event_hash into v_predecessor_hash
from public.audit_events as events
where events.sequence = v_tail.sequence - 1;
if not found or v_tail.previous_event_hash is distinct from v_predecessor_hash then
raise exception 'audit retained tail link is invalid' using errcode = '23514';
end if;
end if;
end if;
v_sequence := v_head.last_sequence + 1;
v_payload := pg_catalog.jsonb_build_object(
'hash_format', 'v2',
'id', p_event_id::text,
'occurred_at', pg_catalog.to_char(
p_occurred_at at time zone 'UTC',
'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
),
'correlation_id', p_correlation_id,
'actor_type', p_actor_type,
'actor_id', p_actor_id,
'action', p_action,
'resource_type', p_resource_type,
'resource_id', p_resource_id,
'outcome', p_outcome,
'details', p_details,
'previous_event_hash', v_head.last_event_hash
)::text;
v_hash := pg_catalog.encode(
pg_catalog.sha256(pg_catalog.convert_to(v_payload, 'UTF8')), 'hex'
);
insert into public.audit_events (
id, sequence, occurred_at, correlation_id, actor_type, actor_id, action,
resource_type, resource_id, outcome, details, previous_event_hash, event_hash,
hash_format, canonical_payload
) values (
p_event_id, v_sequence, p_occurred_at, p_correlation_id, p_actor_type, p_actor_id,
p_action, p_resource_type, p_resource_id, p_outcome, p_details,
v_head.last_event_hash, v_hash, 'v2', v_payload
);
update public.audit_chain_heads as head
set event_count = v_head.event_count + 1,
last_sequence = v_sequence,
last_event_hash = v_hash,
updated_at = p_occurred_at
where head.singleton_id = 1
and head.event_count = v_head.event_count
and head.last_sequence = v_head.last_sequence
and head.last_event_hash is not distinct from v_head.last_event_hash
and head.hash_format = v_head.hash_format
and head.v2_start_sequence = v_head.v2_start_sequence
and head.legacy_prefix_count = v_head.legacy_prefix_count
and head.legacy_prefix_seal = v_head.legacy_prefix_seal;
get diagnostics v_updated = row_count;
if v_updated <> 1 then
raise exception 'audit checkpoint compare-and-set failed' using errcode = '40001';
end if;
return query select p_event_id, v_sequence, v_hash, p_occurred_at;
end
$append$;
alter function modelforge_audit.append_event_v2(
uuid, timestamptz, text, text, text, text, text, text, text, jsonb,
bigint, bigint, text, text, bigint, bigint, text
) owner to modelforge;
revoke all on function modelforge_audit.append_event_v2(
uuid, timestamptz, text, text, text, text, text, text, text, jsonb,
bigint, bigint, text, text, bigint, bigint, text
) from public;
grant execute on function modelforge_audit.append_event_v2(
uuid, timestamptz, text, text, text, text, text, text, text, jsonb,
bigint, bigint, text, text, bigint, bigint, text
) to modelforge_runtime;
grant select, insert, update, delete on all tables in schema public to modelforge_runtime;
grant usage, select on all sequences in schema public to modelforge_runtime;
revoke execute on all functions in schema public from public, modelforge_runtime;
revoke insert, update, delete, truncate, references, trigger
on public.audit_events, public.audit_chain_heads from modelforge_runtime;
grant select on public.audit_events, public.audit_chain_heads to modelforge_runtime;
alter default privileges for role modelforge in schema public
grant select, insert, update, delete on tables to modelforge_runtime;
alter default privileges for role modelforge in schema public
grant usage, select on sequences to modelforge_runtime;
alter default privileges for role modelforge in schema public
revoke execute on functions from public;
do $database_privileges$
begin
execute pg_catalog.format(
'revoke create, temporary on database %I from modelforge_runtime',
pg_catalog.current_database()
);
end
$database_privileges$;
"""
def _normalise_timestamp(value: datetime | str) -> str:
if isinstance(value, datetime):
moment = value
elif isinstance(value, str):
candidate = value.strip()
if candidate.endswith("Z"):
candidate = candidate[:-1] + "+00:00"
try:
moment = datetime.fromisoformat(candidate)
except ValueError as error:
raise RuntimeError("legacy audit event has an invalid occurred_at") from error
else:
raise RuntimeError("legacy audit event has an invalid occurred_at")
if moment.tzinfo is None:
moment = moment.replace(tzinfo=UTC)
return moment.astimezone(UTC).isoformat(timespec="microseconds").replace("+00:00", "Z")
def _details(value: Any) -> dict[str, Any]:
if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError as error:
raise RuntimeError("legacy audit event details are not valid JSON") from error
if not isinstance(value, dict):
raise RuntimeError("legacy audit event details must be a JSON object")
return value
def _legacy_hash(row: sa.RowMapping) -> str:
payload = {
"correlation_id": row["correlation_id"],
"actor_type": row["actor_type"],
"actor_id": row["actor_id"],
"action": row["action"],
"resource_type": row["resource_type"],
"resource_id": row["resource_id"],
"outcome": row["outcome"],
"details": _details(row["details"]),
"previous_event_hash": row["previous_event_hash"],
}
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
def _prefix_entry(row: sa.RowMapping) -> bytes:
try:
event_id = str(uuid.UUID(str(row["id"])))
except (AttributeError, TypeError, ValueError) as error:
raise RuntimeError("legacy audit event id is not a UUID") from error
payload = {
"sequence": int(row["sequence"]),
"id": event_id,
"occurred_at": _normalise_timestamp(row["occurred_at"]),
"event_hash": str(row["event_hash"]),
}
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") + b"\n"
def _validate_legacy_rows(rows: list[sa.RowMapping]) -> dict[str, Any]:
"""Validate the production-shaped v1 chain and return its immutable cutover state."""
previous_hash: str | None = None
prefix = hashlib.sha256()
prefix.update(_LEGACY_PREFIX_DOMAIN)
for expected_sequence, row in enumerate(rows, start=1):
try:
sequence = int(row["sequence"])
except (TypeError, ValueError) as error:
raise RuntimeError("legacy audit event sequence is not an integer") from error
if sequence != expected_sequence:
raise RuntimeError(
f"legacy audit chain has sequence {sequence}; expected {expected_sequence}"
)
event_hash = str(row["event_hash"])
if _SHA256.fullmatch(event_hash) is None:
raise RuntimeError(f"legacy audit event {sequence} has a malformed event hash")
if row["previous_event_hash"] != previous_hash:
raise RuntimeError(f"legacy audit event {sequence} has an invalid previous hash")
if _legacy_hash(row) != event_hash:
raise RuntimeError(f"legacy audit event {sequence} content hash is invalid")
prefix.update(_prefix_entry(row))
previous_hash = event_hash
count = len(rows)
return {
"event_count": count,
"last_sequence": count,
"last_event_hash": previous_hash,
"v2_start_sequence": count + 1,
"legacy_prefix_count": count,
"legacy_prefix_seal": prefix.hexdigest(),
}
def _read_and_validate_legacy_chain(connection: Connection) -> dict[str, Any]:
rows = list(
connection.execute(
sa.text(
"select id, sequence, occurred_at, correlation_id, actor_type, actor_id, "
"action, resource_type, resource_id, outcome, details, previous_event_hash, "
"event_hash from audit_events order by sequence, id"
)
).mappings()
)
return _validate_legacy_rows(rows)
def _lock_legacy_audit_chain(connection: Connection) -> None:
"""Serialize validation and checkpoint seed against every legacy writer.
The shared advisory key coordinates with 0024-aware writers. ``ACCESS EXCLUSIVE`` also blocks
pre-0024 applications, which do not know that key, until this migration transaction commits.
SQLite is test-only; a no-op write upgrades its deferred transaction to a writer before the
validation read so another test connection cannot append into the validation/seed window.
"""
dialect = connection.dialect.name
if dialect == "postgresql":
connection.execute(
sa.text("select pg_advisory_xact_lock(:lock_key)"),
{"lock_key": _AUDIT_CHAIN_LOCK_KEY},
)
connection.execute(sa.text("lock table audit_events in access exclusive mode"))
return
if dialect == "sqlite":
connection.execute(sa.text("update audit_events set event_hash = event_hash where 1 = 0"))
return
raise RuntimeError(f"audit-chain migration does not support the {dialect!r} dialect")
def _validate_postgres_role_preflight(connection: Connection) -> None:
"""Require the separately provisioned non-superuser owner/runtime roles before DDL.
Existing 1.2.1 installations commonly made ``modelforge`` the cluster bootstrap superuser.
That credential cannot be converted into the API boundary implicitly by an application
migration. Operators must first run the documented admin-owned provisioning step; failure is
deliberately before this migration changes a column or seeds a checkpoint.
"""
if connection.dialect.name != "postgresql":
return
roles = list(
connection.execute(
sa.text(
"select rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, "
"rolcanlogin, rolreplication, rolbypassrls from pg_catalog.pg_roles "
"where rolname in (:owner_role, :runtime_role) order by rolname"
),
{"owner_role": _AUDIT_OWNER_ROLE, "runtime_role": _AUDIT_RUNTIME_ROLE},
).mappings()
)
by_name = {str(row["rolname"]): row for row in roles}
if set(by_name) != {_AUDIT_OWNER_ROLE, _AUDIT_RUNTIME_ROLE}:
raise RuntimeError(
"audit migration preflight requires separately provisioned modelforge owner and "
"modelforge_runtime roles; run the v1.2.1-to-schema-0024 role provisioning step"
)
current_role = str(connection.scalar(sa.text("select current_user")))
if current_role != _AUDIT_OWNER_ROLE:
raise RuntimeError(
"audit migration must run with the non-superuser modelforge owner credential"
)
session_role = str(connection.scalar(sa.text("select session_user")))
if session_role != _AUDIT_OWNER_ROLE:
raise RuntimeError(
"audit migration must authenticate directly as modelforge, not SET ROLE from admin"
)
for role_name, require_noinherit in (
(_AUDIT_OWNER_ROLE, False),
(_AUDIT_RUNTIME_ROLE, True),
):
role = by_name[role_name]
forbidden = any(
bool(role[field])
for field in (
"rolsuper",
"rolcreaterole",
"rolcreatedb",
"rolreplication",
"rolbypassrls",
)
)
if forbidden or not bool(role["rolcanlogin"]):
raise RuntimeError(f"database role {role_name} has forbidden administrative powers")
if require_noinherit and bool(role["rolinherit"]):
raise RuntimeError("modelforge_runtime must be provisioned NOINHERIT")
app_role_membership_count = int(
connection.scalar(
sa.text(
"select count(*) from pg_catalog.pg_auth_members as membership "
"join pg_catalog.pg_roles as member on member.oid = membership.member "
"where member.rolname in (:runtime_role, :owner_role)"
),
{"runtime_role": _AUDIT_RUNTIME_ROLE, "owner_role": _AUDIT_OWNER_ROLE},
)
or 0
)
if app_role_membership_count:
raise RuntimeError(
"modelforge and modelforge_runtime must have no SET ROLE-capable memberships"
)
def _install_postgres_audit_boundary(connection: Connection) -> None:
if connection.dialect.name == "postgresql":
for statement in _postgres_sql_statements(_POSTGRES_AUDIT_BOUNDARY_SQL):
_exec_postgres_sql(connection, statement)
def _exec_postgres_sql(connection: Connection, statement: str) -> None:
"""Execute trusted static SQL without exposing PostgreSQL percent syntax to DBAPI parsing."""
paramstyle = getattr(connection.dialect, "paramstyle", None)
driver_statement = (
statement.replace("%", "%%")
if paramstyle in {"format", "pyformat"}
else statement
)
connection.exec_driver_sql(driver_statement)
def _postgres_sql_statements(script: str) -> list[str]:
"""Split this migration's trusted static SQL without splitting function bodies."""
statements: list[str] = []
start = 0
index = 0
quote: str | None = None
while index < len(script):
if quote is not None:
if quote == "'" and script.startswith("''", index):
index += 2
continue
if script.startswith(quote, index):
index += len(quote)
quote = None
continue
index += 1
continue
character = script[index]
if character == "'":
quote = "'"
index += 1
continue
if character == "$":
delimiter = re.match(r"\$[A-Za-z_][A-Za-z0-9_]*\$|\$\$", script[index:])
if delimiter is not None:
quote = delimiter.group(0)
index += len(quote)
continue
if character == ";":
statement = script[start:index].strip()
if statement:
statements.append(statement)
start = index + 1
index += 1
trailing = script[start:].strip()
if quote is not None:
raise RuntimeError("generated PostgreSQL audit boundary SQL has an unterminated literal")
if trailing:
statements.append(trailing)
return statements
def _remove_postgres_audit_boundary(connection: Connection) -> None:
if connection.dialect.name != "postgresql":
return
script = (
"drop trigger if exists trg_modelforge_audit_events_owner on public.audit_events; "
"drop trigger if exists trg_modelforge_audit_events_truncate_owner "
"on public.audit_events; "
"drop trigger if exists trg_modelforge_audit_head_owner on public.audit_chain_heads; "
"drop trigger if exists trg_modelforge_audit_head_truncate_owner "
"on public.audit_chain_heads; "
"drop function if exists modelforge_audit.append_event_v2("
"uuid, timestamptz, text, text, text, text, text, text, text, jsonb, "
"bigint, bigint, text, text, bigint, bigint, text); "
"drop function if exists modelforge_audit.enforce_owner_mutation(); "
"drop schema if exists modelforge_audit; "
"grant select, insert on public.audit_events to modelforge_runtime"
)
for statement in _postgres_sql_statements(script):
_exec_postgres_sql(connection, statement)
def upgrade() -> None:
connection = op.get_bind()
_validate_postgres_role_preflight(connection)
_lock_legacy_audit_chain(connection)
# Validation deliberately precedes every schema mutation. In particular, legacy recovery
# markers written with a random hash stop the migration instead of being blessed by a seal.
legacy = _read_and_validate_legacy_chain(connection)
op.add_column(
"audit_events",
sa.Column("hash_format", sa.String(length=16), nullable=True),
)
op.add_column(
"audit_events",
sa.Column("canonical_payload", sa.Text(), nullable=True),
)
connection.execute(sa.text("update audit_events set hash_format = 'v1'"))
with op.batch_alter_table("audit_events") as batch:
batch.alter_column(
"hash_format",
existing_type=sa.String(length=16),
nullable=False,
)
batch.create_check_constraint(
"ck_audit_event_hash_format", "hash_format IN ('v1', 'v2')"
)
batch.create_check_constraint(
"ck_audit_event_canonical_payload",
"((hash_format = 'v1' AND canonical_payload IS NULL) OR "
"(hash_format = 'v2' AND canonical_payload IS NOT NULL))",
)
op.create_table(
"audit_chain_heads",
sa.Column("singleton_id", sa.Integer(), nullable=False),
sa.Column("event_count", sa.BigInteger(), nullable=False),
sa.Column("last_sequence", sa.BigInteger(), nullable=False),
sa.Column("last_event_hash", sa.String(length=64), nullable=True),
sa.Column("hash_format", sa.String(length=16), nullable=False),
sa.Column("v2_start_sequence", sa.BigInteger(), nullable=False),
sa.Column("legacy_prefix_count", sa.BigInteger(), nullable=False),
sa.Column("legacy_prefix_seal", sa.String(length=64), nullable=False),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("CURRENT_TIMESTAMP"),
nullable=False,
),
sa.CheckConstraint("singleton_id = 1", name="ck_audit_chain_head_singleton"),
sa.CheckConstraint("event_count >= 0", name="ck_audit_chain_head_count"),
sa.CheckConstraint("last_sequence >= 0", name="ck_audit_chain_head_sequence"),
sa.CheckConstraint(
"event_count = last_sequence",
name="ck_audit_chain_head_count_sequence",
),
sa.CheckConstraint("v2_start_sequence >= 1", name="ck_audit_chain_head_cutover"),
sa.CheckConstraint(
"legacy_prefix_count = v2_start_sequence - 1",
name="ck_audit_chain_head_prefix_count",
),
sa.CheckConstraint(
"legacy_prefix_count <= event_count",
name="ck_audit_chain_head_prefix_within_chain",
),
sa.CheckConstraint("hash_format = 'v2'", name="ck_audit_chain_head_hash_format"),
sa.CheckConstraint(
"length(legacy_prefix_seal) = 64",
name="ck_audit_chain_head_prefix_seal",
),
sa.CheckConstraint(
"((event_count = 0 AND last_sequence = 0 AND last_event_hash IS NULL) OR "
"(event_count > 0 AND last_sequence > 0 AND last_event_hash IS NOT NULL))",
name="ck_audit_chain_head_shape",
),
sa.PrimaryKeyConstraint("singleton_id"),
)
connection.execute(
sa.text(
"insert into audit_chain_heads (singleton_id, event_count, last_sequence, "
"last_event_hash, hash_format, v2_start_sequence, legacy_prefix_count, "
"legacy_prefix_seal) values (1, :event_count, :last_sequence, :last_event_hash, "
"'v2', :v2_start_sequence, :legacy_prefix_count, :legacy_prefix_seal)"
),
legacy,
)
_install_postgres_audit_boundary(connection)
def downgrade() -> None:
connection = op.get_bind()
_validate_postgres_role_preflight(connection)
_lock_legacy_audit_chain(connection)
non_legacy = int(
connection.scalar(
sa.text("select count(*) from audit_events where hash_format <> 'v1'")
)
or 0
)
if non_legacy:
raise RuntimeError(
"cannot downgrade audit hash format after v2 events exist without rewriting history"
)
legacy = _read_and_validate_legacy_chain(connection)
head = connection.execute(
sa.text(
"select event_count, last_sequence, last_event_hash, hash_format, "
"v2_start_sequence, legacy_prefix_count, legacy_prefix_seal "
"from audit_chain_heads where singleton_id = 1"
)
).mappings().one_or_none()
if head is None or head["hash_format"] != "v2":
raise RuntimeError("cannot downgrade a missing or malformed audit checkpoint")
for key, expected in legacy.items():
if head[key] != expected:
raise RuntimeError(f"cannot downgrade: audit checkpoint {key} is inconsistent")
_remove_postgres_audit_boundary(connection)
op.drop_table("audit_chain_heads")
with op.batch_alter_table("audit_events") as batch:
batch.drop_constraint("ck_audit_event_canonical_payload", type_="check")
batch.drop_constraint("ck_audit_event_hash_format", type_="check")
batch.drop_column("canonical_payload")
batch.drop_column("hash_format")
+65
View File
@@ -0,0 +1,65 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "modelforge-api"
version = "1.2.2"
description = "ITWorx ModelForge control-plane API"
license = "AGPL-3.0-or-later"
requires-python = ">=3.12"
dependencies = [
"fastapi==0.141.1",
"uvicorn[standard]==0.52.4",
"pydantic==2.13.4",
"pydantic-settings==2.15.0",
"sqlalchemy==2.0.52",
"alembic==1.19.1",
"psycopg[binary]==3.3.4",
"cryptography==50.0.1",
"redis==6.4.0",
"httpx==0.28.1",
"huggingface-hub==1.29.0",
"nvidia-ml-py==13.610.43",
"psutil==7.2.2",
"pyyaml==6.0.3",
"structlog==25.5.0",
]
[project.optional-dependencies]
dev = [
"pytest==8.4.2",
"pytest-asyncio==1.4.0",
"ruff==0.16.5",
"mypy==1.20.2",
]
[tool.hatch.build.targets.wheel]
packages = ["src/modelforge_api"]
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
[tool.mypy]
# Strict, and analysed for the platform the images actually run on.
#
# Both of these were assumed rather than configured. There was no [tool.mypy] section at all, so
# `mypy src` ran with defaults while every milestone report described it as strict; and it analysed
# the developer's platform, so a Windows-only winreg branch type-checked cleanly here and failed in
# CI on identical code. Turning strict on cost three stale `type: ignore` comments.
strict = true
platform = "linux"
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
extend-select = ["B", "BLE", "I", "S", "SIM", "UP"]
ignore = ["B008"]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]
"src/modelforge_api/settings.py" = ["S104"]
"src/modelforge_api/hardware/collectors.py" = ["B023"]
+11
View File
@@ -0,0 +1,11 @@
"""ITWorx ModelForge control plane.
The version is derived from the repository's VERSION file rather than declared again here. Four
independent copies of the product version is three chances to publish a release that misdescribes
itself, which is exactly the class of mistake a release gate is supposed to make impossible.
"""
from modelforge_api.domain.release import PRODUCT_VERSION
__all__ = ["__version__"]
__version__ = PRODUCT_VERSION
@@ -0,0 +1,208 @@
from __future__ import annotations
import hmac
import re
from dataclasses import dataclass
from enum import StrEnum
from typing import Annotated
from fastapi import Depends, Header, HTTPException, Request
from modelforge_api.settings import Settings, get_settings
class PrincipalRole(StrEnum):
"""Closed operator-console roles, ordered from read-only to security administration."""
VIEWER = "viewer"
OPERATOR = "operator"
ADMIN = "admin"
class AccessBoundary(StrEnum):
"""Mutually exclusive request authentication boundaries.
The control-plane boundary is deliberately the default. New endpoints therefore fail closed
behind the human operator credential until their narrower machine/public policy is explicitly
recorded here and in the route inventory tests.
"""
PUBLIC = "public"
SINGLE_USE_ENROLLMENT = "single_use_enrollment"
NODE = "node"
CAPABILITY_CLIENT = "capability_client"
CONTROL_PLANE = "control_plane"
_PUBLIC_REQUESTS = {
("GET", "/"),
("GET", "/api/v1/health/live"),
("GET", "/api/v1/health/ready"),
("GET", "/api/v1/version"),
}
_INTERACTIVE_API_PATHS = {"/docs", "/docs/oauth2-redirect", "/redoc", "/openapi.json"}
_NODE_REQUESTS = {
("POST", "/api/v1/agent/heartbeat"),
("PUT", "/api/v1/agent/inventory"),
("PUT", "/api/v1/agent/telemetry"),
("GET", "/api/v1/agent/artifact-jobs/next"),
("POST", "/api/v1/agent/artifact-jobs/{job_id}/progress"),
("POST", "/api/v1/agent/artifact-jobs/{job_id}/complete"),
("POST", "/api/v1/agent/artifact-jobs/{job_id}/fail"),
("GET", "/api/v1/agent/runtime-probes/next"),
("POST", "/api/v1/agent/runtime-probes/{probe_id}/progress"),
("POST", "/api/v1/agent/runtime-probes/{probe_id}/complete"),
("POST", "/api/v1/agent/runtime-probes/{probe_id}/fail"),
("GET", "/api/v1/agent/serving-jobs/next"),
("POST", "/api/v1/agent/serving-jobs/{job_id}/complete"),
("POST", "/api/v1/agent/serving-jobs/{job_id}/fail"),
("POST", "/api/v1/agent/serving-state"),
}
_NODE_PARAMETERIZED_REQUESTS = (
("POST", re.compile(r"^/api/v1/agent/artifact-jobs/[^/]+/(?:progress|complete|fail)$")),
("POST", re.compile(r"^/api/v1/agent/runtime-probes/[^/]+/(?:progress|complete|fail)$")),
("POST", re.compile(r"^/api/v1/agent/serving-jobs/[^/]+/(?:complete|fail)$")),
)
_CAPABILITY_REQUIREMENTS = {
("POST", "/api/v1/capabilities/rag.embedding@1/invoke"): "rag.embedding@1",
("POST", "/api/v1/capabilities/rag.reranking@1/invoke"): "rag.reranking@1",
("POST", "/api/v1/capabilities/document.ocr@1/invoke"): "document.ocr@1",
("POST", "/api/v1/capabilities/vision.embedding@1/invoke"): "vision.embedding@1",
("POST", "/api/v1/capabilities/speech.transcription@1/invoke"): "speech.transcription@1",
("POST", "/api/v1/capability-experiments/{route_key}/invoke"): "rag.embedding@1",
("POST", "/v1/embeddings"): "rag.embedding@1",
}
_CAPABILITY_CLIENT_REQUESTS = set(_CAPABILITY_REQUIREMENTS)
_CAPABILITY_EXPERIMENT_REQUEST = re.compile(r"^/api/v1/capability-experiments/[^/]+/invoke$")
def access_boundary_for_request(
method: str,
path: str,
) -> AccessBoundary:
"""Classify an HTTP request without reading its body or accepting credential aliases."""
normalized_method = method.upper()
if (normalized_method, path) in _PUBLIC_REQUESTS:
return AccessBoundary.PUBLIC
if path in _INTERACTIVE_API_PATHS:
# These routes exist only when the FastAPI app is explicitly constructed for development.
# In test/production they must reach routing unauthenticated so the disabled surface is a
# genuine 404 rather than an operator-auth challenge that reveals a hidden endpoint.
return AccessBoundary.PUBLIC
if normalized_method == "POST" and path == "/api/v1/agent/enroll":
return AccessBoundary.SINGLE_USE_ENROLLMENT
if (normalized_method, path) in _NODE_REQUESTS or any(
normalized_method == rule_method and pattern.fullmatch(path)
for rule_method, pattern in _NODE_PARAMETERIZED_REQUESTS
):
return AccessBoundary.NODE
if (normalized_method, path) in _CAPABILITY_CLIENT_REQUESTS or (
normalized_method == "POST" and _CAPABILITY_EXPERIMENT_REQUEST.fullmatch(path)
):
return AccessBoundary.CAPABILITY_CLIENT
return AccessBoundary.CONTROL_PLANE
def required_capability_for_request(method: str, path: str) -> str | None:
"""Return the exact capability scope for a registered project-facing route."""
normalized_method = method.upper()
requirement = _CAPABILITY_REQUIREMENTS.get((normalized_method, path))
if requirement is not None:
return requirement
if normalized_method == "POST" and _CAPABILITY_EXPERIMENT_REQUEST.fullmatch(path):
return "rag.embedding@1"
return None
def request_body_limit_bytes(settings: Settings, boundary: AccessBoundary) -> int:
"""Select an explicit pre-parser body limit for every request boundary."""
if boundary is AccessBoundary.CAPABILITY_CLIENT:
return settings.gateway_max_payload_bytes
if boundary in {AccessBoundary.NODE, AccessBoundary.SINGLE_USE_ENROLLMENT}:
return settings.node_agent_max_payload_bytes
return settings.control_plane_max_payload_bytes
_ROLE_RANK = {
PrincipalRole.VIEWER: 0,
PrincipalRole.OPERATOR: 1,
PrincipalRole.ADMIN: 2,
}
@dataclass(frozen=True, slots=True)
class Principal:
"""An authenticated human/control-plane principal.
Node and capability credentials deliberately never become a ``Principal``. Their separate
dependencies remain the only way into node-agent and inference surfaces, preventing credential
confusion at the type and dependency boundaries.
"""
subject: str
role: PrincipalRole
authentication_method: str
def permits(self, required: PrincipalRole) -> bool:
return _ROLE_RANK[self.role] >= _ROLE_RANK[required]
def authenticate_operator_token(settings: Settings, token: str | None) -> Principal:
"""Authenticate the backward-compatible operator key as an admin principal.
The legacy key is the sole human credential in this release. OIDC/session authentication can
add another principal producer later without changing route authorization policy.
"""
configured = settings.operator_api_key
if configured is None or not configured.get_secret_value():
raise HTTPException(status_code=503, detail="operator API authentication is not configured")
if token is None or not hmac.compare_digest(token, configured.get_secret_value()):
raise HTTPException(status_code=401, detail="invalid operator credential")
return Principal(
subject="legacy-operator",
role=PrincipalRole.ADMIN,
authentication_method="legacy_admin_token",
)
def authenticate_operator_principal(
request: Request,
settings: Annotated[Settings, Depends(get_settings)],
token: Annotated[str | None, Header(alias="X-ModelForge-Admin-Token")] = None,
) -> Principal:
"""Reuse the pre-body principal and retain a safe direct-dependency fallback."""
principal = getattr(request.state, "principal", None)
if isinstance(principal, Principal):
return principal
return authenticate_operator_token(settings, token)
AuthenticatedPrincipal = Annotated[Principal, Depends(authenticate_operator_principal)]
def _require_role(principal: Principal, required: PrincipalRole) -> Principal:
if not principal.permits(required):
raise HTTPException(status_code=403, detail="operator role is not authorized")
return principal
def require_viewer(principal: AuthenticatedPrincipal) -> Principal:
return _require_role(principal, PrincipalRole.VIEWER)
def require_operator(principal: AuthenticatedPrincipal) -> Principal:
return _require_role(principal, PrincipalRole.OPERATOR)
def require_admin(principal: AuthenticatedPrincipal) -> Principal:
return _require_role(principal, PrincipalRole.ADMIN)
Viewer = Annotated[Principal, Depends(require_viewer)]
Operator = Annotated[Principal, Depends(require_operator)]
Admin = Annotated[Principal, Depends(require_admin)]
@@ -0,0 +1,205 @@
from __future__ import annotations
from dataclasses import dataclass
from starlette.responses import JSONResponse
from starlette.types import ASGIApp, Message, Receive, Scope, Send
MAX_CONSECUTIVE_EMPTY_REQUEST_EVENTS = 32
MAX_TOTAL_EMPTY_REQUEST_EVENTS = 128
MAX_REQUEST_BODY_EVENTS = 4096
@dataclass(frozen=True, slots=True)
class RequestBodyTooLarge(Exception):
limit_bytes: int
@dataclass(frozen=True, slots=True)
class RequestBodyProgressExhausted(Exception):
received_events: int
empty_events: int
@dataclass(frozen=True, slots=True)
class InvalidContentLength(Exception):
message: str
class RequestBodyLimitMiddleware:
"""Reject declared or streamed oversized bodies before application parsing.
The receive wrapper forwards chunks only while the cumulative size is within the configured
boundary. It raises as soon as the next chunk crosses the limit and never drains or buffers the
remaining request body. It also rejects a body stream that exceeds the fixed request-event or
empty-progress budgets, preventing an immediately-ready sequence of empty ASGI frames from
spinning indefinitely without yielding useful input.
"""
def __init__(self, app: ASGIApp) -> None:
self.app = app
@staticmethod
def _declared_length(scope: Scope) -> int | None:
values = [
value for name, value in scope.get("headers", []) if name.lower() == b"content-length"
]
if not values:
return None
if len(values) != 1:
raise ValueError("multiple content-length headers are not accepted")
try:
rendered = values[0].decode("ascii")
if not rendered.isdecimal():
raise ValueError("content-length must be an unsigned decimal integer")
return int(rendered)
except UnicodeDecodeError as exc:
raise ValueError("content-length must be ASCII") from exc
@staticmethod
async def _error_response(
scope: Scope,
receive: Receive,
send: Send,
*,
status_code: int,
code: str,
message: str,
details: dict[str, int] | None = None,
) -> None:
state = scope.get("state", {})
correlation_id = state.get("correlation_id", "unknown")
response = JSONResponse(
status_code=status_code,
content={
"error": {
"code": code,
"message": message,
"correlation_id": correlation_id,
"details": details or {},
}
},
headers={"X-Correlation-ID": str(correlation_id)},
)
await response(scope, receive, send)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
state = scope.setdefault("state", {})
configured_limit: int | None = None
headers_checked = False
received_bytes = 0
received_events = 0
empty_events = 0
consecutive_empty_events = 0
request_error: (
InvalidContentLength | RequestBodyTooLarge | RequestBodyProgressExhausted | None
) = None
async def limited_receive() -> Message:
nonlocal configured_limit, headers_checked
nonlocal consecutive_empty_events, empty_events, received_bytes
nonlocal received_events, request_error
if not headers_checked:
candidate_limit = state.get("request_body_limit_bytes")
if not isinstance(candidate_limit, int) or candidate_limit < 0:
raise RuntimeError("request body limit was not established before routing")
configured_limit = candidate_limit
headers_checked = True
try:
declared_length = self._declared_length(scope)
except ValueError as exc:
request_error = InvalidContentLength(str(exc))
state["request_body_error_status_code"] = 400
raise request_error from exc
if declared_length is not None and declared_length > configured_limit:
request_error = RequestBodyTooLarge(configured_limit)
state["request_body_error_status_code"] = 413
raise request_error
message = await receive()
if message["type"] == "http.request":
if configured_limit is None: # pragma: no cover - guarded before receive
raise RuntimeError("request body limit was not established before routing")
received_events += 1
body = message.get("body", b"")
received_bytes += len(body)
if received_bytes > configured_limit:
request_error = RequestBodyTooLarge(configured_limit)
state["request_body_error_status_code"] = 413
raise request_error
if body:
consecutive_empty_events = 0
elif message.get("more_body", False):
empty_events += 1
consecutive_empty_events += 1
else:
consecutive_empty_events = 0
if (
received_events > MAX_REQUEST_BODY_EVENTS
or empty_events > MAX_TOTAL_EMPTY_REQUEST_EVENTS
or consecutive_empty_events > MAX_CONSECUTIVE_EMPTY_REQUEST_EVENTS
):
request_error = RequestBodyProgressExhausted(
received_events=received_events,
empty_events=empty_events,
)
state["request_body_error_status_code"] = 400
raise request_error
return message
response_started = False
async def tracked_send(message: Message) -> None:
nonlocal response_started
# FastAPI deliberately converts arbitrary request-body receive failures to a generic
# 400. Once this middleware has observed a boundary violation, suppress that parser
# response and emit the boundary's typed response after the inner app unwinds.
if request_error is not None:
return
if message["type"] == "http.response.start":
response_started = True
await send(message)
try:
await self.app(scope, limited_receive, tracked_send)
except (InvalidContentLength, RequestBodyProgressExhausted, RequestBodyTooLarge) as exc:
request_error = exc
if request_error is not None:
if response_started:
raise request_error
if isinstance(request_error, InvalidContentLength):
await self._error_response(
scope,
receive,
send,
status_code=400,
code="invalid_content_length",
message=request_error.message,
)
elif isinstance(request_error, RequestBodyTooLarge):
await self._error_response(
scope,
receive,
send,
status_code=413,
code="request_body_too_large",
message="Request body exceeds the permitted boundary",
details={"limit_bytes": request_error.limit_bytes},
)
else:
await self._error_response(
scope,
receive,
send,
status_code=400,
code="request_body_progress_exhausted",
message="Request body made insufficient bounded progress",
details={
"max_consecutive_empty_events": MAX_CONSECUTIVE_EMPTY_REQUEST_EVENTS,
"max_empty_events": MAX_TOTAL_EMPTY_REQUEST_EVENTS,
"max_request_events": MAX_REQUEST_BODY_EVENTS,
},
)
@@ -0,0 +1,134 @@
from __future__ import annotations
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import require_admin, require_operator, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.acquisition import (
ArtifactJobResponse,
ArtifactSetResponse,
DiscoveryCandidate,
DiscoverySearchRequest,
DownloadPlanCreate,
DownloadPlanResponse,
UpstreamRefreshRequest,
UpstreamSnapshotResponse,
)
from modelforge_api.providers.huggingface import OfficialHuggingFaceProvider
from modelforge_api.services.acquisition import AcquisitionService
from modelforge_api.settings import Settings, get_settings
router = APIRouter(
prefix="/api/v1",
tags=["artifact-acquisition"],
dependencies=[Depends(require_viewer)],
)
def get_acquisition_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> AcquisitionService:
token = settings.hf_token.get_secret_value() if settings.hf_token else None
provider = OfficialHuggingFaceProvider(token=token, timeout=settings.hf_timeout_seconds)
return AcquisitionService(session, settings, provider)
Service = Annotated[AcquisitionService, Depends(get_acquisition_service)]
@router.post(
"/discovery/search",
response_model=list[DiscoveryCandidate],
dependencies=[Depends(require_operator)],
)
def search(request: DiscoverySearchRequest, service: Service) -> list[DiscoveryCandidate]:
return service.search(request)
@router.post(
"/models/{model_id}/refresh-upstream",
response_model=UpstreamSnapshotResponse,
dependencies=[Depends(require_operator)],
)
def refresh_upstream(
model_id: uuid.UUID, request: UpstreamRefreshRequest, service: Service
) -> UpstreamSnapshotResponse:
return service.refresh_model(model_id, request.revision)
@router.get("/models/{model_id}/upstream", response_model=UpstreamSnapshotResponse)
def model_upstream(model_id: uuid.UUID, service: Service) -> UpstreamSnapshotResponse:
return service.latest_snapshot(model_id)
@router.get("/revisions/{revision_id}/artifact-sets", response_model=list[ArtifactSetResponse])
def artifact_sets(revision_id: uuid.UUID, service: Service) -> list[ArtifactSetResponse]:
return service.artifact_sets(revision_id)
@router.post(
"/download-plans",
response_model=DownloadPlanResponse,
status_code=201,
dependencies=[Depends(require_operator)],
)
def create_download_plan(request: DownloadPlanCreate, service: Service) -> DownloadPlanResponse:
return service.create_plan(request)
@router.get("/download-plans/{plan_id}", response_model=DownloadPlanResponse)
def download_plan(plan_id: uuid.UUID, service: Service) -> DownloadPlanResponse:
return service.plan_response(plan_id)
@router.post(
"/download-plans/{plan_id}/approve",
response_model=DownloadPlanResponse,
dependencies=[Depends(require_admin)],
)
def approve_download_plan(plan_id: uuid.UUID, service: Service) -> DownloadPlanResponse:
return service.approve_plan(plan_id)
@router.post(
"/download-plans/{plan_id}/execute",
response_model=ArtifactJobResponse,
dependencies=[Depends(require_operator)],
)
def execute_download_plan(plan_id: uuid.UUID, service: Service) -> ArtifactJobResponse:
return service.execute_plan(plan_id)
@router.get("/artifact-jobs", response_model=list[ArtifactJobResponse])
def artifact_jobs(
service: Service, limit: Annotated[int, Query(ge=1, le=100)] = 100
) -> list[ArtifactJobResponse]:
return service.jobs()[:limit]
@router.get("/artifact-jobs/{job_id}", response_model=ArtifactJobResponse)
def artifact_job(job_id: uuid.UUID, service: Service) -> ArtifactJobResponse:
return service.job(job_id)
@router.post(
"/artifact-jobs/{job_id}/cancel",
response_model=ArtifactJobResponse,
dependencies=[Depends(require_operator)],
)
def cancel_artifact_job(job_id: uuid.UUID, service: Service) -> ArtifactJobResponse:
return service.cancel(job_id)
@router.post(
"/artifact-jobs/{job_id}/retry",
response_model=ArtifactJobResponse,
dependencies=[Depends(require_operator)],
)
def retry_artifact_job(job_id: uuid.UUID, service: Service) -> ArtifactJobResponse:
return service.retry(job_id)
@@ -0,0 +1,371 @@
from __future__ import annotations
import time
import uuid
from collections import defaultdict, deque
from typing import Annotated
from fastapi import APIRouter, Depends, Header, HTTPException, Request, Response, status
from pydantic import BaseModel
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin
from modelforge_api.db import get_session
from modelforge_api.domain.acquisition import (
AgentArtifactJobLease,
AgentJobComplete,
AgentJobControl,
AgentJobFailure,
AgentJobProgress,
ArtifactJobResponse,
)
from modelforge_api.domain.agent_protocol import (
EnrollmentRequest,
EnrollmentResponse,
EnrollmentTokenCreate,
EnrollmentTokenCreated,
EnrollmentTokenSummary,
HeartbeatRequest,
InventoryReport,
NodeCredentialCreated,
NodeManagementUpdate,
ObservationAck,
TelemetryReport,
)
from modelforge_api.domain.node_decommission import (
NodeDecommissionExecute,
NodeDecommissionPreview,
NodeDecommissionResult,
)
from modelforge_api.domain.runtime import (
AgentRuntimeProbeComplete,
AgentRuntimeProbeControl,
AgentRuntimeProbeFailure,
AgentRuntimeProbeLease,
AgentRuntimeProbeProgress,
RuntimeProbeResponse,
)
from modelforge_api.persistence.models import ComputeNode, NodeCredential
from modelforge_api.providers.huggingface import OfficialHuggingFaceProvider
from modelforge_api.services.acquisition import AcquisitionService
from modelforge_api.services.node_agent import NodeAgentService, NodeAuthenticationEvidence
from modelforge_api.services.node_decommission import NodeDecommissionService
from modelforge_api.services.runtime import RuntimeService
from modelforge_api.settings import Settings, get_settings
router = APIRouter(tags=["node-agent"])
class ActionResponse(BaseModel):
status: str = "ok"
class AttemptLimiter:
def __init__(self, limit: int = 10, window_seconds: int = 60) -> None:
self.limit = limit
self.window_seconds = window_seconds
self.attempts: dict[str, deque[float]] = defaultdict(deque)
def check(self, key: str) -> None:
now = time.monotonic()
bucket = self.attempts[key]
while bucket and bucket[0] <= now - self.window_seconds:
bucket.popleft()
if len(bucket) >= self.limit:
raise HTTPException(status_code=429, detail="enrollment rate limit exceeded")
bucket.append(now)
enrollment_limiter = AttemptLimiter()
def get_agent_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> NodeAgentService:
return NodeAgentService(session, settings)
Service = Annotated[NodeAgentService, Depends(get_agent_service)]
def get_decommission_service(
session: Annotated[Session, Depends(get_session)],
) -> NodeDecommissionService:
return NodeDecommissionService(session)
DecommissionService = Annotated[NodeDecommissionService, Depends(get_decommission_service)]
@router.post(
"/api/v1/admin/node-enrollments",
response_model=EnrollmentTokenCreated,
status_code=status.HTTP_201_CREATED,
)
def create_enrollment(
request: EnrollmentTokenCreate, service: Service, _admin: Admin, response: Response
) -> EnrollmentTokenCreated:
response.headers["Cache-Control"] = "no-store"
return service.create_enrollment(request)
@router.get("/api/v1/admin/node-enrollments", response_model=list[EnrollmentTokenSummary])
def list_enrollments(service: Service, _admin: Admin) -> list[EnrollmentTokenSummary]:
return [
EnrollmentTokenSummary(
id=str(item.id),
created_at=item.created_at,
expires_at=item.expires_at,
used_at=item.used_at,
revoked_at=item.revoked_at,
)
for item in service.repository.enrollments()
]
@router.delete("/api/v1/admin/node-enrollments/{enrollment_id}", response_model=ActionResponse)
def revoke_enrollment(enrollment_id: uuid.UUID, service: Service, _admin: Admin) -> ActionResponse:
service.revoke_enrollment(enrollment_id)
return ActionResponse()
@router.patch("/api/v1/admin/hardware/nodes/{node_id}", response_model=ActionResponse)
def update_node(
node_id: uuid.UUID,
request: NodeManagementUpdate,
service: Service,
_admin: Admin,
) -> ActionResponse:
service.update_node(node_id, request)
return ActionResponse()
@router.post(
"/api/v1/admin/hardware/nodes/{node_id}/decommission/preview",
response_model=NodeDecommissionPreview,
)
def preview_node_decommission(
node_id: uuid.UUID, service: DecommissionService, _admin: Admin
) -> NodeDecommissionPreview:
return service.preview(node_id)
@router.post(
"/api/v1/admin/hardware/nodes/{node_id}/decommission",
response_model=NodeDecommissionResult,
)
def execute_node_decommission(
node_id: uuid.UUID,
request: NodeDecommissionExecute,
service: DecommissionService,
_admin: Admin,
) -> NodeDecommissionResult:
return service.execute(node_id, request)
@router.delete("/api/v1/admin/hardware/nodes/{node_id}/credential", response_model=ActionResponse)
def revoke_credential(node_id: uuid.UUID, service: Service, _admin: Admin) -> ActionResponse:
service.revoke_credential(node_id)
return ActionResponse()
@router.post(
"/api/v1/admin/hardware/nodes/{node_id}/credential/rotate",
response_model=NodeCredentialCreated,
)
def rotate_credential(
node_id: uuid.UUID, service: Service, _admin: Admin, response: Response
) -> NodeCredentialCreated:
response.headers["Cache-Control"] = "no-store"
return service.rotate_credential(node_id)
@router.post(
"/api/v1/agent/enroll",
response_model=EnrollmentResponse,
status_code=status.HTTP_201_CREATED,
)
def enroll(
request: EnrollmentRequest, http_request: Request, service: Service, response: Response
) -> EnrollmentResponse:
enrollment_limiter.check(http_request.client.host if http_request.client else "unknown")
response.headers["Cache-Control"] = "no-store"
return service.enroll(request)
def authenticated_node(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> tuple[NodeCredential, ComputeNode]:
evidence = getattr(request.state, "node_authentication", None)
if isinstance(evidence, NodeAuthenticationEvidence):
return service.reuse_authentication(evidence, authorization)
return service.authenticate(authorization)
NodeIdentity = tuple[NodeCredential, ComputeNode]
AgentIdentity = Annotated[NodeIdentity, Depends(authenticated_node)]
def get_agent_acquisition_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> AcquisitionService:
token = settings.hf_token.get_secret_value() if settings.hf_token else None
return AcquisitionService(
session,
settings,
OfficialHuggingFaceProvider(token=token, timeout=settings.hf_timeout_seconds),
actor_type="node_agent",
actor_id="authenticated-node",
)
AgentAcquisition = Annotated[AcquisitionService, Depends(get_agent_acquisition_service)]
def get_agent_runtime_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> RuntimeService:
return RuntimeService(
session,
settings,
actor_type="runtime_worker",
actor_id="authenticated-node",
)
AgentRuntime = Annotated[RuntimeService, Depends(get_agent_runtime_service)]
@router.post("/api/v1/agent/heartbeat", response_model=ObservationAck)
def heartbeat(
request: HeartbeatRequest, service: Service, identity: AgentIdentity
) -> ObservationAck:
_credential, node = identity
return service.heartbeat(node, request)
@router.put("/api/v1/agent/inventory", response_model=ObservationAck)
def publish_inventory(
request: InventoryReport, service: Service, identity: AgentIdentity
) -> ObservationAck:
_credential, node = identity
return service.publish_inventory(node, request)
@router.put("/api/v1/agent/telemetry", response_model=ObservationAck)
def publish_telemetry(
request: TelemetryReport, service: Service, identity: AgentIdentity
) -> ObservationAck:
_credential, node = identity
return service.publish_telemetry(node, request)
@router.get(
"/api/v1/agent/artifact-jobs/next",
response_model=AgentArtifactJobLease | None,
)
def claim_artifact_job(
acquisition: AgentAcquisition, identity: AgentIdentity
) -> AgentArtifactJobLease | None:
_credential, node = identity
return acquisition.claim_next(node)
@router.post(
"/api/v1/agent/artifact-jobs/{job_id}/progress",
response_model=AgentJobControl,
)
def artifact_job_progress(
job_id: uuid.UUID,
request: AgentJobProgress,
acquisition: AgentAcquisition,
identity: AgentIdentity,
) -> AgentJobControl:
_credential, node = identity
return acquisition.progress(job_id, node, request)
@router.post(
"/api/v1/agent/artifact-jobs/{job_id}/complete",
response_model=ArtifactJobResponse,
)
def artifact_job_complete(
job_id: uuid.UUID,
request: AgentJobComplete,
acquisition: AgentAcquisition,
identity: AgentIdentity,
) -> ArtifactJobResponse:
_credential, node = identity
return acquisition.complete(job_id, node, request)
@router.post(
"/api/v1/agent/artifact-jobs/{job_id}/fail",
response_model=ArtifactJobResponse,
)
def artifact_job_fail(
job_id: uuid.UUID,
request: AgentJobFailure,
acquisition: AgentAcquisition,
identity: AgentIdentity,
) -> ArtifactJobResponse:
_credential, node = identity
return acquisition.fail(job_id, node, request)
@router.get(
"/api/v1/agent/runtime-probes/next",
response_model=AgentRuntimeProbeLease | None,
)
def claim_runtime_probe(
runtime: AgentRuntime, identity: AgentIdentity
) -> AgentRuntimeProbeLease | None:
_credential, node = identity
return runtime.claim_next(node)
@router.post(
"/api/v1/agent/runtime-probes/{probe_id}/progress",
response_model=AgentRuntimeProbeControl,
)
def runtime_probe_progress(
probe_id: uuid.UUID,
request: AgentRuntimeProbeProgress,
runtime: AgentRuntime,
identity: AgentIdentity,
) -> AgentRuntimeProbeControl:
_credential, node = identity
return runtime.progress(probe_id, node, request)
@router.post(
"/api/v1/agent/runtime-probes/{probe_id}/complete",
response_model=RuntimeProbeResponse,
)
def runtime_probe_complete(
probe_id: uuid.UUID,
request: AgentRuntimeProbeComplete,
runtime: AgentRuntime,
identity: AgentIdentity,
) -> RuntimeProbeResponse:
_credential, node = identity
return runtime.complete(probe_id, node, request)
@router.post(
"/api/v1/agent/runtime-probes/{probe_id}/fail",
response_model=RuntimeProbeResponse,
)
def runtime_probe_fail(
probe_id: uuid.UUID,
request: AgentRuntimeProbeFailure,
runtime: AgentRuntime,
identity: AgentIdentity,
) -> RuntimeProbeResponse:
_credential, node = identity
return runtime.fail(probe_id, node, request)
@@ -0,0 +1,273 @@
from fastapi import APIRouter, Depends
from sqlalchemy import select
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.schemas import (
CapabilityContractResponse,
CapabilityEstateResponse,
InstallationDependencyResponse,
ModelInstallationRationaleResponse,
ProjectBindingResponse,
ProjectResponse,
)
from modelforge_api.persistence.models import (
ArtifactSet,
Capability,
CapabilityContract,
CapabilityDeployment,
CapabilityEvaluationRun,
CapabilityResourceEnvelope,
Model,
ModelRevision,
Project,
ProjectBinding,
ProjectFitEvidence,
ResidencyAllocation,
RuntimeProfile,
ServiceClient,
)
from modelforge_api.services.manifest_registry import ManifestRegistry, get_manifest_registry
router = APIRouter(
prefix="/api/v1", tags=["registry"], dependencies=[Depends(require_viewer)]
)
@router.get("/capabilities", response_model=list[CapabilityContractResponse])
def list_capabilities(
registry: ManifestRegistry = Depends(get_manifest_registry),
session: Session = Depends(get_session),
) -> list[CapabilityContractResponse]:
responses = []
for contract in registry.capabilities():
stable = session.execute(
select(CapabilityDeployment)
.join(
CapabilityContract,
CapabilityContract.id == CapabilityDeployment.capability_contract_id,
)
.join(Capability, Capability.id == CapabilityContract.capability_id)
.where(
Capability.key == contract.capability,
CapabilityContract.version == contract.version,
CapabilityDeployment.status == "stable",
CapabilityDeployment.production.is_(True),
)
).scalar_one_or_none()
responses.append(
CapabilityContractResponse(
key=contract.capability,
version=contract.version,
description=contract.description,
contract=contract,
stable_deployment=(
{
"id": str(stable.id),
"status": stable.status,
"health": stable.health_status,
"production": stable.production,
}
if stable
else None
),
)
)
return responses
@router.get("/capability-estate", response_model=list[CapabilityEstateResponse])
def capability_estate(
registry: ManifestRegistry = Depends(get_manifest_registry),
session: Session = Depends(get_session),
) -> list[CapabilityEstateResponse]:
responses: list[CapabilityEstateResponse] = []
for manifest in registry.capabilities():
contract = session.scalar(
select(CapabilityContract)
.join(Capability, Capability.id == CapabilityContract.capability_id)
.where(Capability.key == manifest.capability, CapabilityContract.version == manifest.version)
)
deployment = None
model = None
revision = None
profile = None
envelope = None
latest_evaluation = None
if contract:
deployment = session.scalar(
select(CapabilityDeployment)
.where(CapabilityDeployment.capability_contract_id == contract.id)
.order_by(
CapabilityDeployment.production.desc(),
CapabilityDeployment.created_at.desc(),
)
.limit(1)
)
if deployment:
artifact_set = session.get(ArtifactSet, deployment.artifact_set_id)
revision = session.get(ModelRevision, artifact_set.revision_id) if artifact_set else None
model = session.get(Model, revision.model_id) if revision else None
profile = session.get(RuntimeProfile, deployment.runtime_profile_id)
envelope = session.scalar(
select(CapabilityResourceEnvelope).where(
CapabilityResourceEnvelope.capability_deployment_id == deployment.id
)
)
latest_evaluation = session.scalar(
select(CapabilityEvaluationRun)
.where(CapabilityEvaluationRun.capability_deployment_id == deployment.id)
.order_by(CapabilityEvaluationRun.created_at.desc())
.limit(1)
)
operational_state = (
deployment.status
if deployment
else ("blocked" if manifest.estate.stability == "blocked" else "not_deployed")
)
responses.append(
CapabilityEstateResponse(
capability=manifest.capability,
version=manifest.version,
category=manifest.estate.category,
purpose=manifest.estate.purpose,
declared_stability=manifest.estate.stability,
operational_state=operational_state,
current_deployment_id=deployment.id if deployment else None,
model=model.display_name if model else None,
revision=revision.resolved_commit_sha if revision else None,
runtime=profile.runtime_type if profile else None,
node=str(deployment.compute_node_id) if deployment else None,
resource_class=manifest.estate.resource_class,
measured_required_vram_bytes=envelope.required_vram_bytes if envelope else None,
consumers=manifest.estate.consumers,
privacy_class=manifest.privacy.classification,
evaluation_type=manifest.estate.evaluation_type,
evaluation_state=latest_evaluation.status if latest_evaluation else "not_evaluated",
)
)
return responses
@router.get(
"/models/installation-rationale",
response_model=list[ModelInstallationRationaleResponse],
)
def model_installation_rationale(
session: Session = Depends(get_session),
) -> list[ModelInstallationRationaleResponse]:
responses: list[ModelInstallationRationaleResponse] = []
for model in session.scalars(select(Model).order_by(Model.display_name)).all():
revisions = session.scalars(
select(ModelRevision).where(ModelRevision.model_id == model.id)
).all()
revision_ids = [item.id for item in revisions]
sets = (
session.scalars(select(ArtifactSet).where(ArtifactSet.revision_id.in_(revision_ids))).all()
if revision_ids
else []
)
installed_sets = [item for item in sets if item.availability == "local"]
dependencies: list[InstallationDependencyResponse] = []
for artifact_set in installed_sets:
deployments = session.scalars(
select(CapabilityDeployment).where(
CapabilityDeployment.artifact_set_id == artifact_set.id
)
).all()
for deployment in deployments:
contract = session.get(CapabilityContract, deployment.capability_contract_id)
capability = session.get(Capability, contract.capability_id) if contract else None
if not contract or not capability:
continue
projects = session.scalars(
select(Project)
.join(ProjectBinding, ProjectBinding.project_id == Project.id)
.where(
ProjectBinding.capability_contract_id == contract.id,
ProjectBinding.deprecated_at.is_(None),
)
.distinct()
).all()
active_projects = session.scalars(
select(Project)
.join(ProjectBinding, ProjectBinding.project_id == Project.id)
.join(ServiceClient, ServiceClient.project_binding_id == ProjectBinding.id)
.where(
ProjectBinding.capability_contract_id == contract.id,
ServiceClient.status == "active",
)
.distinct()
).all()
project_fit_ids = session.scalars(
select(ProjectFitEvidence.id)
.join(
ProjectBinding,
ProjectBinding.id == ProjectFitEvidence.project_binding_id,
)
.where(ProjectBinding.capability_contract_id == contract.id)
).all()
evaluations = session.scalars(
select(CapabilityEvaluationRun).where(
CapabilityEvaluationRun.capability_deployment_id == deployment.id
)
).all()
residency = session.scalar(
select(ResidencyAllocation).where(
ResidencyAllocation.capability_deployment_id == deployment.id
)
)
dependencies.append(
InstallationDependencyResponse(
capability=capability.key,
version=contract.version,
deployment_id=deployment.id,
channel=deployment.channel,
production=deployment.production,
project_consumers=[item.key for item in projects],
active_project_consumers=[item.key for item in active_projects],
project_fit_evidence_ids=list(project_fit_ids),
evaluation_run_ids=[item.id for item in evaluations],
last_used_at=residency.last_used_at if residency else None,
)
)
blockers = []
if dependencies:
blockers.append("capability_deployment_dependency")
if any(item.production for item in dependencies):
blockers.append("production_dependency")
responses.append(
ModelInstallationRationaleResponse(
model_id=model.id,
display_name=model.display_name,
upstream_source=model.upstream_source,
installed=bool(installed_sets),
installed_bytes=sum(item.total_size_bytes for item in installed_sets),
dependencies=dependencies,
can_delete=bool(installed_sets) and not blockers,
deletion_blockers=blockers,
)
)
return responses
@router.get("/projects", response_model=list[ProjectResponse])
def list_projects(
registry: ManifestRegistry = Depends(get_manifest_registry),
) -> list[ProjectResponse]:
return [
ProjectResponse(
**project.project.model_dump(),
notes=project.notes,
bindings=[
ProjectBindingResponse(
capability=capability,
contract_version=binding.contract_version,
binding=binding,
)
for capability, binding in project.bindings.items()
],
)
for project in registry.projects()
]
@@ -0,0 +1,387 @@
from __future__ import annotations
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.capability_evaluation import (
CapabilityAdvisorResponse,
CapabilityEvaluationRunCreate,
CapabilityEvaluationRunResponse,
CapabilityEvaluationSuiteCreate,
CapabilityEvaluationSuiteResponse,
)
from modelforge_api.domain.evaluation import (
AdvisorPolicyResponse,
AdvisorPolicyUpdate,
AdvisorRecommendationCreate,
AdvisorRecommendationDismiss,
AdvisorRecommendationResponse,
DiscoveryCandidateAssessmentCreate,
DiscoveryCandidateAssessmentResponse,
EmbeddingMigrationCreate,
EmbeddingMigrationResponse,
EvaluationCaseDefinitionResponse,
EvaluationCaseResultResponse,
EvaluationComparisonCreate,
EvaluationComparisonResponse,
EvaluationRunComplete,
EvaluationRunCreate,
EvaluationRunResponse,
EvaluationSuiteCreate,
EvaluationSuiteResponse,
MigrationUpdate,
ModelComparisonCreate,
ModelComparisonResponse,
RerankingCaseResultResponse,
RerankingRunComplete,
RerankingRunCreate,
RerankingRunResponse,
RetrievalCandidatePoolCreate,
RetrievalCandidatePoolResponse,
RetrievalPipelineIdentityCreate,
RetrievalPipelineIdentityResponse,
)
from modelforge_api.services.capability_evaluation import CapabilityEvaluationService
from modelforge_api.services.evaluation import EvaluationService
router = APIRouter(
prefix="/api/v1",
tags=["project-evaluation"],
dependencies=[Depends(require_viewer)],
)
def service(session: Annotated[Session, Depends(get_session)]) -> EvaluationService:
return EvaluationService(session)
Service = Annotated[EvaluationService, Depends(service)]
def capability_service(
session: Annotated[Session, Depends(get_session)],
) -> CapabilityEvaluationService:
return CapabilityEvaluationService(session)
CapabilityService = Annotated[CapabilityEvaluationService, Depends(capability_service)]
@router.get("/capability-advisor", response_model=list[CapabilityAdvisorResponse])
def capability_advisor(svc: CapabilityService) -> list[CapabilityAdvisorResponse]:
return svc.advisor()
@router.get(
"/capability-evaluation-suites",
response_model=list[CapabilityEvaluationSuiteResponse],
)
def capability_suites(svc: CapabilityService) -> list[CapabilityEvaluationSuiteResponse]:
return svc.suites()
@router.post(
"/capability-evaluation-suites",
response_model=CapabilityEvaluationSuiteResponse,
status_code=status.HTTP_201_CREATED,
)
def create_capability_suite(
request: CapabilityEvaluationSuiteCreate, svc: CapabilityService, _admin: Admin
) -> CapabilityEvaluationSuiteResponse:
return svc.create_suite(request)
@router.get(
"/capability-evaluation-runs",
response_model=list[CapabilityEvaluationRunResponse],
)
def capability_runs(svc: CapabilityService) -> list[CapabilityEvaluationRunResponse]:
return svc.runs()
@router.post(
"/capability-evaluation-runs",
response_model=CapabilityEvaluationRunResponse,
status_code=status.HTTP_201_CREATED,
)
def create_capability_run(
request: CapabilityEvaluationRunCreate, svc: CapabilityService, _admin: Admin
) -> CapabilityEvaluationRunResponse:
return svc.create_run(request)
@router.get("/evaluation-suites", response_model=list[EvaluationSuiteResponse])
def suites(svc: Service) -> list[EvaluationSuiteResponse]:
return svc.suites()
@router.post(
"/evaluation-suites",
response_model=EvaluationSuiteResponse,
status_code=status.HTTP_201_CREATED,
)
def create_suite(
request: EvaluationSuiteCreate, svc: Service, _admin: Admin
) -> EvaluationSuiteResponse:
return svc.create_suite(request)
@router.get(
"/evaluation-suites/{suite_id}/revisions/{revision_id}/cases",
response_model=list[EvaluationCaseDefinitionResponse],
)
def suite_cases(
suite_id: uuid.UUID, revision_id: uuid.UUID, svc: Service
) -> list[EvaluationCaseDefinitionResponse]:
return svc.suite_cases(suite_id, revision_id)
@router.get("/evaluation-runs", response_model=list[EvaluationRunResponse])
def runs(svc: Service) -> list[EvaluationRunResponse]:
return svc.runs()
@router.post(
"/evaluation-runs", response_model=EvaluationRunResponse, status_code=status.HTTP_201_CREATED
)
def create_run(request: EvaluationRunCreate, svc: Service, _admin: Admin) -> EvaluationRunResponse:
return svc.create_run(request)
@router.get("/evaluation-runs/{run_id}", response_model=EvaluationRunResponse)
def run(run_id: uuid.UUID, svc: Service) -> EvaluationRunResponse:
return svc.run(run_id)
@router.post("/evaluation-runs/{run_id}/complete", response_model=EvaluationRunResponse)
def complete_run(
run_id: uuid.UUID, request: EvaluationRunComplete, svc: Service, _admin: Admin
) -> EvaluationRunResponse:
return svc.complete_run(run_id, request)
@router.get("/evaluation-runs/{run_id}/cases", response_model=list[EvaluationCaseResultResponse])
def case_results(run_id: uuid.UUID, svc: Service) -> list[EvaluationCaseResultResponse]:
return svc.case_results(run_id)
@router.post(
"/retrieval-candidate-pools",
response_model=RetrievalCandidatePoolResponse,
status_code=status.HTTP_201_CREATED,
)
def create_candidate_pool(
request: RetrievalCandidatePoolCreate, svc: Service, _admin: Admin
) -> RetrievalCandidatePoolResponse:
return svc.create_candidate_pool(request)
@router.get("/retrieval-candidate-pools", response_model=list[RetrievalCandidatePoolResponse])
def candidate_pools(svc: Service) -> list[RetrievalCandidatePoolResponse]:
return svc.candidate_pools()
@router.post(
"/retrieval-pipeline-identities",
response_model=RetrievalPipelineIdentityResponse,
status_code=status.HTTP_201_CREATED,
)
def create_pipeline_identity(
request: RetrievalPipelineIdentityCreate, svc: Service, _admin: Admin
) -> RetrievalPipelineIdentityResponse:
return svc.create_pipeline_identity(request)
@router.get(
"/retrieval-pipeline-identities", response_model=list[RetrievalPipelineIdentityResponse]
)
def pipeline_identities(svc: Service) -> list[RetrievalPipelineIdentityResponse]:
return svc.pipeline_identities()
@router.post(
"/reranking-runs",
response_model=RerankingRunResponse,
status_code=status.HTTP_201_CREATED,
)
def create_reranking_run(
request: RerankingRunCreate, svc: Service, _admin: Admin
) -> RerankingRunResponse:
return svc.create_reranking_run(request)
@router.get("/reranking-runs", response_model=list[RerankingRunResponse])
def reranking_runs(svc: Service) -> list[RerankingRunResponse]:
return svc.reranking_runs()
@router.post("/reranking-runs/{run_id}/complete", response_model=RerankingRunResponse)
def complete_reranking_run(
run_id: uuid.UUID, request: RerankingRunComplete, svc: Service, _admin: Admin
) -> RerankingRunResponse:
return svc.complete_reranking_run(run_id, request)
@router.get("/reranking-runs/{run_id}/cases", response_model=list[RerankingCaseResultResponse])
def reranking_case_results(run_id: uuid.UUID, svc: Service) -> list[RerankingCaseResultResponse]:
return svc.reranking_case_results(run_id)
@router.post(
"/discovery-assessments",
response_model=DiscoveryCandidateAssessmentResponse,
status_code=status.HTTP_201_CREATED,
)
def create_discovery_assessment(
request: DiscoveryCandidateAssessmentCreate, svc: Service, _admin: Admin
) -> DiscoveryCandidateAssessmentResponse:
return svc.create_discovery_assessment(request)
@router.get("/discovery-assessments", response_model=list[DiscoveryCandidateAssessmentResponse])
def discovery_assessments(svc: Service) -> list[DiscoveryCandidateAssessmentResponse]:
return svc.discovery_assessments()
@router.post(
"/evaluation-comparisons",
response_model=EvaluationComparisonResponse,
status_code=status.HTTP_201_CREATED,
)
def compare(
request: EvaluationComparisonCreate, svc: Service, _admin: Admin
) -> EvaluationComparisonResponse:
return svc.compare(request)
@router.get("/evaluation-comparisons", response_model=list[EvaluationComparisonResponse])
def comparisons(svc: Service) -> list[EvaluationComparisonResponse]:
return svc.comparisons()
@router.get("/evaluation-comparisons/{comparison_id}", response_model=EvaluationComparisonResponse)
def comparison(comparison_id: uuid.UUID, svc: Service) -> EvaluationComparisonResponse:
return svc.comparison(comparison_id)
@router.post(
"/model-comparisons",
response_model=ModelComparisonResponse,
status_code=status.HTTP_201_CREATED,
)
def create_model_comparison(
request: ModelComparisonCreate, svc: Service, _admin: Admin
) -> ModelComparisonResponse:
return svc.create_model_comparison(request)
@router.get("/model-comparisons", response_model=list[ModelComparisonResponse])
def model_comparisons(svc: Service) -> list[ModelComparisonResponse]:
return svc.model_comparisons()
@router.get("/model-comparisons/{comparison_id}", response_model=ModelComparisonResponse)
def model_comparison(comparison_id: uuid.UUID, svc: Service) -> ModelComparisonResponse:
return svc.model_comparison(comparison_id)
@router.post(
"/model-comparisons/{comparison_id}/recommendations",
response_model=AdvisorRecommendationResponse,
status_code=status.HTTP_201_CREATED,
)
def create_recommendation(
comparison_id: uuid.UUID,
request: AdvisorRecommendationCreate,
svc: Service,
_admin: Admin,
) -> AdvisorRecommendationResponse:
return svc.recommend(comparison_id, request)
@router.get("/recommendations", response_model=list[AdvisorRecommendationResponse])
def recommendations(svc: Service) -> list[AdvisorRecommendationResponse]:
return svc.recommendations()
@router.get("/recommendations/{recommendation_id}", response_model=AdvisorRecommendationResponse)
def recommendation(recommendation_id: uuid.UUID, svc: Service) -> AdvisorRecommendationResponse:
return svc.recommendation(recommendation_id)
@router.post(
"/recommendations/{recommendation_id}/dismiss",
response_model=AdvisorRecommendationResponse,
)
def dismiss_recommendation(
recommendation_id: uuid.UUID,
request: AdvisorRecommendationDismiss,
svc: Service,
_admin: Admin,
) -> AdvisorRecommendationResponse:
return svc.dismiss_recommendation(recommendation_id, request)
@router.get("/admin/advisor-policies/current", response_model=AdvisorPolicyResponse)
def advisor_policy(svc: Service, _admin: Admin) -> AdvisorPolicyResponse:
return svc.advisor_policy()
@router.patch("/admin/advisor-policies/current", response_model=AdvisorPolicyResponse)
def update_advisor_policy(
request: AdvisorPolicyUpdate, svc: Service, _admin: Admin
) -> AdvisorPolicyResponse:
return svc.update_advisor_policy(request)
@router.get("/embedding-migrations", response_model=list[EmbeddingMigrationResponse])
def migrations(svc: Service) -> list[EmbeddingMigrationResponse]:
return svc.migrations()
@router.post(
"/projects/{project_id}/embedding-migrations",
response_model=EmbeddingMigrationResponse,
status_code=status.HTTP_201_CREATED,
)
def create_migration(
project_id: uuid.UUID, request: EmbeddingMigrationCreate, svc: Service, _admin: Admin
) -> EmbeddingMigrationResponse:
return svc.create_migration(project_id, request)
@router.get("/embedding-migrations/{migration_id}", response_model=EmbeddingMigrationResponse)
def migration(migration_id: uuid.UUID, svc: Service) -> EmbeddingMigrationResponse:
return svc.migration(migration_id)
@router.post(
"/embedding-migrations/{migration_id}/state", response_model=EmbeddingMigrationResponse
)
def update_migration(
migration_id: uuid.UUID, request: MigrationUpdate, svc: Service, _admin: Admin
) -> EmbeddingMigrationResponse:
return svc.update_migration(migration_id, request)
@router.post(
"/embedding-migrations/{migration_id}/start", response_model=EmbeddingMigrationResponse
)
def start_migration(
migration_id: uuid.UUID, svc: Service, _admin: Admin
) -> EmbeddingMigrationResponse:
return svc.start_migration(migration_id)
@router.post(
"/embedding-migrations/{migration_id}/cancel", response_model=EmbeddingMigrationResponse
)
def cancel_migration(
migration_id: uuid.UUID, svc: Service, _admin: Admin
) -> EmbeddingMigrationResponse:
return svc.cancel_migration(migration_id)
@@ -0,0 +1,82 @@
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import require_operator, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.hardware import AcceleratorState, HardwareState, NodeState
from modelforge_api.services.hardware_factory import build_hardware_service
from modelforge_api.services.hardware_inventory import HardwareInventoryService, HardwareRefreshBusy
from modelforge_api.settings import Settings, get_settings
router = APIRouter(
prefix="/api/v1/hardware", tags=["hardware"], dependencies=[Depends(require_viewer)]
)
def get_hardware_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> HardwareInventoryService:
return build_hardware_service(session, settings)
Service = Annotated[HardwareInventoryService, Depends(get_hardware_service)]
@router.get("", response_model=HardwareState)
def hardware_overview(service: Service) -> HardwareState:
return service.state()
@router.get("/nodes", response_model=list[NodeState])
def list_nodes(service: Service) -> list[NodeState]:
return service.state().nodes
@router.get("/nodes/{node_id}", response_model=NodeState)
def get_node(node_id: uuid.UUID, service: Service) -> NodeState:
node = next((item for item in service.state().nodes if item.id == str(node_id)), None)
if node is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="compute node not found")
return node
@router.get("/accelerators", response_model=list[AcceleratorState])
def list_accelerators(service: Service) -> list[AcceleratorState]:
return [accelerator for node in service.state().nodes for accelerator in node.accelerators]
@router.get("/accelerators/{accelerator_id}", response_model=AcceleratorState)
def get_accelerator(accelerator_id: uuid.UUID, service: Service) -> AcceleratorState:
accelerator = next(
(
item
for node in service.state().nodes
for item in node.accelerators
if item.id == str(accelerator_id)
),
None,
)
if accelerator is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="accelerator not found")
return accelerator
@router.post(
"/refresh",
response_model=HardwareState,
dependencies=[Depends(require_operator)],
)
def refresh_hardware(service: Service) -> HardwareState:
try:
return service.refresh()
except HardwareRefreshBusy as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"hardware inventory failed: {type(exc).__name__}",
) from exc
@@ -0,0 +1,109 @@
from fastapi import APIRouter, Depends
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from modelforge_api import __version__
from modelforge_api.api.authorization import require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.enums import HealthStatus
from modelforge_api.domain.release import (
CURRENT_AGENT_PROTOCOL_VERSION,
MINIMUM_POSTGRES_MAJOR,
MINIMUM_UPGRADE_SOURCE,
PRODUCT_NAME,
RELEASE_CHANNEL,
SUPPORTED_AGENT_PROTOCOL_VERSIONS,
SUPPORTED_SCHEMA_REVISIONS,
TARGET_SCHEMA_REVISION,
build_identity,
)
from modelforge_api.domain.schemas import (
HealthResponse,
ReadinessResponse,
ReleaseCompatibility,
ReleaseInfo,
SystemMetadata,
)
from modelforge_api.persistence.models import CapabilityDeployment
from modelforge_api.services.manifest_registry import ManifestRegistry, get_manifest_registry
from modelforge_api.settings import Settings, get_settings
router = APIRouter(prefix="/api/v1", tags=["system"])
@router.get("/health/live", response_model=HealthResponse)
def liveness() -> HealthResponse:
return HealthResponse(version=__version__)
@router.get("/health/ready", response_model=ReadinessResponse)
def readiness(registry: ManifestRegistry = Depends(get_manifest_registry)) -> ReadinessResponse:
registry.capabilities()
registry.projects()
registry.candidates()
registry.benchmarks()
registry.policies()
return ReadinessResponse(
status=HealthStatus.HEALTHY,
checks={"manifests": HealthStatus.HEALTHY},
version=__version__,
)
@router.get(
"/system",
response_model=SystemMetadata,
dependencies=[Depends(require_viewer)],
)
def system_metadata(
settings: Settings = Depends(get_settings),
session: Session = Depends(get_session),
) -> SystemMetadata:
production_count = int(
session.scalar(
select(func.count(CapabilityDeployment.id)).where(
CapabilityDeployment.status == "stable",
CapabilityDeployment.production.is_(True),
)
)
or 0
)
return SystemMetadata(
name=PRODUCT_NAME,
version=__version__,
environment=settings.env,
release_channel=RELEASE_CHANNEL,
production_inference_available=production_count > 0,
)
@router.get("/version", response_model=ReleaseInfo)
def release_info(settings: Settings = Depends(get_settings)) -> ReleaseInfo:
"""Build identity and compatibility for the running process.
Unauthenticated on purpose: an operator diagnosing a deployment needs to know which build is
answering before they have credentials for it, and everything here is already implied by the
image they are running. Nothing configuration-derived or sensitive is exposed.
"""
identity = build_identity(
source_commit=settings.build_commit,
built_at=settings.build_timestamp,
image_digest=settings.build_image_digest,
)
return ReleaseInfo(
name=PRODUCT_NAME,
version=identity.version,
release_channel=identity.channel,
source_commit=identity.source_commit,
built_at=identity.built_at,
image_digest=identity.image_digest,
compatibility=ReleaseCompatibility(
schema_revision=TARGET_SCHEMA_REVISION,
supported_schema_revisions=list(SUPPORTED_SCHEMA_REVISIONS),
agent_protocol_version=CURRENT_AGENT_PROTOCOL_VERSION,
supported_agent_protocol_versions=list(SUPPORTED_AGENT_PROTOCOL_VERSIONS),
minimum_upgrade_source=MINIMUM_UPGRADE_SOURCE,
minimum_postgres_major=MINIMUM_POSTGRES_MAJOR,
),
)
@@ -0,0 +1,248 @@
from __future__ import annotations
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.lifecycle_contracts import (
ApprovalDecision,
ApprovalPolicyCreate,
ApprovalPolicyResponse,
ApprovalRequestCreate,
ApprovalRequestResponse,
CanaryObservation,
CleanupExecutionCreate,
CleanupPlanCreate,
CleanupPlanResponse,
LifecycleEventResponse,
LifecycleOperationResponse,
LifecycleSubjectCreate,
LifecycleSubjectResponse,
PlanExecutionCreate,
PromotionPlanCreate,
PromotionPlanResponse,
RetentionPolicyCreate,
RetentionPolicyResponse,
)
from modelforge_api.services.lifecycle import LifecycleService
router = APIRouter(
prefix="/api/v1/admin/lifecycle",
tags=["lifecycle"],
dependencies=[Depends(require_viewer)],
)
def get_lifecycle_service(
session: Annotated[Session, Depends(get_session)],
) -> LifecycleService:
return LifecycleService(session)
Service = Annotated[LifecycleService, Depends(get_lifecycle_service)]
@router.get("/approval-policies", response_model=list[ApprovalPolicyResponse])
def approval_policies(service: Service, _admin: Admin) -> list[ApprovalPolicyResponse]:
return service.policies()
@router.post(
"/approval-policies",
response_model=ApprovalPolicyResponse,
status_code=status.HTTP_201_CREATED,
)
def create_approval_policy(
request: ApprovalPolicyCreate, service: Service, _admin: Admin
) -> ApprovalPolicyResponse:
return service.create_policy(request)
@router.get("/subjects", response_model=list[LifecycleSubjectResponse])
def subjects(service: Service, _admin: Admin) -> list[LifecycleSubjectResponse]:
return service.subjects()
@router.post(
"/subjects",
response_model=LifecycleSubjectResponse,
status_code=status.HTTP_201_CREATED,
)
def create_subject(
request: LifecycleSubjectCreate, service: Service, _admin: Admin
) -> LifecycleSubjectResponse:
return service.create_subject(request)
@router.get("/approval-requests", response_model=list[ApprovalRequestResponse])
def approval_requests(service: Service, _admin: Admin) -> list[ApprovalRequestResponse]:
return service.approval_requests()
@router.post(
"/approval-requests",
response_model=ApprovalRequestResponse,
status_code=status.HTTP_201_CREATED,
)
def request_approval(
request: ApprovalRequestCreate, service: Service, _admin: Admin
) -> ApprovalRequestResponse:
return service.request_approval(request)
@router.get("/approval-requests/{approval_id}", response_model=ApprovalRequestResponse)
def approval_request(
approval_id: uuid.UUID, service: Service, _admin: Admin
) -> ApprovalRequestResponse:
return service.approval(approval_id)
@router.post("/approval-requests/{approval_id}/approve", response_model=ApprovalRequestResponse)
def approve_request(
approval_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> ApprovalRequestResponse:
return service.decide_approval(approval_id, request, "approve")
@router.post("/approval-requests/{approval_id}/reject", response_model=ApprovalRequestResponse)
def reject_request(
approval_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> ApprovalRequestResponse:
return service.decide_approval(approval_id, request, "reject")
@router.post("/approval-requests/{approval_id}/revoke", response_model=ApprovalRequestResponse)
def revoke_request(
approval_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> ApprovalRequestResponse:
return service.decide_approval(approval_id, request, "revoke")
@router.get("/promotion-plans", response_model=list[PromotionPlanResponse])
def promotion_plans(service: Service, _admin: Admin) -> list[PromotionPlanResponse]:
return service.plans()
@router.post(
"/promotion-plans",
response_model=PromotionPlanResponse,
status_code=status.HTTP_201_CREATED,
)
def create_promotion_plan(
request: PromotionPlanCreate, service: Service, _admin: Admin
) -> PromotionPlanResponse:
return service.create_plan(request)
@router.get("/promotion-plans/{plan_id}", response_model=PromotionPlanResponse)
def promotion_plan(plan_id: uuid.UUID, service: Service, _admin: Admin) -> PromotionPlanResponse:
return service.plan(plan_id)
@router.post("/promotion-plans/{plan_id}/approve", response_model=PromotionPlanResponse)
def approve_promotion_plan(
plan_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> PromotionPlanResponse:
return service.approve_plan(plan_id, request)
@router.post("/promotion-plans/{plan_id}/execute", response_model=LifecycleOperationResponse)
def execute_promotion_plan(
plan_id: uuid.UUID, request: PlanExecutionCreate, service: Service, _admin: Admin
) -> LifecycleOperationResponse:
return service.execute_plan(plan_id, request)
@router.get("/operations", response_model=list[LifecycleOperationResponse])
def operations(service: Service, _admin: Admin) -> list[LifecycleOperationResponse]:
return service.operations()
@router.get("/operations/{operation_id}", response_model=LifecycleOperationResponse)
def operation(
operation_id: uuid.UUID, service: Service, _admin: Admin
) -> LifecycleOperationResponse:
return service.operation(operation_id)
@router.post("/operations/{operation_id}/canary", response_model=LifecycleOperationResponse)
def observe_canary(
operation_id: uuid.UUID, request: CanaryObservation, service: Service, _admin: Admin
) -> LifecycleOperationResponse:
return service.observe_canary(operation_id, request)
@router.post("/operations/{operation_id}/commit", response_model=LifecycleOperationResponse)
def commit_operation(
operation_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> LifecycleOperationResponse:
return service.commit_operation(operation_id, request)
@router.post("/operations/{operation_id}/rollback", response_model=LifecycleOperationResponse)
def rollback_operation(
operation_id: uuid.UUID, request: ApprovalDecision, service: Service, _admin: Admin
) -> LifecycleOperationResponse:
return service.rollback_operation(operation_id, request)
@router.post("/reconcile", response_model=dict[str, int])
def reconcile(service: Service, _admin: Admin) -> dict[str, int]:
return {"reconciled_operations": service.reconcile_incomplete()}
@router.get("/retention-policies", response_model=list[RetentionPolicyResponse])
def retention_policies(service: Service, _admin: Admin) -> list[RetentionPolicyResponse]:
return service.retention_policies()
@router.post(
"/retention-policies",
response_model=RetentionPolicyResponse,
status_code=status.HTTP_201_CREATED,
)
def create_retention_policy(
request: RetentionPolicyCreate, service: Service, _admin: Admin
) -> RetentionPolicyResponse:
return service.create_retention_policy(request)
@router.get("/cleanup-plans", response_model=list[CleanupPlanResponse])
def cleanup_plans(service: Service, _admin: Admin) -> list[CleanupPlanResponse]:
return service.cleanup_plans()
@router.post(
"/cleanup-plans",
response_model=CleanupPlanResponse,
status_code=status.HTTP_201_CREATED,
)
def create_cleanup_plan(
request: CleanupPlanCreate, service: Service, _admin: Admin
) -> CleanupPlanResponse:
return service.create_cleanup_plan(request)
@router.get("/cleanup-plans/{plan_id}", response_model=CleanupPlanResponse)
def cleanup_plan(plan_id: uuid.UUID, service: Service, _admin: Admin) -> CleanupPlanResponse:
return service.cleanup_plan(plan_id)
@router.post("/cleanup-plans/{plan_id}/execute", response_model=CleanupPlanResponse)
def execute_cleanup_plan(
plan_id: uuid.UUID, request: CleanupExecutionCreate, service: Service, _admin: Admin
) -> CleanupPlanResponse:
return service.execute_cleanup(plan_id, request)
@router.get("/events", response_model=list[LifecycleEventResponse])
def lifecycle_events(
service: Service,
_admin: Admin,
limit: Annotated[int, Query(ge=1, le=1000)] = 200,
) -> list[LifecycleEventResponse]:
return service.events(limit)
@@ -0,0 +1,191 @@
from __future__ import annotations
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.migration_contracts import (
BatchReport,
CutoverOperationResponse,
CutoverPrepare,
CutoverReport,
MigrationBatchResponse,
MigrationEventResponse,
MigrationPlanCreate,
MigrationPlanResponse,
MigrationValidationPolicyCreate,
MigrationValidationPolicyResponse,
PreflightReport,
ReconciliationReport,
RollbackReport,
ShadowReport,
StateAction,
ValidationReport,
ValidationSnapshotResponse,
)
from modelforge_api.services.migration_engine import MigrationEngineService
router = APIRouter(
prefix="/api/v1/admin/migrations",
tags=["migrations"],
dependencies=[Depends(require_viewer)],
)
def get_service(session: Annotated[Session, Depends(get_session)]) -> MigrationEngineService:
return MigrationEngineService(session)
Service = Annotated[MigrationEngineService, Depends(get_service)]
@router.get("/validation-policies", response_model=list[MigrationValidationPolicyResponse])
def validation_policies(service: Service, _admin: Admin) -> list[MigrationValidationPolicyResponse]:
return service.validation_policies()
@router.post(
"/validation-policies",
response_model=MigrationValidationPolicyResponse,
status_code=status.HTTP_201_CREATED,
)
def create_validation_policy(
request: MigrationValidationPolicyCreate, service: Service, _admin: Admin
) -> MigrationValidationPolicyResponse:
return service.create_validation_policy(request)
@router.get("/plans", response_model=list[MigrationPlanResponse])
def plans(service: Service, _admin: Admin) -> list[MigrationPlanResponse]:
return service.plans()
@router.post("/plans", response_model=MigrationPlanResponse, status_code=status.HTTP_201_CREATED)
def create_plan(
request: MigrationPlanCreate, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.create_plan(request)
@router.get("/plans/{plan_id}", response_model=MigrationPlanResponse)
def plan(plan_id: uuid.UUID, service: Service, _admin: Admin) -> MigrationPlanResponse:
return service.plan(plan_id)
@router.post("/plans/{plan_id}/preflight", response_model=MigrationPlanResponse)
def preflight(
plan_id: uuid.UUID, request: PreflightReport, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.preflight(plan_id, request)
@router.post("/plans/{plan_id}/backfill/start", response_model=MigrationPlanResponse)
def start_backfill(
plan_id: uuid.UUID, request: StateAction, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.start_backfill(plan_id, request)
@router.post("/plans/{plan_id}/backfill/pause", response_model=MigrationPlanResponse)
def pause_backfill(
plan_id: uuid.UUID, request: StateAction, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.pause_backfill(plan_id, request)
@router.post("/plans/{plan_id}/batches", response_model=MigrationBatchResponse)
def record_batch(
plan_id: uuid.UUID, request: BatchReport, service: Service, _admin: Admin
) -> MigrationBatchResponse:
return service.record_batch(plan_id, request)
@router.get("/plans/{plan_id}/batches", response_model=list[MigrationBatchResponse])
def batches(plan_id: uuid.UUID, service: Service, _admin: Admin) -> list[MigrationBatchResponse]:
return service.batches(plan_id)
@router.post("/plans/{plan_id}/validation", response_model=ValidationSnapshotResponse)
def validate_target(
plan_id: uuid.UUID, request: ValidationReport, service: Service, _admin: Admin
) -> ValidationSnapshotResponse:
return service.validate(plan_id, request)
@router.get("/plans/{plan_id}/validation", response_model=list[ValidationSnapshotResponse])
def validation_snapshots(
plan_id: uuid.UUID, service: Service, _admin: Admin
) -> list[ValidationSnapshotResponse]:
return service.validation_snapshots(plan_id)
@router.post("/plans/{plan_id}/shadow/start", response_model=MigrationPlanResponse)
def start_shadow(
plan_id: uuid.UUID, request: StateAction, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.start_shadow(plan_id, request)
@router.post("/plans/{plan_id}/shadow/complete", response_model=MigrationPlanResponse)
def complete_shadow(
plan_id: uuid.UUID, request: ShadowReport, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.complete_shadow(plan_id, request)
@router.post("/plans/{plan_id}/cutover/prepare", response_model=CutoverOperationResponse)
def prepare_cutover(
plan_id: uuid.UUID, request: CutoverPrepare, service: Service, _admin: Admin
) -> CutoverOperationResponse:
return service.prepare_cutover(plan_id, request)
@router.post("/plans/{plan_id}/cutover/report", response_model=CutoverOperationResponse)
def report_cutover(
plan_id: uuid.UUID, request: CutoverReport, service: Service, _admin: Admin
) -> CutoverOperationResponse:
return service.report_cutover(plan_id, request)
@router.post("/plans/{plan_id}/rollback", response_model=CutoverOperationResponse)
def rollback(
plan_id: uuid.UUID, request: RollbackReport, service: Service, _admin: Admin
) -> CutoverOperationResponse:
return service.rollback(plan_id, request)
@router.post("/reconcile", response_model=CutoverOperationResponse)
def reconcile(
request: ReconciliationReport, service: Service, _admin: Admin
) -> CutoverOperationResponse:
return service.reconcile(request)
@router.post("/plans/{plan_id}/cancel", response_model=MigrationPlanResponse)
def cancel(
plan_id: uuid.UUID, request: StateAction, service: Service, _admin: Admin
) -> MigrationPlanResponse:
return service.cancel(plan_id, request)
@router.get("/cutovers", response_model=list[CutoverOperationResponse])
def cutovers(
service: Service,
_admin: Admin,
plan_id: Annotated[uuid.UUID | None, Query()] = None,
) -> list[CutoverOperationResponse]:
return service.operations(plan_id)
@router.get("/events", response_model=list[MigrationEventResponse])
def events(
service: Service,
_admin: Admin,
plan_id: Annotated[uuid.UUID | None, Query()] = None,
limit: Annotated[int, Query(ge=1, le=1000)] = 200,
) -> list[MigrationEventResponse]:
return service.events(plan_id, limit)
@@ -0,0 +1,237 @@
from __future__ import annotations
import uuid
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Query, Response, status
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.observability import (
AlertAction,
AlertHistoryResponse,
AlertResponse,
AlertRuleCreate,
AlertRuleResponse,
CapacitySnapshotResponse,
IncidentResponse,
MaintenanceWindowCreate,
MaintenanceWindowResponse,
OperationsOverview,
SLIDefinitionResponse,
SLOEvaluationResponse,
SLOPolicyCreate,
SLOPolicyResponse,
TrendResponse,
metrics,
)
from modelforge_api.persistence.models import (
AlertHistoryEvent,
IncidentTimelineEvent,
OperationalIncident,
)
from modelforge_api.services.observability import ObservabilityService
router = APIRouter(tags=["operations"], dependencies=[Depends(require_viewer)])
def get_service(session: Annotated[Session, Depends(get_session)]) -> ObservabilityService:
return ObservabilityService(session)
Service = Annotated[ObservabilityService, Depends(get_service)]
@router.get("/metrics", response_class=Response)
def prometheus_metrics(service: Service, _admin: Admin) -> Response:
"""Admin-isolated Prometheus exposition; process metrics survive DB failure."""
try:
content = service.prometheus()
metrics.gauge("modelforge_observability_degraded", {}, 0)
except SQLAlchemyError:
service.session.rollback()
metrics.gauge("modelforge_observability_degraded", {}, 1)
content = metrics.render()
return Response(content=content, media_type="text/plain; version=0.0.4; charset=utf-8")
@router.get("/api/v1/admin/operations/overview", response_model=OperationsOverview)
def overview(service: Service, _admin: Admin) -> OperationsOverview:
return service.overview()
@router.get(
"/api/v1/admin/operations/slis", response_model=list[SLIDefinitionResponse]
)
def sli_definitions(service: Service, _admin: Admin) -> list[SLIDefinitionResponse]:
return service.definitions()
@router.get(
"/api/v1/admin/operations/slo-policies", response_model=list[SLOPolicyResponse]
)
def slo_policies(service: Service, _admin: Admin) -> list[SLOPolicyResponse]:
return service.policies()
@router.post(
"/api/v1/admin/operations/slo-policies",
response_model=SLOPolicyResponse,
status_code=status.HTTP_201_CREATED,
)
def create_slo_policy(
request: SLOPolicyCreate, service: Service, _admin: Admin
) -> SLOPolicyResponse:
return service.create_policy(request)
@router.get(
"/api/v1/admin/operations/slo-evaluations", response_model=list[SLOEvaluationResponse]
)
def slo_evaluations(
service: Service, _admin: Admin, limit: int = Query(default=100, ge=1, le=1000)
) -> list[SLOEvaluationResponse]:
return service.evaluations(limit)
@router.post(
"/api/v1/admin/operations/slo-evaluations/run",
response_model=list[SLOEvaluationResponse],
)
def evaluate_slos(service: Service, _admin: Admin) -> list[SLOEvaluationResponse]:
return service.evaluate_slos()
@router.get(
"/api/v1/admin/operations/alert-rules", response_model=list[AlertRuleResponse]
)
def alert_rules(service: Service, _admin: Admin) -> list[AlertRuleResponse]:
return service.rules()
@router.post(
"/api/v1/admin/operations/alert-rules",
response_model=AlertRuleResponse,
status_code=status.HTTP_201_CREATED,
)
def create_alert_rule(
request: AlertRuleCreate, service: Service, _admin: Admin
) -> AlertRuleResponse:
return service.create_rule(request)
@router.get("/api/v1/admin/operations/alerts", response_model=list[AlertResponse])
def alerts(
service: Service, _admin: Admin, limit: int = Query(default=200, ge=1, le=1000)
) -> list[AlertResponse]:
return service.alerts(limit)
@router.post("/api/v1/admin/operations/alerts/evaluate", response_model=list[AlertResponse])
def evaluate_alerts(service: Service, _admin: Admin) -> list[AlertResponse]:
return service.evaluate_alerts()
@router.post(
"/api/v1/admin/operations/alerts/{alert_id}/acknowledge", response_model=AlertResponse
)
def acknowledge_alert(
alert_id: uuid.UUID, request: AlertAction, service: Service, _admin: Admin
) -> AlertResponse:
return service.acknowledge(alert_id, request)
@router.get(
"/api/v1/admin/operations/alerts/{alert_id}/history",
response_model=list[AlertHistoryResponse],
)
def alert_history(
alert_id: uuid.UUID, service: Service, _admin: Admin
) -> list[AlertHistoryResponse]:
rows = service.session.scalars(
select(AlertHistoryEvent)
.where(AlertHistoryEvent.alert_id == alert_id)
.order_by(AlertHistoryEvent.occurred_at)
)
return [AlertHistoryResponse.model_validate(item) for item in rows]
@router.get(
"/api/v1/admin/operations/maintenance-windows",
response_model=list[MaintenanceWindowResponse],
)
def maintenance_windows(service: Service, _admin: Admin) -> list[MaintenanceWindowResponse]:
return service.maintenance_windows()
@router.post(
"/api/v1/admin/operations/maintenance-windows",
response_model=MaintenanceWindowResponse,
status_code=status.HTTP_201_CREATED,
)
def create_maintenance_window(
request: MaintenanceWindowCreate, service: Service, _admin: Admin
) -> MaintenanceWindowResponse:
return service.create_maintenance_window(request)
@router.get("/api/v1/admin/operations/capacity", response_model=list[CapacitySnapshotResponse])
def capacity(
service: Service, _admin: Admin, limit: int = Query(default=500, ge=1, le=5000)
) -> list[CapacitySnapshotResponse]:
return service.capacity(limit)
@router.post(
"/api/v1/admin/operations/capacity/collect", response_model=list[CapacitySnapshotResponse]
)
def collect_capacity(service: Service, _admin: Admin) -> list[CapacitySnapshotResponse]:
return service.collect_capacity()
@router.get("/api/v1/admin/operations/capacity/{node_id}/trend", response_model=TrendResponse)
def capacity_trend(
node_id: uuid.UUID,
service: Service,
_admin: Admin,
hours: int = Query(default=24, ge=1, le=2160),
) -> TrendResponse:
return service.capacity_trend(node_id, hours)
@router.post("/api/v1/admin/operations/retention/run", response_model=dict[str, int])
def apply_retention(service: Service, _admin: Admin) -> dict[str, int]:
return service.apply_retention()
@router.get("/api/v1/admin/operations/incidents", response_model=list[IncidentResponse])
def incidents(service: Service, _admin: Admin) -> list[IncidentResponse]:
rows = service.session.scalars(
select(OperationalIncident).order_by(OperationalIncident.last_seen_at.desc())
)
return [IncidentResponse.model_validate(item) for item in rows]
@router.get("/api/v1/admin/operations/incidents/{incident_id}/timeline")
def incident_timeline(
incident_id: uuid.UUID, service: Service, _admin: Admin
) -> list[dict[str, Any]]:
rows = service.session.scalars(
select(IncidentTimelineEvent)
.where(IncidentTimelineEvent.incident_id == incident_id)
.order_by(IncidentTimelineEvent.occurred_at)
)
return [
{
"id": str(item.id),
"alert_id": str(item.alert_id) if item.alert_id else None,
"event_type": item.event_type,
"relation": item.relation,
"summary": item.summary,
"occurred_at": item.occurred_at,
}
for item in rows
]
@@ -0,0 +1,215 @@
"""Operator-only recovery API.
Backup creation, verification and restore *planning* are safe control-plane operations. Restore
*execution* is deliberately restricted: it can only run against an isolated destination that is
not this control plane's own database, and replacing a production database stays an operator
runbook/CLI action rather than a remote call.
"""
from __future__ import annotations
import uuid
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.recovery import (
ArtifactRecoveryCreate,
ArtifactRecoveryResponse,
BackupCapacityEstimate,
BackupSetCreate,
BackupSetResponse,
RecoveryAssetResponse,
RecoveryDashboard,
RecoveryPolicyCreate,
RecoveryPolicyResponse,
RestoreAdvanceRequest,
RestoreOperationEventResponse,
RestoreOperationResponse,
RestorePlanCreate,
RestorePlanResponse,
)
from modelforge_api.services.recovery import RecoveryService
from modelforge_api.settings import Settings, get_settings
router = APIRouter(
prefix="/api/v1/admin/recovery",
tags=["recovery"],
dependencies=[Depends(require_viewer)],
)
def get_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> RecoveryService:
return RecoveryService(session, settings)
Service = Annotated[RecoveryService, Depends(get_service)]
@router.get("/dashboard", response_model=RecoveryDashboard)
def dashboard(service: Service, _admin: Admin) -> RecoveryDashboard:
return service.dashboard()
@router.get("/policies", response_model=list[RecoveryPolicyResponse])
def policies(service: Service, _admin: Admin) -> list[RecoveryPolicyResponse]:
return service.policies()
@router.post(
"/policies",
response_model=RecoveryPolicyResponse,
status_code=status.HTTP_201_CREATED,
)
def create_policy(
request: RecoveryPolicyCreate, service: Service, _admin: Admin
) -> RecoveryPolicyResponse:
return service.create_policy(request)
@router.get("/assets", response_model=list[RecoveryAssetResponse])
def assets(service: Service, _admin: Admin) -> list[RecoveryAssetResponse]:
return service.assets()
@router.get("/capacity", response_model=BackupCapacityEstimate)
def capacity(service: Service, _admin: Admin) -> BackupCapacityEstimate:
return service.estimate_capacity()
@router.get("/backups", response_model=list[BackupSetResponse])
def backups(
service: Service, _admin: Admin, limit: int = Query(default=100, ge=1, le=1000)
) -> list[BackupSetResponse]:
return service.backups(limit)
@router.post(
"/backups", response_model=BackupSetResponse, status_code=status.HTTP_201_CREATED
)
def create_backup(
request: BackupSetCreate, service: Service, _admin: Admin
) -> BackupSetResponse:
return service.create_backup(request)
@router.get("/backups/{backup_set_id}", response_model=BackupSetResponse)
def backup(backup_set_id: uuid.UUID, service: Service, _admin: Admin) -> BackupSetResponse:
return service.backup(backup_set_id)
@router.post("/backups/{backup_set_id}/verify", response_model=BackupSetResponse)
def verify_backup(
backup_set_id: uuid.UUID, service: Service, _admin: Admin
) -> BackupSetResponse:
return service.verify_backup(backup_set_id)
@router.post("/retention/run", response_model=dict[str, int])
def apply_retention(service: Service, _admin: Admin) -> dict[str, int]:
return service.apply_retention()
@router.get("/restore-plans", response_model=list[RestorePlanResponse])
def restore_plans(
service: Service, _admin: Admin, limit: int = Query(default=100, ge=1, le=1000)
) -> list[RestorePlanResponse]:
return service.restore_plans(limit)
@router.post(
"/restore-plans", response_model=RestorePlanResponse, status_code=status.HTTP_201_CREATED
)
def create_restore_plan(
request: RestorePlanCreate, service: Service, _admin: Admin
) -> RestorePlanResponse:
return service.create_restore_plan(request)
@router.get("/restore-plans/{plan_id}", response_model=RestorePlanResponse)
def restore_plan(plan_id: uuid.UUID, service: Service, _admin: Admin) -> RestorePlanResponse:
return service.restore_plan(plan_id)
@router.post("/restore-plans/{plan_id}/preflight", response_model=RestorePlanResponse)
def preflight(plan_id: uuid.UUID, service: Service, _admin: Admin) -> RestorePlanResponse:
return service.preflight(plan_id)
@router.post(
"/restore-plans/{plan_id}/start",
response_model=RestoreOperationResponse,
status_code=status.HTTP_201_CREATED,
)
def start_restore(
plan_id: uuid.UUID,
request: RestoreAdvanceRequest,
service: Service,
_admin: Admin,
) -> RestoreOperationResponse:
return service.start_restore(plan_id, request)
@router.get("/restore-operations", response_model=list[RestoreOperationResponse])
def restore_operations(
service: Service, _admin: Admin, limit: int = Query(default=100, ge=1, le=1000)
) -> list[RestoreOperationResponse]:
return service.restore_operations(limit)
@router.get("/restore-operations/{operation_id}", response_model=RestoreOperationResponse)
def restore_operation(
operation_id: uuid.UUID, service: Service, _admin: Admin
) -> RestoreOperationResponse:
return service.restore_operation(operation_id)
@router.post("/restore-operations/{operation_id}/advance", response_model=RestoreOperationResponse)
def advance_restore(
operation_id: uuid.UUID,
request: RestoreAdvanceRequest,
service: Service,
_admin: Admin,
) -> RestoreOperationResponse:
return service.advance_restore(operation_id, request)
@router.get(
"/restore-operations/{operation_id}/events",
response_model=list[RestoreOperationEventResponse],
)
def restore_events(
operation_id: uuid.UUID, service: Service, _admin: Admin
) -> list[RestoreOperationEventResponse]:
return service.restore_events(operation_id)
@router.get("/artifact-recoveries", response_model=list[ArtifactRecoveryResponse])
def artifact_recoveries(
service: Service, _admin: Admin, limit: int = Query(default=100, ge=1, le=1000)
) -> list[ArtifactRecoveryResponse]:
return service.artifact_recoveries(limit)
@router.post(
"/artifact-recoveries",
response_model=ArtifactRecoveryResponse,
status_code=status.HTTP_201_CREATED,
)
def plan_artifact_recovery(
request: ArtifactRecoveryCreate, service: Service, _admin: Admin
) -> ArtifactRecoveryResponse:
return service.plan_artifact_recovery(request)
@router.get("/fingerprint", response_model=dict[str, Any])
def fingerprint(service: Service, _admin: Admin) -> dict[str, Any]:
"""Bounded semantic fingerprint of the live control plane; contains no secret material."""
return service.fingerprint()
@@ -0,0 +1,291 @@
from __future__ import annotations
import math
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query, Response, status
from pydantic import BaseModel
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import require_admin, require_operator, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.registry import (
ArtifactCreate,
ArtifactResponse,
CapacityDecision,
DerivedArtifactCreate,
DerivedArtifactResponse,
ModelCreate,
ModelResponse,
ModelUpdate,
Page,
RevisionCreate,
RevisionResponse,
StorageRootCreate,
StorageRootObservation,
StorageRootResponse,
StorageRootUpdate,
VerifyResponse,
)
from modelforge_api.services.registry import RegistryService
router = APIRouter(
prefix="/api/v1", tags=["model-registry"], dependencies=[Depends(require_viewer)]
)
def get_registry_service(session: Annotated[Session, Depends(get_session)]) -> RegistryService:
return RegistryService(session)
Service = Annotated[RegistryService, Depends(get_registry_service)]
def page_values[T: BaseModel](items: list[T], page: int, page_size: int) -> Page[T]:
total = len(items)
start = (page - 1) * page_size
return Page(
items=items[start : start + page_size],
page=page,
page_size=page_size,
total=total,
pages=math.ceil(total / page_size) if total else 0,
)
@router.get("/models", response_model=Page[ModelResponse])
def list_models(
service: Service,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=100)] = 20,
search: str | None = None,
lifecycle: str | None = None,
source_type: str | None = None,
) -> Page[ModelResponse]:
return service.list_models(
page=page,
page_size=page_size,
search=search,
lifecycle=lifecycle,
source_type=source_type,
)
@router.post(
"/models",
response_model=ModelResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_operator)],
)
def create_model(request: ModelCreate, service: Service) -> ModelResponse:
return service.create_model(request)
@router.get("/models/{model_id}", response_model=ModelResponse)
def get_model(model_id: uuid.UUID, service: Service) -> ModelResponse:
return service.get_model(model_id)
@router.patch(
"/models/{model_id}",
response_model=ModelResponse,
dependencies=[Depends(require_operator)],
)
def update_model(model_id: uuid.UUID, request: ModelUpdate, service: Service) -> ModelResponse:
return service.update_model(model_id, request)
@router.post(
"/models/{model_id}/deprecate",
response_model=ModelResponse,
dependencies=[Depends(require_admin)],
)
def deprecate_model(model_id: uuid.UUID, service: Service) -> ModelResponse:
return service.deprecate_model(model_id)
@router.post(
"/models/{model_id}/archive",
response_model=ModelResponse,
dependencies=[Depends(require_admin)],
)
def archive_model(model_id: uuid.UUID, service: Service) -> ModelResponse:
return service.archive_model(model_id)
@router.delete(
"/models/{model_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_admin)],
)
def delete_model(model_id: uuid.UUID, service: Service) -> Response:
service.delete("model", model_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/models/{model_id}/revisions", response_model=Page[RevisionResponse])
def list_revisions(
model_id: uuid.UUID,
service: Service,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=100)] = 20,
) -> Page[RevisionResponse]:
return page_values(service.revisions(model_id), page, page_size)
@router.post(
"/models/{model_id}/revisions",
response_model=RevisionResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_operator)],
)
def create_revision(
model_id: uuid.UUID, request: RevisionCreate, service: Service
) -> RevisionResponse:
return service.create_revision(model_id, request)
@router.delete(
"/revisions/{revision_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_admin)],
)
def delete_revision(revision_id: uuid.UUID, service: Service) -> Response:
service.delete("model_revision", revision_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/revisions/{revision_id}/artifacts", response_model=Page[ArtifactResponse])
def list_artifacts(
revision_id: uuid.UUID,
service: Service,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=100)] = 20,
) -> Page[ArtifactResponse]:
return page_values(service.artifacts(revision_id), page, page_size)
@router.post(
"/revisions/{revision_id}/artifacts",
response_model=ArtifactResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_operator)],
)
def create_artifact(
revision_id: uuid.UUID, request: ArtifactCreate, service: Service
) -> ArtifactResponse:
return service.create_artifact(revision_id, request)
@router.delete(
"/artifacts/{artifact_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_admin)],
)
def delete_artifact(artifact_id: uuid.UUID, service: Service) -> Response:
service.delete("model_artifact", artifact_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/artifacts/{artifact_id}/verify",
response_model=VerifyResponse,
dependencies=[Depends(require_operator)],
)
def verify_artifact(
artifact_id: uuid.UUID, location_id: uuid.UUID, service: Service
) -> VerifyResponse:
return service.verify_artifact(artifact_id, location_id)
@router.get(
"/revisions/{revision_id}/derived-artifacts", response_model=Page[DerivedArtifactResponse]
)
def list_derived_artifacts(
revision_id: uuid.UUID,
service: Service,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=100)] = 20,
) -> Page[DerivedArtifactResponse]:
return page_values(service.derived(revision_id), page, page_size)
@router.post(
"/derived-artifacts",
response_model=DerivedArtifactResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_operator)],
)
def create_derived_artifact(
request: DerivedArtifactCreate, service: Service
) -> DerivedArtifactResponse:
return service.create_derived(request)
@router.delete(
"/derived-artifacts/{artifact_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_admin)],
)
def delete_derived_artifact(artifact_id: uuid.UUID, service: Service) -> Response:
service.delete("derived_artifact", artifact_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/storage-roots", response_model=list[StorageRootResponse])
def list_storage_roots(
service: Service, node_id: uuid.UUID | None = None
) -> list[StorageRootResponse]:
return service.storage_roots(node_id)
@router.post(
"/storage-roots",
response_model=StorageRootResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_admin)],
)
def create_storage_root(request: StorageRootCreate, service: Service) -> StorageRootResponse:
return service.create_storage_root(request)
@router.patch(
"/storage-roots/{root_id}",
response_model=StorageRootResponse,
dependencies=[Depends(require_admin)],
)
def update_storage_root(
root_id: uuid.UUID, request: StorageRootUpdate, service: Service
) -> StorageRootResponse:
return service.update_storage_root(root_id, request)
@router.post(
"/storage-roots/{root_id}/observations",
response_model=StorageRootResponse,
dependencies=[Depends(require_operator)],
)
def observe_storage_root(
root_id: uuid.UUID, request: StorageRootObservation, service: Service
) -> StorageRootResponse:
return service.observe_storage_root(root_id, request)
@router.get(
"/storage-roots/{root_id}/capacity",
response_model=CapacityDecision,
dependencies=[Depends(require_operator)],
)
def check_capacity(root_id: uuid.UUID, requested_bytes: int, service: Service) -> CapacityDecision:
return service.check_capacity(root_id, requested_bytes)
@router.delete(
"/storage-roots/{root_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_admin)],
)
def delete_storage_root(root_id: uuid.UUID, service: Service) -> Response:
service.delete("storage_root", root_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@@ -0,0 +1,187 @@
from __future__ import annotations
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import require_admin, require_operator, require_viewer
from modelforge_api.db import get_session
from modelforge_api.domain.runtime import (
CompatibilityAssessmentCreate,
CompatibilityAssessmentResponse,
DeploymentCandidateResponse,
ExecutionApprovalCreate,
ExecutionApprovalResponse,
RuntimeEnvironmentCreate,
RuntimeEnvironmentResponse,
RuntimeProbeCreate,
RuntimeProbeResponse,
RuntimeProfileCreate,
RuntimeProfileResponse,
)
from modelforge_api.services.runtime import RuntimeService
from modelforge_api.settings import Settings, get_settings
router = APIRouter(
prefix="/api/v1", tags=["runtime-plane"], dependencies=[Depends(require_viewer)]
)
def get_runtime_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
) -> RuntimeService:
return RuntimeService(session, settings)
Service = Annotated[RuntimeService, Depends(get_runtime_service)]
@router.post(
"/runtime-environments",
response_model=RuntimeEnvironmentResponse,
status_code=201,
dependencies=[Depends(require_operator)],
)
def create_environment(
request: RuntimeEnvironmentCreate, service: Service
) -> RuntimeEnvironmentResponse:
return service.create_environment(request)
@router.get("/runtime-environments", response_model=list[RuntimeEnvironmentResponse])
def environments(service: Service) -> list[RuntimeEnvironmentResponse]:
return service.environments()
@router.get("/runtime-environments/{environment_id}", response_model=RuntimeEnvironmentResponse)
def environment(environment_id: uuid.UUID, service: Service) -> RuntimeEnvironmentResponse:
return service.environment(environment_id)
@router.post(
"/runtime-profiles",
response_model=RuntimeProfileResponse,
status_code=201,
dependencies=[Depends(require_operator)],
)
def create_profile(request: RuntimeProfileCreate, service: Service) -> RuntimeProfileResponse:
return service.create_profile(request)
@router.get("/runtime-profiles", response_model=list[RuntimeProfileResponse])
def profiles(service: Service) -> list[RuntimeProfileResponse]:
return service.profiles()
@router.get("/runtime-profiles/{profile_id}", response_model=RuntimeProfileResponse)
def profile(profile_id: uuid.UUID, service: Service) -> RuntimeProfileResponse:
return service.profile(profile_id)
@router.post(
"/artifact-sets/{artifact_set_id}/compatibility-assessments",
response_model=CompatibilityAssessmentResponse,
status_code=201,
dependencies=[Depends(require_operator)],
)
def assess(
artifact_set_id: uuid.UUID,
request: CompatibilityAssessmentCreate,
service: Service,
) -> CompatibilityAssessmentResponse:
profile = service.repo.profile(request.runtime_profile_id)
if not profile or profile.artifact_set_id != artifact_set_id:
from modelforge_api.services.registry import RegistryConflict
raise RegistryConflict("runtime profile belongs to another artifact set")
return service.assess(request)
@router.get("/compatibility-assessments", response_model=list[CompatibilityAssessmentResponse])
def assessments(
service: Service,
artifact_set_id: Annotated[uuid.UUID | None, Query()] = None,
model_id: Annotated[uuid.UUID | None, Query()] = None,
compute_node_id: Annotated[uuid.UUID | None, Query()] = None,
runtime_profile_id: Annotated[uuid.UUID | None, Query()] = None,
status: Annotated[str | None, Query()] = None,
) -> list[CompatibilityAssessmentResponse]:
return service.assessments(
artifact_set_id=artifact_set_id,
model_id=model_id,
compute_node_id=compute_node_id,
runtime_profile_id=runtime_profile_id,
status=status,
)
@router.get(
"/compatibility-assessments/{assessment_id}",
response_model=CompatibilityAssessmentResponse,
)
def assessment(assessment_id: uuid.UUID, service: Service) -> CompatibilityAssessmentResponse:
return service.assessment(assessment_id)
@router.post(
"/artifact-sets/{artifact_set_id}/execution-approvals",
response_model=ExecutionApprovalResponse,
status_code=201,
dependencies=[Depends(require_admin)],
)
def approve_execution(
artifact_set_id: uuid.UUID,
request: ExecutionApprovalCreate,
service: Service,
) -> ExecutionApprovalResponse:
return service.approve(artifact_set_id, request)
@router.get("/execution-approvals", response_model=list[ExecutionApprovalResponse])
def approvals(
service: Service,
artifact_set_id: Annotated[uuid.UUID | None, Query()] = None,
) -> list[ExecutionApprovalResponse]:
return service.approvals(artifact_set_id)
@router.post(
"/runtime-probes",
response_model=RuntimeProbeResponse,
status_code=201,
dependencies=[Depends(require_operator)],
)
def create_probe(request: RuntimeProbeCreate, service: Service) -> RuntimeProbeResponse:
return service.create_probe(request)
@router.get("/runtime-probes", response_model=list[RuntimeProbeResponse])
def probes(service: Service) -> list[RuntimeProbeResponse]:
return service.probes()
@router.get("/runtime-probes/{probe_id}", response_model=RuntimeProbeResponse)
def probe(probe_id: uuid.UUID, service: Service) -> RuntimeProbeResponse:
return service.probe(probe_id)
@router.post(
"/runtime-probes/{probe_id}/cancel",
response_model=RuntimeProbeResponse,
dependencies=[Depends(require_operator)],
)
def cancel_probe(probe_id: uuid.UUID, service: Service) -> RuntimeProbeResponse:
return service.cancel(probe_id)
@router.get("/deployment-candidates", response_model=list[DeploymentCandidateResponse])
def deployment_candidates(service: Service) -> list[DeploymentCandidateResponse]:
return service.candidates()
@router.get("/deployment-candidates/{candidate_id}", response_model=DeploymentCandidateResponse)
def deployment_candidate(candidate_id: uuid.UUID, service: Service) -> DeploymentCandidateResponse:
return service.candidate(candidate_id)
@@ -0,0 +1,582 @@
from __future__ import annotations
import uuid
from functools import lru_cache
from typing import Annotated
from fastapi import APIRouter, Depends, Header, Query, Request, Response, status
from sqlalchemy.orm import Session
from modelforge_api.api.authorization import Admin, require_viewer
from modelforge_api.api.routes.agent import AgentIdentity
from modelforge_api.db import get_session
from modelforge_api.domain.serving import (
AgentServingJobComplete,
AgentServingJobFailure,
AgentServingJobLease,
AgentServingStateAck,
AgentServingStateReport,
CapabilityDeploymentResponse,
CapabilityExperimentCreate,
CapabilityExperimentResponse,
CapabilityPromotionCreate,
CoResidencyEvidenceCreate,
CoResidencyEvidenceResponse,
EmbeddingInvokeRequest,
EmbeddingInvokeResponse,
GatewayRequestResponse,
OCRInvokeRequest,
OCRInvokeResponse,
OpenAIEmbeddingItem,
OpenAIEmbeddingRequest,
OpenAIEmbeddingResponse,
OpenAIUsage,
PlacementPlanRequest,
PlacementPlanResponse,
ProductionApprovalCreate,
ProductionApprovalResponse,
ProjectFitEvidenceCreate,
ProjectFitEvidenceResponse,
ProjectIntegrationResponse,
RerankingInvokeRequest,
RerankingInvokeResponse,
ResidencyPolicyUpdate,
SchedulerBudgetResponse,
SchedulerMetricsResponse,
SchedulerPolicyResponse,
SchedulerPolicyUpdate,
ServiceClientCreate,
ServiceClientCreated,
ServiceClientResponse,
SpeechTranscriptionInvokeRequest,
SpeechTranscriptionInvokeResponse,
StableEmbeddingInvokeResponse,
VisionEmbeddingInvokeRequest,
VisionEmbeddingInvokeResponse,
)
from modelforge_api.persistence.models import ServiceClient
from modelforge_api.services.manifest_registry import ManifestRegistry, get_manifest_registry
from modelforge_api.services.serving import (
CapabilityAuthenticationEvidence,
ServingError,
ServingService,
)
from modelforge_api.services.transient_payloads import RedisPayloadStore
from modelforge_api.settings import Settings, get_settings
router = APIRouter(tags=["capability-serving"])
@lru_cache
def payload_store() -> RedisPayloadStore:
return RedisPayloadStore(get_settings().redis_url)
def get_serving_service(
session: Annotated[Session, Depends(get_session)],
settings: Annotated[Settings, Depends(get_settings)],
manifests: Annotated[ManifestRegistry, Depends(get_manifest_registry)],
) -> ServingService:
return ServingService(session, settings, manifests, payload_store())
Service = Annotated[ServingService, Depends(get_serving_service)]
def _authenticate_capability_client(
request: Request,
service: ServingService,
authorization: str | None,
capability: str,
) -> ServiceClient:
evidence = getattr(request.state, "capability_authentication", None)
if isinstance(evidence, CapabilityAuthenticationEvidence):
return service.reuse_authentication(evidence, authorization, capability)
return service.authenticate(authorization, capability)
def authenticate_rag_embedding_client(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> ServiceClient:
return _authenticate_capability_client(request, service, authorization, "rag.embedding@1")
def authenticate_rag_reranking_client(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> ServiceClient:
return _authenticate_capability_client(request, service, authorization, "rag.reranking@1")
def authenticate_document_ocr_client(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> ServiceClient:
return _authenticate_capability_client(request, service, authorization, "document.ocr@1")
def authenticate_vision_embedding_client(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> ServiceClient:
return _authenticate_capability_client(request, service, authorization, "vision.embedding@1")
def authenticate_speech_transcription_client(
request: Request,
service: Service,
authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> ServiceClient:
return _authenticate_capability_client(
request, service, authorization, "speech.transcription@1"
)
RagEmbeddingClient = Annotated[ServiceClient, Depends(authenticate_rag_embedding_client)]
RagRerankingClient = Annotated[ServiceClient, Depends(authenticate_rag_reranking_client)]
DocumentOcrClient = Annotated[ServiceClient, Depends(authenticate_document_ocr_client)]
VisionEmbeddingClient = Annotated[ServiceClient, Depends(authenticate_vision_embedding_client)]
SpeechTranscriptionClient = Annotated[
ServiceClient, Depends(authenticate_speech_transcription_client)
]
@router.post(
"/api/v1/admin/deployment-candidates/{candidate_id}/production-approvals",
response_model=ProductionApprovalResponse,
status_code=status.HTTP_201_CREATED,
)
def approve_production(
candidate_id: uuid.UUID,
request: ProductionApprovalCreate,
service: Service,
_admin: Admin,
) -> ProductionApprovalResponse:
return service.approve_production(candidate_id, request)
@router.get(
"/api/v1/admin/production-approvals",
response_model=list[ProductionApprovalResponse],
)
def production_approvals(service: Service, _admin: Admin) -> list[ProductionApprovalResponse]:
return service.approvals()
@router.post(
"/api/v1/admin/deployment-candidates/{candidate_id}/promote",
response_model=CapabilityDeploymentResponse,
status_code=status.HTTP_201_CREATED,
)
def promote_candidate(
candidate_id: uuid.UUID,
request: CapabilityPromotionCreate,
service: Service,
_admin: Admin,
) -> CapabilityDeploymentResponse:
return service.promote(candidate_id, request)
@router.get(
"/api/v1/capability-deployments",
response_model=list[CapabilityDeploymentResponse],
dependencies=[Depends(require_viewer)],
)
def capability_deployments(service: Service) -> list[CapabilityDeploymentResponse]:
return service.deployments()
@router.post(
"/api/v1/admin/deployment-candidates/{candidate_id}/experiments",
response_model=CapabilityExperimentResponse,
status_code=status.HTTP_201_CREATED,
)
def create_capability_experiment(
candidate_id: uuid.UUID,
request: CapabilityExperimentCreate,
service: Service,
_admin: Admin,
) -> CapabilityExperimentResponse:
return service.create_experiment(candidate_id, request)
@router.get(
"/api/v1/capability-experiments",
response_model=list[CapabilityExperimentResponse],
dependencies=[Depends(require_viewer)],
)
def capability_experiments(service: Service) -> list[CapabilityExperimentResponse]:
return service.experiments()
@router.post(
"/api/v1/admin/capability-experiments/{experiment_id}/deactivate",
response_model=CapabilityExperimentResponse,
)
def deactivate_capability_experiment(
experiment_id: uuid.UUID, service: Service, _admin: Admin
) -> CapabilityExperimentResponse:
return service.deactivate_experiment(experiment_id)
@router.post(
"/api/v1/admin/capability-deployments/{deployment_id}/unload",
response_model=CapabilityDeploymentResponse,
)
def unload_deployment(
deployment_id: uuid.UUID, service: Service, _admin: Admin
) -> CapabilityDeploymentResponse:
return service.request_unload(deployment_id)
@router.post(
"/api/v1/admin/capability-deployments/{deployment_id}/drain",
response_model=CapabilityDeploymentResponse,
)
def drain_deployment(
deployment_id: uuid.UUID, service: Service, _admin: Admin
) -> CapabilityDeploymentResponse:
return service.request_unload(deployment_id, drain=True)
@router.post(
"/api/v1/admin/service-clients",
response_model=ServiceClientCreated,
status_code=status.HTTP_201_CREATED,
)
def create_service_client(
request: ServiceClientCreate,
service: Service,
_admin: Admin,
response: Response,
) -> ServiceClientCreated:
response.headers["Cache-Control"] = "no-store"
return service.create_client(request)
@router.get("/api/v1/admin/service-clients", response_model=list[ServiceClientResponse])
def service_clients(service: Service, _admin: Admin) -> list[ServiceClientResponse]:
return service.clients()
@router.delete(
"/api/v1/admin/service-clients/{client_id}/credential",
response_model=ServiceClientResponse,
)
def revoke_service_credential(
client_id: uuid.UUID, service: Service, _admin: Admin
) -> ServiceClientResponse:
return service.revoke_client_credential(client_id)
@router.post(
"/api/v1/admin/service-clients/{client_id}/credential/rotate",
response_model=ServiceClientCreated,
)
def rotate_service_credential(
client_id: uuid.UUID, service: Service, _admin: Admin, response: Response
) -> ServiceClientCreated:
response.headers["Cache-Control"] = "no-store"
return service.rotate_client_credential(client_id)
@router.get(
"/api/v1/project-integrations",
response_model=list[ProjectIntegrationResponse],
dependencies=[Depends(require_viewer)],
)
def project_integrations(service: Service) -> list[ProjectIntegrationResponse]:
return service.project_integrations()
@router.post(
"/api/v1/admin/project-fit-evidence",
response_model=ProjectFitEvidenceResponse,
status_code=status.HTTP_201_CREATED,
)
def record_project_fit_evidence(
request: ProjectFitEvidenceCreate, service: Service, _admin: Admin
) -> ProjectFitEvidenceResponse:
return service.record_project_fit(request)
@router.get(
"/api/v1/scheduler",
response_model=list[SchedulerBudgetResponse],
dependencies=[Depends(require_viewer)],
)
def scheduler_overview(service: Service) -> list[SchedulerBudgetResponse]:
return service.scheduler_overview()
@router.get(
"/api/v1/scheduler/co-residency",
response_model=list[CoResidencyEvidenceResponse],
dependencies=[Depends(require_viewer)],
)
def co_residency_matrix(service: Service) -> list[CoResidencyEvidenceResponse]:
return service.co_residency_matrix()
@router.post(
"/api/v1/admin/scheduler/co-residency-evidence",
response_model=CoResidencyEvidenceResponse,
status_code=status.HTTP_201_CREATED,
)
def record_co_residency_evidence(
request: CoResidencyEvidenceCreate, service: Service, _admin: Admin
) -> CoResidencyEvidenceResponse:
return service.record_co_residency_evidence(request)
@router.get("/api/v1/admin/scheduler/policy", response_model=SchedulerPolicyResponse)
def scheduler_policy(service: Service, _admin: Admin) -> SchedulerPolicyResponse:
return service.scheduler_policy()
@router.get("/api/v1/admin/scheduler/metrics", response_model=SchedulerMetricsResponse)
def scheduler_metrics(service: Service, _admin: Admin) -> SchedulerMetricsResponse:
return service.scheduler_metrics()
@router.put("/api/v1/admin/scheduler/policy", response_model=SchedulerPolicyResponse)
def update_scheduler_policy(
request: SchedulerPolicyUpdate, service: Service, _admin: Admin
) -> SchedulerPolicyResponse:
return service.update_scheduler_policy(request)
@router.post(
"/api/v1/admin/scheduler/placements/{deployment_id}/dry-run",
response_model=PlacementPlanResponse,
)
def dry_run_placement(
deployment_id: uuid.UUID,
request: PlacementPlanRequest,
service: Service,
_admin: Admin,
) -> PlacementPlanResponse:
return service.dry_run_placement(deployment_id, request)
@router.get(
"/api/v1/admin/scheduler/placements",
response_model=list[PlacementPlanResponse],
)
def placement_history(
service: Service,
_admin: Admin,
limit: Annotated[int, Query(ge=1, le=500)] = 100,
) -> list[PlacementPlanResponse]:
return service.placement_history(limit)
@router.put(
"/api/v1/admin/capability-deployments/{deployment_id}/residency-policy",
response_model=CapabilityDeploymentResponse,
)
def update_residency_policy(
deployment_id: uuid.UUID,
request: ResidencyPolicyUpdate,
service: Service,
_admin: Admin,
) -> CapabilityDeploymentResponse:
return service.update_residency_policy(deployment_id, request)
@router.get("/api/v1/gateway/requests", response_model=list[GatewayRequestResponse])
def gateway_requests(
service: Service,
_admin: Admin,
limit: Annotated[int, Query(ge=1, le=500)] = 100,
) -> list[GatewayRequestResponse]:
return service.request_history(limit)
@router.get("/api/v1/admin/latency-traces", response_model=list[GatewayRequestResponse])
def latency_traces(
service: Service,
_admin: Admin,
limit: Annotated[int, Query(ge=1, le=500)] = 100,
) -> list[GatewayRequestResponse]:
"""Return bounded span summaries; request content and input digests are excluded."""
return service.request_history(limit)
@router.post(
"/api/v1/capabilities/rag.embedding@1/invoke",
response_model=StableEmbeddingInvokeResponse,
)
def invoke_embedding(
request: EmbeddingInvokeRequest,
service: Service,
client: RagEmbeddingClient,
) -> StableEmbeddingInvokeResponse:
return StableEmbeddingInvokeResponse.model_validate(service.invoke(request, client))
@router.post(
"/api/v1/capabilities/rag.reranking@1/invoke",
response_model=RerankingInvokeResponse,
)
def invoke_reranking(
request: RerankingInvokeRequest,
service: Service,
client: RagRerankingClient,
) -> RerankingInvokeResponse:
return service.invoke_reranking(request, client)
@router.post(
"/api/v1/capabilities/document.ocr@1/invoke",
response_model=OCRInvokeResponse,
)
def invoke_ocr(
request: OCRInvokeRequest,
service: Service,
client: DocumentOcrClient,
) -> OCRInvokeResponse:
result, request_id, execution = service.invoke_modality(
"document.ocr", request.model_dump(mode="json"), client
)
return OCRInvokeResponse(request_id=request_id, execution=execution, **result)
@router.post(
"/api/v1/capabilities/vision.embedding@1/invoke",
response_model=VisionEmbeddingInvokeResponse,
)
def invoke_vision_embedding(
request: VisionEmbeddingInvokeRequest,
service: Service,
client: VisionEmbeddingClient,
) -> VisionEmbeddingInvokeResponse:
result, request_id, execution = service.invoke_modality(
"vision.embedding", request.model_dump(mode="json"), client
)
return VisionEmbeddingInvokeResponse(
request_id=request_id,
execution=execution,
dimension=int(result["dimension"]),
embedding_space_id=result["embedding_space_id"],
data=result["vectors"],
)
@router.post(
"/api/v1/capabilities/speech.transcription@1/invoke",
response_model=SpeechTranscriptionInvokeResponse,
)
def invoke_speech_transcription(
request: SpeechTranscriptionInvokeRequest,
service: Service,
client: SpeechTranscriptionClient,
) -> SpeechTranscriptionInvokeResponse:
result, request_id, execution = service.invoke_modality(
"speech.transcription", request.model_dump(mode="json"), client
)
return SpeechTranscriptionInvokeResponse(
request_id=request_id,
execution=execution,
**result,
)
@router.post(
"/api/v1/capability-experiments/{route_key}/invoke",
response_model=EmbeddingInvokeResponse,
)
def invoke_embedding_experiment(
route_key: str,
request: EmbeddingInvokeRequest,
service: Service,
client: RagEmbeddingClient,
) -> EmbeddingInvokeResponse:
route = service.repo.experiment_route(route_key)
deployment = (
service.repo.deployment(route.capability_deployment_id)
if route and route.status == "active"
else None
)
if not deployment:
raise ServingError(404, "EXPERIMENT_NOT_FOUND", "capability experiment is unavailable")
return service.invoke(
request,
client,
deployment=deployment,
experiment_route=route_key,
)
@router.post("/v1/embeddings", response_model=OpenAIEmbeddingResponse)
def openai_embeddings(
request: OpenAIEmbeddingRequest,
service: Service,
client: RagEmbeddingClient,
) -> OpenAIEmbeddingResponse:
native = service.invoke(EmbeddingInvokeRequest(input=request.input), client)
return OpenAIEmbeddingResponse(
data=[
OpenAIEmbeddingItem(index=index, embedding=embedding)
for index, embedding in enumerate(native.data)
],
usage=OpenAIUsage(
prompt_tokens=native.usage.input_tokens,
total_tokens=native.usage.input_tokens,
),
)
@router.get(
"/api/v1/agent/serving-jobs/next",
response_model=AgentServingJobLease | None,
)
def claim_serving_job(
service: Service,
identity: AgentIdentity,
wait_seconds: Annotated[float, Query(ge=0.0, le=5.0)] = 0.0,
) -> AgentServingJobLease | None:
_credential, node = identity
return service.claim_next(node, wait_seconds=wait_seconds)
@router.post("/api/v1/agent/serving-jobs/{job_id}/complete")
def complete_serving_job(
job_id: uuid.UUID,
request: AgentServingJobComplete,
service: Service,
identity: AgentIdentity,
) -> dict[str, str]:
_credential, node = identity
service.complete_job(job_id, node, request)
return {"status": "accepted"}
@router.post("/api/v1/agent/serving-jobs/{job_id}/fail")
def fail_serving_job(
job_id: uuid.UUID,
request: AgentServingJobFailure,
service: Service,
identity: AgentIdentity,
) -> dict[str, str]:
_credential, node = identity
service.fail_job(job_id, node, request)
return {"status": "accepted"}
@router.post(
"/api/v1/agent/serving-state",
response_model=AgentServingStateAck,
)
def report_serving_state(
request: AgentServingStateReport,
service: Service,
identity: AgentIdentity,
) -> AgentServingStateAck:
_credential, node = identity
return service.report_state(node, request)
@@ -0,0 +1 @@
"""ModelForge operator command-line surfaces."""
+334
View File
@@ -0,0 +1,334 @@
"""Operator disaster-recovery CLI.
Restoring over a live database is an operator action with a runbook behind it, not a remote API
call, so the destructive half of recovery lives here. Every subcommand takes typed arguments and
runs fixed operations: there is no pass-through shell, and no argument reaches a shell.
python -m modelforge_api.cli.dr create-backup --backup-id m15-rehearsal-a --reason "..."
python -m modelforge_api.cli.dr verify-backup --backup-id m15-rehearsal-a
python -m modelforge_api.cli.dr plan-restore --backup-id m15-rehearsal-a --target-url ...
python -m modelforge_api.cli.dr validate-restore --plan-id <uuid>
python -m modelforge_api.cli.dr run-restore --plan-id <uuid> --reason "..."
python -m modelforge_api.cli.dr readiness
python -m modelforge_api.cli.dr bundle --backup-id m15-rehearsal-a
"""
from __future__ import annotations
import argparse
import json
import sys
import uuid
from typing import Any
from sqlalchemy import select
from sqlalchemy.orm import Session
from modelforge_api.db import build_engine
from modelforge_api.domain.recovery import (
POINT_IN_TIME_SUPPORT,
BackupSetCreate,
RestoreAdvanceRequest,
RestoreMode,
RestorePlanCreate,
redact_database_url,
)
from modelforge_api.persistence.models import BackupSet
from modelforge_api.services.recovery import RecoveryError, RecoveryService
from modelforge_api.settings import get_settings
def _service(session: Session) -> RecoveryService:
return RecoveryService(session, get_settings(), "operator", "dr-cli")
def _emit(payload: Any) -> None:
print(json.dumps(payload, indent=2, default=str))
def _backup_by_id(session: Session, backup_id: str) -> BackupSet:
record = session.scalar(select(BackupSet).where(BackupSet.backup_id == backup_id))
if record is None:
raise RecoveryError(404, "backup_not_found", f"no backup set named {backup_id}")
return record
def _cmd_create_backup(session: Session, args: argparse.Namespace) -> int:
service = _service(session)
service.ensure_defaults()
response = service.create_backup(
BackupSetCreate(
backup_id=args.backup_id,
reason=args.reason,
milestone=args.milestone,
legal_hold=args.legal_hold,
created_by=args.actor,
)
)
_emit(json.loads(response.model_dump_json()))
return 0 if response.state.value in {"CREATED", "VERIFIED"} else 1
def _cmd_verify_backup(session: Session, args: argparse.Namespace) -> int:
service = _service(session)
record = _backup_by_id(session, args.backup_id)
response = service.verify_backup(record.id)
_emit(
{
"backup_id": response.backup_id,
"state": response.state.value,
"restore_eligible": response.restore_eligible,
"manifest_sha256": response.manifest_sha256,
"failure_code": response.failure_code,
"failure_reason": response.failure_reason,
"verification": response.verification_details.get("verification", {}),
}
)
return 0 if response.restore_eligible else 1
def _cmd_list_backups(session: Session, args: argparse.Namespace) -> int:
service = _service(session)
_emit(
[
{
"backup_id": item.backup_id,
"state": item.state.value,
"restore_eligible": item.restore_eligible,
"schema_revision": item.schema_revision,
"payload_bytes": item.payload_bytes,
"encrypted": item.encrypted,
"verified_at": item.verified_at,
"expires_at": item.expires_at,
}
for item in service.backups(args.limit)
]
)
return 0
def _cmd_plan_restore(session: Session, args: argparse.Namespace) -> int:
service = _service(session)
record = _backup_by_id(session, args.backup_id)
response = service.create_restore_plan(
RestorePlanCreate(
backup_set_id=record.id,
mode=RestoreMode(args.mode),
target_environment=args.target_environment,
target_label=args.target_label,
database_destination=args.target_url,
artifact_strategy=args.artifact_strategy,
secret_strategy=args.secret_strategy,
node_strategy=args.node_strategy,
reason=args.reason,
created_by=args.actor,
)
)
_emit(json.loads(response.model_dump_json()))
return 0
def _cmd_validate_restore(session: Session, args: argparse.Namespace) -> int:
service = _service(session)
response = service.preflight(uuid.UUID(args.plan_id))
_emit(
{
"plan_id": str(response.id),
"backup_id": response.backup_id,
"state": response.state.value,
"destination": response.database_destination_redacted,
"preflight": response.preflight,
}
)
return 0 if response.preflight.get("status") == "PASS" else 1
def _cmd_run_restore(session: Session, args: argparse.Namespace) -> int:
service = _service(session)
plan_id = uuid.UUID(args.plan_id)
request = RestoreAdvanceRequest(actor=args.actor, reason=args.reason)
operation = service.start_restore(plan_id, request)
operation = service.advance_restore(operation.id, request)
_emit(
{
"operation_id": str(operation.id),
"backup_id": operation.backup_id,
"state": operation.state.value,
"attempt": operation.attempt,
"phase_durations": operation.phase_durations,
"rto_seconds": operation.rto_seconds,
"rpo_seconds": operation.rpo_seconds,
"validation": operation.validation_result,
"fingerprint_diff": operation.fingerprint_diff,
"failure_code": operation.failure_code,
"failure_reason": operation.failure_reason,
}
)
return 0 if operation.state.value == "READY" else 1
def _cmd_readiness(session: Session, _args: argparse.Namespace) -> int:
service = _service(session)
dashboard = service.dashboard()
_emit(json.loads(dashboard.model_dump_json()))
return 0 if not dashboard.unprotected_assets and not dashboard.stale_backup else 1
def _cmd_bundle(session: Session, args: argparse.Namespace) -> int:
"""Print the bounded DR bundle manifest: what an operator needs to rebuild from nothing."""
service = _service(session)
record = _backup_by_id(session, args.backup_id)
backup = service.backup(record.id)
settings = get_settings()
_emit(
{
"modelforge": {
"version": backup.modelforge_version,
"commit": backup.modelforge_commit,
"reference": backup.environment_fingerprint.get("source_reference"),
"repository": record.source_repository,
},
"database_backup": {
"backup_id": backup.backup_id,
"state": backup.state.value,
"restore_eligible": backup.restore_eligible,
"destination_root": backup.destination_root,
"manifest": backup.manifest_relative_path,
"manifest_sha256": backup.manifest_sha256,
"payload_bytes": backup.payload_bytes,
"schema_revision": backup.schema_revision,
"database_identity": backup.database_identity,
"point_in_time_support": POINT_IN_TIME_SUPPORT,
},
"encryption": {
"encrypted": backup.encrypted,
"algorithm": backup.encryption_algorithm,
"key_id": backup.encryption_key_id,
"key_requirement": (
"MODELFORGE_BACKUP_ENCRYPTION_KEY must be supplied by the operator; it is "
"never written into a backup"
),
},
"host_configuration": {
"backup_root": str(settings.backup_root),
"artifact_root": settings.artifact_root,
"quarantine_root": settings.quarantine_root,
"config_root": str(settings.config_root),
"database_url": redact_database_url(settings.database_url),
},
"artifact_recovery_plan": [
{
"object": entry.object_name,
"type": entry.logical_asset_type,
"sha256": entry.sha256,
"size_bytes": entry.size_bytes,
}
for entry in backup.entries
],
"node_enrollment": (
"Issue a fresh enrollment token through /api/v1/admin/node-enrollments and "
"revoke any credential believed lost; hardware identity is persisted so a "
"recovered node keeps its node id."
),
"external_dependencies": [
item.asset_key
for item in service.readiness()
if item.readiness.value == "EXTERNAL_DEPENDENCY"
],
"runbooks": [
"docs/operations/RUNBOOK_FULL_DR.md",
"docs/operations/RUNBOOK_DATABASE_RESTORE.md",
"docs/operations/RUNBOOK_CONTROL_PLANE_LOSS.md",
"docs/operations/RUNBOOK_NODE_LOSS.md",
"docs/operations/RUNBOOK_ARTIFACT_LOSS.md",
"docs/operations/RUNBOOK_BACKUP_FAILURE.md",
],
}
)
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="modelforge-dr", description="ModelForge disaster-recovery operator commands"
)
parser.add_argument("--database-url", default=None, help="override the control-plane database")
subparsers = parser.add_subparsers(dest="command", required=True)
create = subparsers.add_parser("create-backup", help="create a new backup set")
create.add_argument("--backup-id", required=True)
create.add_argument("--reason", required=True)
create.add_argument("--milestone", default=None)
create.add_argument("--legal-hold", action="store_true")
create.add_argument("--actor", default="operator")
create.set_defaults(handler=_cmd_create_backup)
verify = subparsers.add_parser("verify-backup", help="verify a backup set end to end")
verify.add_argument("--backup-id", required=True)
verify.set_defaults(handler=_cmd_verify_backup)
listing = subparsers.add_parser("list-backups", help="list journaled backup sets")
listing.add_argument("--limit", type=int, default=25)
listing.set_defaults(handler=_cmd_list_backups)
plan = subparsers.add_parser("plan-restore", help="create a restore plan")
plan.add_argument("--backup-id", required=True)
plan.add_argument("--target-url", required=True)
plan.add_argument("--target-label", required=True)
plan.add_argument(
"--mode", choices=[item.value for item in RestoreMode], default=RestoreMode.VALIDATION.value
)
plan.add_argument(
"--target-environment", choices=["ISOLATED", "STAGING", "PRODUCTION"], default="ISOLATED"
)
plan.add_argument(
"--artifact-strategy",
choices=["NONE", "MANIFEST_ONLY", "REHYDRATE_MISSING", "RESTORE_LOCAL"],
default="MANIFEST_ONLY",
)
plan.add_argument(
"--secret-strategy", choices=["ROTATE", "RESTORE_HASHES", "MANUAL"], default="RESTORE_HASHES"
)
plan.add_argument(
"--node-strategy", choices=["REUSE_CREDENTIAL", "RE_ENROLL", "NONE"], default="NONE"
)
plan.add_argument("--reason", required=True)
plan.add_argument("--actor", default="operator")
plan.set_defaults(handler=_cmd_plan_restore)
validate = subparsers.add_parser("validate-restore", help="run the restore preflight")
validate.add_argument("--plan-id", required=True)
validate.set_defaults(handler=_cmd_validate_restore)
run = subparsers.add_parser("run-restore", help="execute a restore plan to completion")
run.add_argument("--plan-id", required=True)
run.add_argument("--reason", required=True)
run.add_argument("--actor", default="operator")
run.set_defaults(handler=_cmd_run_restore)
readiness = subparsers.add_parser("readiness", help="print recovery readiness")
readiness.set_defaults(handler=_cmd_readiness)
bundle = subparsers.add_parser("bundle", help="print the operator DR bundle manifest")
bundle.add_argument("--backup-id", required=True)
bundle.set_defaults(handler=_cmd_bundle)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
engine = build_engine(args.database_url)
try:
with Session(engine) as session:
handler = args.handler
return int(handler(session, args))
except RecoveryError as error:
_emit({"error": {"code": error.code, "message": error.message, "details": error.details}})
return 2
finally:
engine.dispose()
if __name__ == "__main__": # pragma: no cover - operator entry point
sys.exit(main())
+38
View File
@@ -0,0 +1,38 @@
from collections.abc import Generator
from typing import Any, cast
from sqlalchemy import CursorResult, Engine, create_engine
from sqlalchemy.engine import Result
from sqlalchemy.orm import Session
from modelforge_api.persistence.runtime_engine import register_application_engine
from modelforge_api.settings import get_settings
def rows_affected(result: Result[Any]) -> int:
"""How many rows a DML statement actually changed.
`Session.execute` is typed as returning `Result`, which carries no `rowcount`; a DML statement
always returns a `CursorResult`, which does. SQLAlchemy 2.0.52 narrowed that return type, so
reading `.rowcount` directly stopped type-checking — while continuing to work at runtime.
The cast is where that knowledge lives, once, rather than at ten call sites. It matters more
than it looks: every single-use claim in the platform is a conditional UPDATE whose row count
decides the winner, and that is what makes enrolment tokens and lifecycle claims atomic instead
of merely usually-correct.
"""
return cast("CursorResult[Any]", result).rowcount
def build_engine(database_url: str | None = None) -> Engine:
url = database_url or get_settings().database_url
return register_application_engine(create_engine(url, pool_pre_ping=True))
engine = build_engine()
def get_session() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session
@@ -0,0 +1,217 @@
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
class AcquisitionModel(BaseModel):
model_config = ConfigDict(extra="forbid", from_attributes=True)
class DiscoverySearchRequest(AcquisitionModel):
query: str = Field(min_length=1, max_length=255)
limit: int = Field(default=20, ge=1, le=100)
sort: Literal["downloads", "likes", "last_modified"] = "downloads"
pipeline_tag: str | None = Field(default=None, max_length=128)
class DiscoveryCandidate(AcquisitionModel):
repository_id: str
resolved_commit_sha: str | None = None
access_state: str
pipeline_tag: str | None = None
library_name: str | None = None
tags: list[str] = Field(default_factory=list)
downloads: int | None = None
likes: int | None = None
last_modified: datetime | None = None
matched_model_id: uuid.UUID | None = None
upstream_facts: dict[str, Any] = Field(default_factory=dict)
local_interpretation: dict[str, Any] = Field(default_factory=dict)
class UpstreamRefreshRequest(AcquisitionModel):
revision: str = Field(default="main", min_length=1, max_length=255)
class UpstreamFileResponse(AcquisitionModel):
id: uuid.UUID
path: str
size_bytes: int | None
blob_id: str | None
upstream_sha256: str | None
file_format: str
role: str
risk_flags: list[str]
metadata_snapshot: dict[str, Any]
class UpstreamSnapshotResponse(AcquisitionModel):
id: uuid.UUID
model_id: uuid.UUID | None
provider: str
repository_id: str
requested_revision: str
resolved_commit_sha: str
access_state: str
metadata_snapshot: dict[str, Any]
card_metadata: dict[str, Any]
security_metadata: dict[str, Any]
source_updated_at: datetime | None
observed_at: datetime
stale_after: datetime
stale: bool
files: list[UpstreamFileResponse]
class ArtifactSetResponse(AcquisitionModel):
id: uuid.UUID
revision_id: uuid.UUID
snapshot_id: uuid.UUID
variant_key: str
label: str
selection_reason: str
selected_paths: list[str]
total_size_bytes: int
file_count: int
availability: str
status: str
completeness: str
security_status: str
license_status: str
immutable_at: datetime
created_at: datetime
updated_at: datetime
class DownloadPlanCreate(AcquisitionModel):
artifact_set_id: uuid.UUID
compute_node_id: uuid.UUID
storage_root_id: uuid.UUID
expires_in_seconds: int = Field(default=3600, ge=300, le=86400)
class DownloadPlanFileResponse(AcquisitionModel):
ordinal: int
path: str
size_bytes: int
upstream_sha256: str | None
file_format: str
role: str
risk_flags: list[str]
class DownloadPlanResponse(AcquisitionModel):
id: uuid.UUID
artifact_set_id: uuid.UUID
compute_node_id: uuid.UUID
storage_root_id: uuid.UUID
repository_id: str
resolved_commit_sha: str
total_size_bytes: int
file_count: int
status: str
idempotency_key: str
preflight: dict[str, Any]
immutable_payload: dict[str, Any]
planned_at: datetime
expires_at: datetime
immutable_at: datetime
created_at: datetime
updated_at: datetime
stale: bool
files: list[DownloadPlanFileResponse]
class ArtifactJobResponse(AcquisitionModel):
id: uuid.UUID
plan_id: uuid.UUID
compute_node_id: uuid.UUID
storage_root_id: uuid.UUID
status: str
attempt_count: int
progress_bytes: int
total_bytes: int
current_file: str | None
cancel_requested: bool
quarantine_relative_path: str | None
promoted_relative_path: str | None
error_code: str | None
error_message: str | None
result: dict[str, Any]
started_at: datetime | None
completed_at: datetime | None
created_at: datetime
updated_at: datetime
class AgentArtifactJobFile(AcquisitionModel):
ordinal: int
path: str
size_bytes: int
upstream_sha256: str | None
file_format: str
role: str
risk_flags: list[str]
class AgentArtifactJobLease(AcquisitionModel):
job_id: uuid.UUID
lease_token: str
lease_expires_at: datetime
repository_id: str
resolved_commit_sha: str
storage_root_id: uuid.UUID
target_root: str
total_size_bytes: int
reserve_bytes: int
reserve_percent: int
files: list[AgentArtifactJobFile]
class AgentJobProgress(AcquisitionModel):
lease_token: str = Field(min_length=32, max_length=512)
status: Literal["claimed", "downloading", "verifying", "promoting"]
progress_bytes: int = Field(ge=0)
current_file: str | None = Field(default=None, max_length=2048)
quarantine_relative_path: str | None = Field(default=None, max_length=2048)
class AgentJobControl(AcquisitionModel):
accepted: bool
cancel_requested: bool
lease_expires_at: datetime
class CompletedFile(AcquisitionModel):
path: str
relative_path: str
size_bytes: int = Field(ge=0)
sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
inspections: list[dict[str, Any]] = Field(default_factory=list)
@field_validator("path", "relative_path")
@classmethod
def safe_path(cls, value: str) -> str:
normalized = value.replace("\\", "/")
if normalized.startswith("/") or ".." in normalized.split("/"):
raise ValueError("path must be relative and confined")
return normalized
class AgentJobComplete(AcquisitionModel):
lease_token: str = Field(min_length=32, max_length=512)
promoted_relative_path: str
capacity_observation: dict[str, Any]
files: list[CompletedFile] = Field(min_length=1)
class AgentJobFailure(AcquisitionModel):
lease_token: str = Field(min_length=32, max_length=512)
error_code: str = Field(min_length=1, max_length=64)
error_message: str = Field(min_length=1, max_length=2000)
retryable: bool = False
details: dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,135 @@
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
from modelforge_api.domain.enums import Availability
from modelforge_api.domain.hardware import (
AcceleratorInventory,
AcceleratorTelemetry,
HostInventory,
ObservedValue,
StorageObservation,
)
AGENT_PROTOCOL_VERSION = 1
AGENT_PROTOCOL_CAPABILITIES = [
"hardware.inventory",
"hardware.telemetry",
"artifact.acquire.v1",
"runtime.probe.v1",
"runtime.health.v1",
"runtime.unload.v1",
"deployment.load.v1",
"deployment.invoke.v1",
"deployment.health.v1",
"deployment.drain.v1",
"deployment.unload.v1",
]
class AgentProtocolModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class AgentMetadata(AgentProtocolModel):
agent_version: str
protocol_version: int
supported_capabilities: list[str] = Field(default_factory=list)
started_at: datetime
class EnrollmentRequest(AgentProtocolModel):
enrollment_token: str = Field(min_length=32, max_length=512)
identity_key: str = Field(min_length=1, max_length=128)
identity_source: str = Field(min_length=1, max_length=32)
hostname: str = Field(min_length=1, max_length=255)
display_name: str = Field(min_length=1, max_length=255)
metadata: AgentMetadata
class EnrollmentResponse(AgentProtocolModel):
node_id: str
credential_id: str
node_credential: str
protocol_version: int = AGENT_PROTOCOL_VERSION
class NodeCredentialCreated(AgentProtocolModel):
node_id: str
credential_id: str
node_credential: str
class HeartbeatRequest(AgentProtocolModel):
identity_key: str
metadata: AgentMetadata
observed_at: datetime
last_error: str | None = Field(default=None, max_length=1000)
class InventoryNvidiaPayload(AgentProtocolModel):
availability: Availability
reason: str | None = None
inventory: list[AcceleratorInventory] = Field(default_factory=list)
class InventoryReport(AgentProtocolModel):
identity_key: str
protocol_version: int
sequence: int = Field(ge=1, le=9_223_372_036_854_775_807)
observed_at: datetime
host: HostInventory
nvidia: InventoryNvidiaPayload
class TelemetryReport(AgentProtocolModel):
identity_key: str
protocol_version: int
sequence: int = Field(ge=1, le=9_223_372_036_854_775_807)
observed_at: datetime
available_ram_bytes: ObservedValue[int]
storage: list[StorageObservation] = Field(default_factory=list)
accelerators: list[AcceleratorTelemetry] = Field(default_factory=list)
class ObservationAck(AgentProtocolModel):
accepted: bool
reason: str | None = None
received_at: datetime
class EnrollmentTokenCreate(AgentProtocolModel):
expires_in_seconds: int = Field(default=900, ge=60, le=86400)
display_name: str | None = Field(default=None, max_length=255)
role: str | None = Field(default=None, max_length=64)
labels: dict[str, str | bool] = Field(default_factory=dict)
production_eligible: bool = False
lab_eligible: bool = True
benchmark_eligible: bool = False
class EnrollmentTokenCreated(AgentProtocolModel):
id: str
enrollment_token: str
expires_at: datetime
setup_environment: dict[str, str]
class EnrollmentTokenSummary(AgentProtocolModel):
id: str
created_at: datetime
expires_at: datetime
used_at: datetime | None
revoked_at: datetime | None
class NodeManagementUpdate(AgentProtocolModel):
display_name: str | None = Field(default=None, min_length=1, max_length=255)
role: str | None = Field(default=None, max_length=64)
labels: dict[str, str | bool] | None = None
enabled: bool | None = None
production_eligible: bool | None = None
lab_eligible: bool | None = None
benchmark_eligible: bool | None = None
+140
View File
@@ -0,0 +1,140 @@
"""Stable audit-chain format identifiers shared by persistence and services."""
from __future__ import annotations
import hashlib
import json
import uuid
from datetime import UTC, datetime
from typing import Any
AUDIT_CHAIN_SINGLETON_ID = 1
AUDIT_HASH_FORMAT_V1 = "v1"
AUDIT_HASH_FORMAT_V2 = "v2"
AUDIT_CURRENT_HASH_FORMAT = AUDIT_HASH_FORMAT_V2
AUDIT_LEGACY_PREFIX_DOMAIN = b"modelforge:audit:legacy-prefix:v1\n"
AUDIT_EMPTY_LEGACY_PREFIX_SEAL = hashlib.sha256(AUDIT_LEGACY_PREFIX_DOMAIN).hexdigest()
def normalise_audit_timestamp(value: datetime | str) -> str:
"""Return the stable UTC/microsecond representation used by v2 audit hashes."""
moment: datetime
if isinstance(value, datetime):
moment = value
elif isinstance(value, str):
candidate = value.strip()
if candidate.endswith("Z"):
candidate = candidate[:-1] + "+00:00"
try:
moment = datetime.fromisoformat(candidate)
except ValueError as error:
raise ValueError("audit occurred_at is not an ISO-8601 timestamp") from error
else:
raise TypeError("audit occurred_at must be a datetime or ISO-8601 string")
if moment.tzinfo is None:
moment = moment.replace(tzinfo=UTC)
return moment.astimezone(UTC).isoformat(timespec="microseconds").replace("+00:00", "Z")
def normalise_audit_event_id(value: Any) -> str:
try:
return str(uuid.UUID(str(value)))
except (AttributeError, TypeError, ValueError) as error:
raise ValueError("audit event id is not a UUID") from error
def canonical_audit_payload_and_hash(
*,
correlation_id: str,
actor_type: str,
actor_id: str,
action: str,
resource_type: str,
resource_id: str | None,
outcome: str,
details: dict[str, Any],
previous_event_hash: str | None,
hash_format: str = AUDIT_HASH_FORMAT_V1,
event_id: Any | None = None,
occurred_at: datetime | str | None = None,
) -> tuple[dict[str, Any], str]:
"""Return detached stored fields and their versioned SHA-256 identity."""
stored_payload, _encoded_hash, event_hash = canonical_audit_payload_text_and_hash(
correlation_id=correlation_id,
actor_type=actor_type,
actor_id=actor_id,
action=action,
resource_type=resource_type,
resource_id=resource_id,
outcome=outcome,
details=details,
previous_event_hash=previous_event_hash,
hash_format=hash_format,
event_id=event_id,
occurred_at=occurred_at,
)
return stored_payload, event_hash
def canonical_audit_payload_text_and_hash(
*,
correlation_id: str,
actor_type: str,
actor_id: str,
action: str,
resource_type: str,
resource_id: str | None,
outcome: str,
details: dict[str, Any],
previous_event_hash: str | None,
hash_format: str = AUDIT_HASH_FORMAT_V1,
event_id: Any | None = None,
occurred_at: datetime | str | None = None,
) -> tuple[dict[str, Any], str, str]:
"""Return stored fields, the exact hashed text, and its SHA-256 digest.
The exact text is persisted for v2 events. That makes PostgreSQL's SECURITY DEFINER writer
authoritative for its own canonical encoding without asking Python and PostgreSQL to reproduce
each other's JSON lexical representation. Strict verification hashes the stored bytes and then
independently checks that the decoded object is exactly the event's semantic payload.
"""
if not isinstance(details, dict):
raise TypeError("audit details must be an object")
stored_payload: dict[str, Any] = {
"correlation_id": correlation_id,
"actor_type": actor_type,
"actor_id": actor_id,
"action": action,
"resource_type": resource_type,
"resource_id": resource_id,
"outcome": outcome,
"details": details,
"previous_event_hash": previous_event_hash,
}
encoded_stored = json.dumps(stored_payload, sort_keys=True, separators=(",", ":"))
canonical_stored = json.loads(encoded_stored)
if not isinstance(canonical_stored, dict): # pragma: no cover - constructed above
raise TypeError("canonical audit payload must be an object")
if hash_format == AUDIT_HASH_FORMAT_V1:
hash_payload = canonical_stored
elif hash_format == AUDIT_HASH_FORMAT_V2:
if event_id is None or occurred_at is None:
raise ValueError("v2 audit hashes require an event id and occurred_at")
hash_payload = {
"hash_format": AUDIT_HASH_FORMAT_V2,
"id": normalise_audit_event_id(event_id),
"occurred_at": normalise_audit_timestamp(occurred_at),
**canonical_stored,
}
else:
raise ValueError(f"unsupported audit hash format {hash_format!r}")
encoded_hash = json.dumps(hash_payload, sort_keys=True, separators=(",", ":"))
return (
canonical_stored,
encoded_hash,
hashlib.sha256(encoded_hash.encode("utf-8")).hexdigest(),
)
@@ -0,0 +1,171 @@
from __future__ import annotations
import math
import re
import uuid
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
EvaluationType = Literal[
"retrieval", "ocr", "visual-retrieval", "asr", "tts", "generation", "llm"
]
CapabilityRecommendationState = Literal[
"KEEP_CURRENT",
"LAB_READY",
"PROMOTION_ELIGIBLE",
"REQUIRES_MORE_EVIDENCE",
"BLOCKED",
]
MetricDirection = Literal["higher_is_better", "lower_is_better", "informational"]
_KEY = re.compile(r"^[a-z0-9][a-z0-9_-]{0,127}$")
ALLOWED_METRICS: dict[str, frozenset[str]] = {
"retrieval": frozenset({"recall_at_5", "recall_at_10", "mrr", "ndcg_at_10"}),
"ocr": frozenset({"cer", "wer", "text_accuracy", "field_accuracy", "layout_accuracy", "latency_ms", "peak_vram_bytes"}),
"visual-retrieval": frozenset({"recall_at_1", "recall_at_5", "recall_at_10", "mrr", "latency_ms", "peak_vram_bytes"}),
"asr": frozenset({"wer", "real_time_factor", "latency_ms", "peak_vram_bytes"}),
"tts": frozenset({"latency_ms", "real_time_factor", "peak_vram_bytes"}),
"generation": frozenset({"latency_ms", "peak_vram_bytes"}),
"llm": frozenset({"task_success", "structured_output_validity", "latency_ms", "peak_vram_bytes"}),
}
class EvaluationModel(BaseModel):
model_config = ConfigDict(extra="forbid", from_attributes=True)
class MetricDefinition(EvaluationModel):
name: str
direction: MetricDirection
unit: str = Field(min_length=1, max_length=64)
minimum: float | None = None
maximum: float | None = None
@model_validator(mode="after")
def valid_range(self) -> MetricDefinition:
if self.minimum is not None and self.maximum is not None and self.minimum > self.maximum:
raise ValueError("metric minimum cannot exceed maximum")
return self
class CapabilityEvaluationCase(EvaluationModel):
key: str
fixture_ref: str = Field(min_length=1, max_length=512)
fixture_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
ground_truth: dict[str, Any]
labels: list[str] = Field(default_factory=list)
critical: bool = False
@field_validator("key")
@classmethod
def valid_key(cls, value: str) -> str:
if not _KEY.fullmatch(value):
raise ValueError("case key must be a safe identifier")
return value
class CapabilityEvaluationSuiteCreate(EvaluationModel):
capability: str = Field(pattern=r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$")
contract_version: int = Field(default=1, ge=1)
key: str
evaluation_type: EvaluationType
revision: str
dataset_revision: str
metrics: list[MetricDefinition] = Field(min_length=1)
cases: list[CapabilityEvaluationCase] = Field(min_length=1, max_length=500)
thresholds: dict[str, float] = Field(default_factory=dict)
@model_validator(mode="after")
def validate_definition(self) -> CapabilityEvaluationSuiteCreate:
for key in (self.key, self.revision, self.dataset_revision):
if not _KEY.fullmatch(key):
raise ValueError("suite identifiers must be safe")
metric_names = [item.name for item in self.metrics]
if len(metric_names) != len(set(metric_names)):
raise ValueError("metric names must be unique")
unsupported = set(metric_names) - ALLOWED_METRICS[self.evaluation_type]
if unsupported:
raise ValueError(f"metrics are invalid for {self.evaluation_type}: {sorted(unsupported)}")
if set(self.thresholds) - set(metric_names):
raise ValueError("thresholds must refer to declared metrics")
if len({item.key for item in self.cases}) != len(self.cases):
raise ValueError("case keys must be unique")
return self
class CapabilityEvaluationSuiteResponse(EvaluationModel):
id: uuid.UUID
capability: str
contract_version: int
key: str
evaluation_type: EvaluationType
revision: str
dataset_revision: str
definition_digest: str
metrics: list[MetricDefinition]
cases: list[CapabilityEvaluationCase]
thresholds: dict[str, float]
created_at: datetime
class CapabilityCaseResult(EvaluationModel):
case_key: str
metrics: dict[str, float]
output_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
status: Literal["passed", "failed", "error"]
error_code: str | None = Field(default=None, max_length=64)
class CapabilityEvaluationRunCreate(EvaluationModel):
suite_id: uuid.UUID
capability_deployment_id: uuid.UUID
status: Literal["completed", "failed"]
metrics: dict[str, float]
cases: list[CapabilityCaseResult] = Field(min_length=1, max_length=500)
resource_metrics: dict[str, float]
environment_fingerprint: str = Field(pattern=r"^[a-f0-9]{64}$")
evidence: dict[str, Any] = Field(default_factory=dict)
started_at: datetime
completed_at: datetime
@field_validator("metrics", "resource_metrics")
@classmethod
def finite_values(cls, value: dict[str, float]) -> dict[str, float]:
if any(not math.isfinite(item) for item in value.values()):
raise ValueError("metric values must be finite")
return value
@model_validator(mode="after")
def chronological(self) -> CapabilityEvaluationRunCreate:
if self.completed_at < self.started_at:
raise ValueError("evaluation completion precedes its start")
return self
class CapabilityEvaluationRunResponse(EvaluationModel):
id: uuid.UUID
suite_id: uuid.UUID
capability_deployment_id: uuid.UUID
evaluation_type: EvaluationType
status: str
metrics: dict[str, float]
cases: list[CapabilityCaseResult]
resource_metrics: dict[str, float]
environment_fingerprint: str
evidence_digest: str
evidence: dict[str, Any]
started_at: datetime
completed_at: datetime
created_at: datetime
class CapabilityAdvisorResponse(EvaluationModel):
capability: str
contract_version: int
state: CapabilityRecommendationState
deployment_id: uuid.UUID | None = None
evaluation_run_id: uuid.UUID | None = None
reasons: list[str]
automatic_promotion: Literal[False] = False
@@ -0,0 +1,282 @@
"""Documentation for every setting, held next to the settings themselves.
`.env.example` and `docs/CONFIGURATION.md` are generated from this table joined with the typed
defaults, and a test fails when a setting exists without an entry here. Hand-maintained
configuration documentation drifts silently — the operator finds out when a production deployment
does something the manual said it would not.
`required_in_production` means startup validation refuses to run production without it, not merely
that it is recommended.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
class Sensitivity(StrEnum):
PUBLIC = "public"
#: Reveals deployment topology. Not a credential, but not for a public issue tracker either.
INTERNAL = "internal"
#: A credential or key. Never logged, never packaged, never echoed in an error.
SECRET = "secret" # noqa: S105 - a classification label, not a credential
@dataclass(frozen=True, slots=True)
class SettingDoc:
description: str
sensitivity: Sensitivity = Sensitivity.PUBLIC
required_in_production: bool = False
#: False when the value is re-read per request or per poll rather than only at startup.
restart_required: bool = True
def _p(description: str, **kwargs: object) -> SettingDoc:
return SettingDoc(description, **kwargs) # type: ignore[arg-type]
def _secret(description: str, *, required: bool = False) -> SettingDoc:
return SettingDoc(description, Sensitivity.SECRET, required_in_production=required)
def _internal(description: str, *, required: bool = False) -> SettingDoc:
return SettingDoc(description, Sensitivity.INTERNAL, required_in_production=required)
SETTING_DOCS: dict[str, SettingDoc] = {
# ---------------------------------------------------------------- process
"env": _p(
"Deployment profile. 'production' turns on every fail-closed startup rule; "
"'development' and 'test' report the same problems without refusing to start."
),
"api_host": _p("Interface the API binds inside its container. Leave at 0.0.0.0."),
"api_port": _p("Port the API listens on inside its container."),
"control_plane_max_payload_bytes": _p(
"Pre-parser request-body limit for public and operator control-plane routes."
),
"service_name": _p("Name this process reports in logs and audit events."),
"log_level": _p("Structured log level: DEBUG, INFO, WARNING or ERROR.", restart_required=True),
# ---------------------------------------------------------------- dependencies
"database_url": _secret(
"SQLAlchemy URL for the API's non-owner modelforge_runtime role. It may read the audit "
"trail and execute the canonical append function, but cannot mutate audit tables directly.",
required=True,
),
"migration_database_url": _secret(
"SQLAlchemy URL for the non-superuser modelforge schema-owner role. Set only in the "
"one-shot migration process; production API startup refuses when this secret is present."
),
"redis_url": _internal("Redis URL for transient request payloads and queues.", required=True),
"cors_origins": _p(
"Comma-separated exact origins allowed to call the API from a browser. "
"A wildcard is refused in production because requests are credentialed."
),
# ---------------------------------------------------------------- credentials
"operator_api_key": _secret(
"Operator API key guarding every admin route. Generate at least 32 random characters; "
"ModelForge never mints one for you.",
required=True,
),
"hf_token": _secret(
"Optional Hugging Face token, used only for acquiring gated repositories. "
"It is never passed to a runtime and never leaves the control plane."
),
"backup_encryption_key": _secret(
"Base64 AES-256 key for backup encryption. Without it no backup can be produced, and "
"without the same key no backup can be restored — store it outside this deployment.",
required=True,
),
"backup_encryption_key_id": _p(
"Identifier recorded in each backup manifest so a restore can name the key it needs."
),
# ---------------------------------------------------------------- storage
"hf_home": _p("Hugging Face cache root inside the container."),
"artifact_root": _p("Verified model artifact root. Must exist and be writable."),
"quarantine_root": _p("Where acquired artifacts are held until their checks pass."),
"runtime_artifact_root": _p("Artifact root as a runtime worker sees it on a compute node."),
"config_root": _p("Directory holding the capability, project and policy manifests."),
"backup_root": _p("Backup destination. Must exist and be writable, or backups fail closed."),
"backup_restore_root": _p("Working directory a restore stages into before it commits."),
"alembic_directory": _p("Override for the migration directory. Leave empty in a container."),
# ---------------------------------------------------------------- acquisition
"hf_timeout_seconds": _p("Per-request timeout for Hugging Face metadata calls."),
"hf_snapshot_ttl_seconds": _p("How long a resolved upstream snapshot stays cached."),
"allow_remote_code": _p(
"Whether model repositories may execute their own Python. Always false in production; "
"startup refuses any other value there."
),
# ---------------------------------------------------------------- hardware and nodes
"enable_gpu_telemetry": _p("Collect GPU telemetry on this host."),
"hardware_refresh_on_startup": _p("Run a hardware inventory pass when the process starts."),
"hardware_poll_interval_seconds": _p("Interval between hardware inventory passes."),
"node_identity": _internal("Explicit node identity. Leave empty to use the persisted file."),
"node_identity_mode": _p("'persisted' keeps a node's identity across restarts; 'auto' derives it."),
"node_identity_file": _p("Where a persisted node identity is stored."),
"node_stale_after_seconds": _p("Silence after which a node is considered stale."),
"node_offline_after_seconds": _p(
"Silence after which a node is considered offline. Must exceed the stale threshold."
),
"liveness_poll_interval_seconds": _p("How often node liveness is re-evaluated."),
"agent_max_clock_skew_seconds": _p("Clock skew tolerated on an agent report before refusal."),
"node_agent_max_payload_bytes": _p(
"Pre-parser request-body limit for enrollment and authenticated Node Agent reports."
),
"node_liveness_monitor_enabled": _p("Run the node liveness monitor in this process."),
"agent_protocol_version": _p("Agent protocol version this control plane speaks."),
# ---------------------------------------------------------------- gateway
"gateway_max_batch_size": _p("Maximum inputs accepted in a single capability invocation."),
"gateway_max_input_characters": _p("Maximum characters per input item."),
"gateway_max_payload_bytes": _p("Maximum accepted request body size."),
"gateway_request_timeout_seconds": _p(
"Total time a capability invocation may take. Must exceed the queue timeout."
),
"gateway_queue_timeout_seconds": _p("How long a request may wait for capacity before rejection."),
"serving_job_lease_seconds": _p("Lease held by a serving job before it is reclaimed."),
"serving_payload_ttl_seconds": _p("How long a request payload survives in Redis."),
# ---------------------------------------------------------------- scheduler
"scheduler_safety_reserve_bytes": _p("VRAM never offered to a placement, as an absolute floor."),
"scheduler_safety_reserve_percentage": _p(
"VRAM never offered to a placement, as a fraction. Half a device leaves nothing schedulable."
),
"scheduler_runtime_margin_bytes": _p("Headroom reserved for runtime overhead per node."),
"scheduler_deployment_margin_bytes": _p("Absolute headroom added to each deployment estimate."),
"scheduler_deployment_margin_percentage": _p("Proportional headroom added to each estimate."),
"scheduler_global_queue_limit": _p("Queued requests accepted before capacity rejection begins."),
"scheduler_telemetry_stale_seconds": _p(
"Telemetry age past which admission is blocked rather than extrapolated."
),
"scheduler_pressure_stable_seconds": _p("How long pressure must hold before the state changes."),
"scheduler_eviction_cooldown_seconds": _p("Minimum interval between evictions on a node."),
"scheduler_placement_history_limit": _p("Placement decisions retained for inspection."),
# ---------------------------------------------------------------- background work
"registry_seed_on_startup": _p("Seed the candidate and project registries from manifests."),
"serving_reconciliation_enabled": _p("Reconcile abandoned serving work in this process."),
"serving_reconciliation_interval_seconds": _p("Interval between serving reconciliation passes."),
"lifecycle_reconciliation_enabled": _p("Roll back incomplete lifecycle operations at startup."),
"migration_reconciliation_enabled": _p(
"Report interrupted migration cutovers at startup. They are never auto-resolved: external "
"alias truth cannot be inferred after a crash."
),
"observability_monitor_enabled": _p("Run SLO and alert evaluation in this process."),
"observability_poll_interval_seconds": _p("Interval between observability evaluation passes."),
"recovery_reconciliation_enabled": _p("Reconcile interrupted backups and restores at startup."),
# ---------------------------------------------------------------- recovery
"backup_pg_dump_path": _p("pg_dump executable. Must match the server major version."),
"backup_pg_restore_path": _p("pg_restore executable."),
"backup_psql_path": _p("psql executable."),
"backup_command_timeout_seconds": _p("Timeout for a dump or restore command."),
"backup_stale_after_seconds": _p("Age past which the newest verified backup raises BACKUP_STALE."),
"backup_minimum_free_bytes": _p("Free space below which a backup refuses to start."),
"backup_capacity_headroom_ratio": _p("Required free space as a multiple of the estimated size."),
"restore_allow_production_target": _p(
"Whether a restore may overwrite the live database. Keep false outside a rehearsal."
),
# ---------------------------------------------------------------- build identity
"build_commit": _p("Source commit stamped into the image at build time. Never set by hand."),
"build_timestamp": _p("Build time stamped into the image. Never set by hand."),
"build_image_digest": _p("Image digest recorded at deployment. Never set by hand."),
}
# --------------------------------------------------------------------------- deployment variables
#
# Variables the deployment reads rather than the control-plane process: Compose interpolation, the
# Node Agent and the Runtime Worker. They are not `Settings` fields, but an operator still has to
# set them, so leaving them out of the reference would recreate exactly the undocumented-variable
# problem this table exists to prevent.
DEPLOYMENT_DOCS: dict[str, SettingDoc] = {
# ---------------------------------------------------------------- network bindings
"MODELFORGE_POSTGRES_BIND": _p(
"Host address the control-plane database is published on. Defaults to 127.0.0.1; "
"publishing it more widely exposes provenance, credential hashes and the audit trail."
),
"MODELFORGE_REDIS_BIND": _p("Host address Redis is published on. Defaults to 127.0.0.1."),
"MODELFORGE_API_BIND": _p(
"Host address the API is published on. Defaults to 0.0.0.0 deliberately: the console and "
"compute nodes need it, and every admin route is operator-authenticated."
),
"MODELFORGE_WEB_BIND": _p("Host address the console is published on. Defaults to 127.0.0.1."),
"MODELFORGE_POSTGRES_PORT": _p("Host port the database is published on. Defaults to 5432."),
"MODELFORGE_REDIS_PORT": _p("Host port Redis is published on. Defaults to 6379."),
"MODELFORGE_API_PUBLISHED_PORT": _p("Host port the API is published on. Defaults to 8000."),
"MODELFORGE_WEB_PORT": _p("Host port the console is published on. Defaults to 3000."),
"MODELFORGE_DR_POSTGRES_BIND": _p("Host address for the DR rehearsal database. Loopback only."),
"MODELFORGE_DR_API_BIND": _p("Host address for the DR rehearsal API. Loopback only."),
"VITE_API_BASE_URL": _p(
"API base URL compiled into the console. Vite inlines it at build time, so changing it "
"requires rebuilding the console image, not restarting it."
),
# ---------------------------------------------------------------- production identity
"MODELFORGE_POSTGRES_DB": _p("Production database name. Required by the production overlay."),
"MODELFORGE_POSTGRES_ADMIN_USER": _internal(
"Bootstrap/admin role used only by PostgreSQL provisioning; defaults to postgres."
),
"MODELFORGE_POSTGRES_ADMIN_PASSWORD": _secret(
"Bootstrap/admin password; never passed to the migration or API container.", required=True
),
"MODELFORGE_MIGRATION_DB_PASSWORD": _secret(
"Raw password supplied to provisioning for the non-superuser modelforge owner role.",
required=True,
),
"MODELFORGE_RUNTIME_DB_PASSWORD": _secret(
"Raw password supplied to provisioning for the non-owner modelforge_runtime role.",
required=True,
),
"MODELFORGE_RUNTIME_DATABASE_URL": _secret(
"Non-owner runtime-role URL passed only to the API container.", required=True
),
"MODELFORGE_VERSION": _p(
"Exact version tag applied to built images and required when the production overlay is "
"not given explicit API and web image references."
),
"MODELFORGE_COMMIT": _p("Source commit stamped into images at build time."),
"MODELFORGE_BUILT_AT": _p("Build timestamp stamped into images."),
"MODELFORGE_API_IMAGE": _p(
"Exact tag or digest the production overlay runs for the API; never use latest."
),
"MODELFORGE_WEB_IMAGE": _p(
"Exact tag or digest the production overlay runs for the console; never use latest."
),
"MODELFORGE_NODE_AGENT_IMAGE": _p(
"Exact release tag or digest for the standalone Node Agent. The local-build fallback is "
"named local and never resolves to latest."
),
"MODELFORGE_API_IMAGE_DIGEST": _p("Digest recorded as the running API build identity."),
# ---------------------------------------------------------------- volumes
"MODELFORGE_BACKUP_VOLUME": _p("Volume or bind path backing the backup root."),
"MODELFORGE_RESTORE_VOLUME": _p("Volume or bind path backing the restore staging root."),
"MODELFORGE_AGENT_STATE_VOLUME": _p("Volume or bind path holding the agent's persisted identity."),
"MODELFORGE_AGENT_HF_CACHE_VOLUME": _p("Volume or bind path for the agent's Hugging Face cache."),
"MODELFORGE_AGENT_ARTIFACT_VOLUME": _p("Volume or bind path for verified artifacts on a node."),
"MODELFORGE_AGENT_QUARANTINE_VOLUME": _p("Volume or bind path for the node's quarantine area."),
# ---------------------------------------------------------------- node agent
"MODELFORGE_AGENT_CONTROL_PLANE_URL": _p(
"URL the agent reports to. Outbound only; the control plane never dials a node."
),
"MODELFORGE_AGENT_ENROLLMENT_TOKEN": _secret(
"Single-use enrolment token. Consumed atomically: a storm against one token produces "
"exactly one identity."
),
"MODELFORGE_AGENT_HOSTNAME": _p("Hostname the agent enrols under."),
"MODELFORGE_AGENT_ACCELERATOR_MODE": _p(
"Accelerator contract: nvidia fails closed unless NVML inventory and telemetry are valid; "
"cpu permits a legitimate CPU-only node; auto requires NVIDIA when injected devices are "
"observed. Canonical GPU Compose deployments set nvidia explicitly."
),
"MODELFORGE_AGENT_TLS_VERIFY": _p(
"Whether the agent verifies the control plane's certificate. True wherever TLS is real."
),
"MODELFORGE_AGENT_CONTROL_PLANE_HOST_ADDRESS": _internal(
"Host address mapped for a private-CA deployment."
),
"MODELFORGE_AGENT_CA_CERT_PATH": _p("Path to the private CA certificate the agent trusts."),
# ---------------------------------------------------------------- runtime worker
"MODELFORGE_RUNTIME_WORKER_ARTIFACT_ROOT": _p("Artifact root as the runtime worker sees it."),
"MODELFORGE_RUNTIME_WORKER_POLL_INTERVAL_SECONDS": _p("Worker poll interval, in seconds."),
# ---------------------------------------------------------------- provenance passthrough
"MODELFORGE_SOURCE_COMMIT": _p("Source commit reported by the deployment."),
"MODELFORGE_SOURCE_REFERENCE": _p("Git reference reported by the deployment."),
"MODELFORGE_SOURCE_REPOSITORY": _p("Repository URL reported by the deployment."),
}
@@ -0,0 +1,432 @@
from __future__ import annotations
import hashlib
import json
import re
from datetime import UTC, datetime
from typing import Any, Literal, Protocol
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from .enums import (
DeploymentChannel,
FailureCode,
FailureOwner,
HealthStatus,
MigrationStatus,
ResidencyPolicy,
UpgradeClass,
VerificationStatus,
WorkloadPriority,
)
CAPABILITY_KEY = re.compile(r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$")
SHA256 = re.compile(r"^[a-f0-9]{64}$")
COMMIT_SHA = re.compile(r"^[a-f0-9]{40,64}$")
class StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class SchemaDocument(StrictModel):
schema_: dict[str, Any] = Field(alias="schema")
class ModalityContract(StrictModel):
input: list[str] = Field(min_length=1)
output: list[str] = Field(min_length=1)
class VectorContract(StrictModel):
dimensionality: int | None = Field(default=None, gt=0)
normalized: bool | None = None
cross_deployment_compatible: bool = False
class SLOContract(StrictModel):
latency_p95_ms: int | None = Field(default=None, gt=0)
availability_percent: float | None = Field(default=None, ge=0, le=100)
max_concurrency: int | None = Field(default=None, gt=0)
class PrivacyContract(StrictModel):
classification: Literal["public", "internal", "confidential", "restricted"] = "internal"
allow_persistence: bool = False
allow_logging_payloads: bool = False
allow_network_egress: bool = False
class ResourceRequirement(StrictModel):
accelerator_required: bool = False
minimum_vram_mb: int | None = Field(default=None, ge=0)
cpu_fallback_allowed: bool = False
CapabilityCategory = Literal[
"TEXT", "RAG", "DOCUMENT", "VISION", "AUDIO", "GENERATION", "ASSISTANTS"
]
CapabilityStability = Literal["stable", "experimental", "blocked", "planned"]
EvaluationType = Literal[
"retrieval", "ocr", "visual-retrieval", "asr", "tts", "generation", "llm"
]
ResourceClass = Literal["LIGHT", "MEDIUM", "HEAVY", "EXCLUSIVE_GPU"]
class PayloadLimits(StrictModel):
max_bytes: int = Field(ge=1, le=67_108_864)
max_batch_count: int = Field(default=1, ge=1, le=64)
max_width: int | None = Field(default=None, ge=1, le=16_384)
max_height: int | None = Field(default=None, ge=1, le=16_384)
max_pages: int | None = Field(default=None, ge=1, le=128)
max_duration_seconds: float | None = Field(default=None, gt=0, le=3600)
class CapabilityEstateMetadata(StrictModel):
category: CapabilityCategory
purpose: str = Field(min_length=1, max_length=1000)
stability: CapabilityStability
resource_class: ResourceClass
evaluation_type: EvaluationType
consumers: list[str] = Field(default_factory=list)
payload_limits: PayloadLimits
class FallbackContract(StrictModel):
allowed: bool = False
mode: Literal["none", "compatible_deployment", "degrade", "hard_fail"] = "hard_fail"
capability: str | None = None
@model_validator(mode="after")
def validate_fallback(self) -> FallbackContract:
if not self.allowed and self.mode not in {"none", "hard_fail"}:
raise ValueError("disabled fallback must use none or hard_fail")
return self
class CapabilityContractManifest(StrictModel):
capability: str
version: int = Field(ge=1)
description: str = Field(min_length=1)
input_schema: dict[str, Any]
output_schema: dict[str, Any]
modalities: ModalityContract
languages: list[str] = Field(default_factory=list)
streaming: bool = False
structured_output: bool = False
vector: VectorContract | None = None
slo: SLOContract = Field(default_factory=SLOContract)
quality_metrics: list[str] = Field(min_length=1)
upgrade_class: UpgradeClass
fallback: FallbackContract = Field(default_factory=FallbackContract)
privacy: PrivacyContract = Field(default_factory=PrivacyContract)
resources: ResourceRequirement = Field(default_factory=ResourceRequirement)
production_priority: WorkloadPriority
default_residency: ResidencyPolicy
estate: CapabilityEstateMetadata
@field_validator("capability")
@classmethod
def validate_key(cls, value: str) -> str:
if not CAPABILITY_KEY.fullmatch(value):
raise ValueError("capability must be a dotted, lowercase logical key")
return value
@model_validator(mode="after")
def vector_safety(self) -> CapabilityContractManifest:
vector_output = self.vector is not None or "embedding" in self.capability
if vector_output and not self.vector:
raise ValueError("vector-producing capabilities require a vector contract")
if (
vector_output
and self.vector is not None
and not self.vector.cross_deployment_compatible
and self.upgrade_class is not UpgradeClass.REQUIRES_REINDEX
):
raise ValueError("incompatible vector spaces require requires_reindex")
return self
class ProjectDefinition(StrictModel):
id: str = Field(pattern=r"^[a-z][a-z0-9-]*$")
name: str = Field(min_length=1)
description: str = Field(min_length=1)
class ProjectBindingManifest(StrictModel):
contract_version: int = Field(ge=1)
channel: DeploymentChannel
priority: WorkloadPriority
optional: bool = False
fallback: FallbackContract = Field(default_factory=FallbackContract)
migration_support: Literal["none", "reindex", "schema"] = "none"
slo: SLOContract = Field(default_factory=SLOContract)
benchmark_suites: list[str] = Field(default_factory=list)
class ProjectManifest(StrictModel):
project: ProjectDefinition
bindings: dict[str, ProjectBindingManifest]
notes: list[str] = Field(default_factory=list)
@field_validator("bindings")
@classmethod
def validate_bindings(
cls, value: dict[str, ProjectBindingManifest]
) -> dict[str, ProjectBindingManifest]:
for key in value:
if not CAPABILITY_KEY.fullmatch(key):
raise ValueError(f"invalid capability binding key: {key}")
return value
class CandidateManifest(StrictModel):
id: str
display_name: str
source: str
intended_capabilities: list[str] = Field(min_length=1)
proposed_role: str
preferred_runtime: str | None = None
verification_status: VerificationStatus = VerificationStatus.UNVERIFIED
deployment_status: Literal["not_deployed"] = "not_deployed"
class CandidateRegistryManifest(StrictModel):
candidates: list[CandidateManifest]
class BenchmarkSuiteDefinition(StrictModel):
id: str
capability: str
version: int = Field(ge=1)
purpose: str
dataset_revision: str
runnable: bool = False
class BenchmarkSuiteManifest(StrictModel):
suite: BenchmarkSuiteDefinition
metrics: dict[str, list[str]]
performance: dict[str, list[str]]
release_policy: dict[str, Any]
@model_validator(mode="after")
def require_pinned_dataset_for_runnable_suite(self) -> BenchmarkSuiteManifest:
if self.suite.runnable and self.suite.dataset_revision.startswith("pending"):
raise ValueError("runnable benchmark suites require a pinned dataset revision")
return self
class SecurityDefaults(StrictModel):
trust_remote_code: Literal[False] = False
prefer_safetensors: Literal[True] = True
require_exact_revision_for_approval: Literal[True] = True
require_artifact_digest: Literal[True] = True
inference_network_egress: Literal[False] = False
expose_runtime_workers: Literal[False] = False
class LifecycleDefaults(StrictModel):
automatic_production_promotion: Literal[False] = False
require_local_benchmark_for_stable: Literal[True] = True
retain_previous_stable_as_rollback_target: Literal[True] = True
destructive_cleanup_requires_explicit_approval: Literal[True] = True
class SchedulerDefaults(StrictModel):
priority_order: list[WorkloadPriority]
reserve_vram_mb: int = Field(ge=0)
default_warm_ttl_seconds: int = Field(ge=0)
benchmark_may_disrupt_production: Literal[False] = False
class UpgradeDefaults(StrictModel):
default_behavioral_change_class: Literal[UpgradeClass.BEHAVIORAL]
vector_producer_default_change_class: Literal[UpgradeClass.REQUIRES_REINDEX]
schema_change_requires_contract_version_bump: Literal[True] = True
class PolicyDefaults(StrictModel):
security: SecurityDefaults
lifecycle: LifecycleDefaults
scheduler: SchedulerDefaults
upgrades: UpgradeDefaults
class ArtifactProvenance(StrictModel):
upstream_repository: str
resolved_commit_sha: str
filename: str
artifact_type: str
sha256: str
source_artifact_sha256: str | None = None
derivation_tool: str | None = None
derivation_tool_version: str | None = None
derivation_arguments: dict[str, Any] = Field(default_factory=dict)
imported_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
@field_validator("resolved_commit_sha")
@classmethod
def validate_commit(cls, value: str) -> str:
value = value.lower()
if not COMMIT_SHA.fullmatch(value):
raise ValueError("resolved commit must be a 40-64 character hexadecimal digest")
return value
@field_validator("sha256", "source_artifact_sha256")
@classmethod
def validate_digest(cls, value: str | None) -> str | None:
if value is not None and not SHA256.fullmatch(value.lower()):
raise ValueError("artifact digest must be lowercase SHA-256")
return value.lower() if value else None
@model_validator(mode="after")
def validate_lineage(self) -> ArtifactProvenance:
derived = self.source_artifact_sha256 is not None
if derived and (not self.derivation_tool or not self.derivation_tool_version):
raise ValueError("derived artifacts require derivation tool and version")
return self
class RuntimeProfileContract(StrictModel):
runtime_type: Literal["vllm", "transformers", "diffusers", "llama_cpp", "custom"]
runtime_version: str
runtime_image_digest: str | None = Field(default=None, pattern=r"^sha256:[a-f0-9]{64}$")
artifact_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
quantization: str | None = None
context_length: int | None = Field(default=None, gt=0)
max_concurrency: int = Field(default=1, gt=0)
launch_arguments: dict[str, Any] = Field(default_factory=dict)
environment_constraints: dict[str, Any] = Field(default_factory=dict)
trust_remote_code: bool = False
network_egress: bool = False
@property
def fingerprint(self) -> str:
payload = self.model_dump(mode="json", exclude_none=True)
return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
class ResourceEnvelopeContract(StrictModel):
deployment_id: str
accelerator_kind: str
context_length: int | None = Field(default=None, gt=0)
concurrency: int = Field(ge=1)
batch_size: int = Field(ge=1)
idle_vram_mb: int = Field(ge=0)
peak_vram_mb: int = Field(ge=0)
safety_margin_mb: int = Field(default=1024, ge=0)
@model_validator(mode="after")
def peak_not_below_idle(self) -> ResourceEnvelopeContract:
if self.peak_vram_mb < self.idle_vram_mb:
raise ValueError("peak VRAM cannot be below idle VRAM")
return self
class LeaseRequest(StrictModel):
deployment_id: str
priority: WorkloadPriority
residency: ResidencyPolicy
envelope: ResourceEnvelopeContract
warm_ttl_seconds: int = Field(default=900, ge=0)
exclusive: bool = False
allow_cpu_fallback: bool = False
class BenchmarkEnvironmentFingerprint(StrictModel):
runtime_type: str
runtime_version: str
runtime_image_digest: str | None = None
launch_arguments: dict[str, Any]
cuda_version: str | None
driver_version: str | None
accelerator_name: str
accelerator_uuid: str | None
context_length: int | None
concurrency: int = Field(ge=1)
seed: int | None
operating_system: str
python_version: str | None = None
@property
def digest(self) -> str:
raw = json.dumps(self.model_dump(mode="json"), sort_keys=True, separators=(",", ":"))
return hashlib.sha256(raw.encode()).hexdigest()
def comparable_with(self, other: BenchmarkEnvironmentFingerprint) -> bool:
relevant = (
"runtime_type",
"runtime_version",
"runtime_image_digest",
"launch_arguments",
"cuda_version",
"driver_version",
"accelerator_name",
"context_length",
"concurrency",
)
return all(getattr(self, field) == getattr(other, field) for field in relevant)
class BenchmarkRunContract(StrictModel):
deployment_id: str
model_revision_sha: str = Field(pattern=r"^[a-f0-9]{40,64}$")
artifact_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
suite_key: str
suite_revision: str
dataset_revision: str
environment: BenchmarkEnvironmentFingerprint
started_at: datetime
completed_at: datetime | None = None
class MigrationContract(StrictModel):
project_id: str
capability: str
source_deployment_id: str
target_deployment_id: str
upgrade_class: UpgradeClass
current_index_ref: str
shadow_index_ref: str
status: MigrationStatus = MigrationStatus.PLANNED
rollback_retain_until: datetime
@model_validator(mode="after")
def require_reindex(self) -> MigrationContract:
if (
"embedding" in self.capability
and self.upgrade_class is not UpgradeClass.REQUIRES_REINDEX
):
raise ValueError("embedding migrations must be classified requires_reindex")
if self.current_index_ref == self.shadow_index_ref:
raise ValueError("shadow index must be distinct from current production index")
return self
class LayerHealth(StrictModel):
process: HealthStatus
runtime: HealthStatus
model: HealthStatus
capability: HealthStatus
project: HealthStatus
reasons: list[str] = Field(default_factory=list)
class FailurePolicy(StrictModel):
code: FailureCode
owner: FailureOwner
retryable: bool
fallback_allowed: bool
hard_failure: bool
class RuntimeAdapter(Protocol):
"""Stable control-plane boundary; concrete adapters arrive in M5."""
runtime_type: str
def validate_profile(self, profile: RuntimeProfileContract) -> None: ...
def probe(self, profile: RuntimeProfileContract) -> LayerHealth: ...
def load(self, profile: RuntimeProfileContract, lease_id: str) -> str: ...
def unload(self, deployment_id: str) -> None: ...
+200
View File
@@ -0,0 +1,200 @@
from enum import StrEnum
class ModelLifecycle(StrEnum):
DISCOVERED = "discovered"
CANDIDATE = "candidate"
DOWNLOADING = "downloading"
QUARANTINED = "quarantined"
VERIFIED = "verified"
TESTING = "testing"
APPROVED = "approved"
STANDBY = "standby"
ACTIVE = "active"
DEPRECATED = "deprecated"
ARCHIVED = "archived"
REJECTED = "rejected"
INCOMPATIBLE = "incompatible"
SECURITY_BLOCKED = "security_blocked"
LICENSE_BLOCKED = "license_blocked"
class UpgradeClass(StrEnum):
TRANSPARENT = "transparent"
BEHAVIORAL = "behavioral"
REQUIRES_REINDEX = "requires_reindex"
SCHEMA_BREAKING = "schema_breaking"
class DeploymentChannel(StrEnum):
STABLE = "stable"
CANDIDATE = "candidate"
EXPERIMENTAL = "experimental"
ARCHIVE = "archive"
class ResidencyPolicy(StrEnum):
ALWAYS_WARM = "always_warm"
KEEP_WARM = "keep_warm"
LOAD_ON_DEMAND = "load_on_demand"
EXCLUSIVE = "exclusive"
LAB_ONLY = "lab_only"
class WorkloadPriority(StrEnum):
PRODUCTION = "production"
INTERACTIVE = "interactive"
BACKGROUND = "background"
BENCHMARK = "benchmark"
MAINTENANCE = "maintenance"
class VerificationStatus(StrEnum):
UNKNOWN = "unknown"
UNVERIFIED = "unverified"
QUARANTINED = "quarantined"
VERIFIED = "verified"
BLOCKED = "blocked"
class LicenseStatus(StrEnum):
UNKNOWN = "unknown"
REVIEW_REQUIRED = "review_required"
APPROVED = "approved"
BLOCKED = "blocked"
class ArtifactStatus(StrEnum):
REMOTE = "remote"
LOCAL = "local"
VERIFYING = "verifying"
VERIFIED = "verified"
MISSING = "missing"
CORRUPT = "corrupt"
QUARANTINED = "quarantined"
ARCHIVED = "archived"
UNREACHABLE = "unreachable"
class StorageRootStatus(StrEnum):
UNKNOWN = "unknown"
READY = "ready"
READ_ONLY = "read_only"
CAPACITY_BLOCKED = "capacity_blocked"
UNAVAILABLE = "unavailable"
DEPRECATED = "deprecated"
class DeploymentStatus(StrEnum):
DRAFT = "draft"
VALIDATING = "validating"
READY = "ready"
ACTIVE = "active"
UNHEALTHY = "unhealthy"
RETIRED = "retired"
class MigrationStatus(StrEnum):
PLANNED = "planned"
BACKFILLING = "backfilling"
VALIDATING = "validating"
SHADOWING = "shadowing"
READY = "ready"
PROMOTED = "promoted"
ROLLED_BACK = "rolled_back"
FAILED = "failed"
class HealthStatus(StrEnum):
HEALTHY = "healthy"
DEGRADED = "degraded"
UNHEALTHY = "unhealthy"
UNAVAILABLE = "unavailable"
UNKNOWN = "unknown"
class FailureCode(StrEnum):
MODEL_LOAD_FAILED = "MODEL_LOAD_FAILED"
GPU_OOM = "GPU_OOM"
RUNTIME_CRASH = "RUNTIME_CRASH"
TIMEOUT = "TIMEOUT"
INVALID_OUTPUT = "INVALID_OUTPUT"
HEALTHCHECK_FAILED = "HEALTHCHECK_FAILED"
ARTIFACT_CORRUPT = "ARTIFACT_CORRUPT"
DRIVER_ERROR = "DRIVER_ERROR"
CAPABILITY_UNAVAILABLE = "CAPABILITY_UNAVAILABLE"
RUNTIME_INCOMPATIBLE = "RUNTIME_INCOMPATIBLE"
ARTIFACT_INCOMPLETE = "ARTIFACT_INCOMPLETE"
EXECUTION_NOT_APPROVED = "EXECUTION_NOT_APPROVED"
RUNTIME_DEPENDENCY_MISSING = "RUNTIME_DEPENDENCY_MISSING"
OFFLINE_LOAD_VIOLATION = "OFFLINE_LOAD_VIOLATION"
UNLOAD_FAILED = "UNLOAD_FAILED"
GPU_MEMORY_NOT_RECLAIMED = "GPU_MEMORY_NOT_RECLAIMED"
NO_ELIGIBLE_NODE = "NO_ELIGIBLE_NODE"
INSUFFICIENT_SCHEDULABLE_VRAM = "INSUFFICIENT_SCHEDULABLE_VRAM"
QUEUE_FULL = "QUEUE_FULL"
LEASE_TIMEOUT = "LEASE_TIMEOUT"
RESIDENCY_LOAD_FAILED = "RESIDENCY_LOAD_FAILED"
RESIDENCY_UNLOAD_FAILED = "RESIDENCY_UNLOAD_FAILED"
AUTHORIZATION_DENIED = "AUTHORIZATION_DENIED"
RATE_LIMITED = "RATE_LIMITED"
EXTERNAL_GPU_PRESSURE = "EXTERNAL_GPU_PRESSURE"
STALE_RESOURCE_ENVELOPE = "STALE_RESOURCE_ENVELOPE"
class FailureOwner(StrEnum):
RUNTIME_ADAPTER = "runtime_adapter"
SCHEDULER = "scheduler"
GATEWAY = "gateway"
CONTROL_PLANE = "control_plane"
OPERATOR = "operator"
class Availability(StrEnum):
KNOWN = "known"
UNKNOWN = "unknown"
UNSUPPORTED = "unsupported"
UNAVAILABLE = "unavailable"
TEMPORARILY_FAILED = "temporarily_failed"
class HardwareStatus(StrEnum):
UNKNOWN = "unknown"
ACTIVE = "active"
MISSING = "missing"
UNAVAILABLE = "unavailable"
DEGRADED = "degraded"
PENDING = "pending"
DECOMMISSIONED = "decommissioned"
class InventorySource(StrEnum):
HOST = "host"
NVIDIA_NVML = "nvidia_nvml"
class InventoryRunStatus(StrEnum):
RUNNING = "running"
SUCCEEDED = "succeeded"
DEGRADED = "degraded"
FAILED = "failed"
class NodeLiveness(StrEnum):
ONLINE = "online"
STALE = "stale"
OFFLINE = "offline"
DISABLED = "disabled"
DECOMMISSIONED = "decommissioned"
class AgentHealth(StrEnum):
HEALTHY = "healthy"
INCOMPATIBLE = "incompatible"
REVOKED = "revoked"
UNKNOWN = "unknown"
class ObservationSource(StrEnum):
LOCAL_CONTROL_PLANE = "local_control_plane"
REMOTE_AGENT = "remote_agent"
@@ -0,0 +1,593 @@
from __future__ import annotations
import math
import re
import uuid
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
MetricName = Literal["recall_at_5", "recall_at_10", "mrr", "ndcg_at_10"]
TargetKind = Literal["current", "shadow"]
_KEY = re.compile(r"^[a-z0-9][a-z0-9_-]{0,127}$")
class EvaluationModel(BaseModel):
model_config = ConfigDict(extra="forbid", from_attributes=True)
class EvaluationCaseCreate(EvaluationModel):
case_key: str
query: str = Field(min_length=1, max_length=4000)
relevant_chunk_ids: list[uuid.UUID] = Field(min_length=1, max_length=100)
relevant_document_ids: list[uuid.UUID] = Field(default_factory=list, max_length=100)
relevance_grades: dict[str, int] = Field(default_factory=dict)
label_provenance: dict[str, Any]
critical: bool = False
review_status: Literal["reviewed", "approved"]
@field_validator("case_key")
@classmethod
def valid_key(cls, value: str) -> str:
if not _KEY.fullmatch(value):
raise ValueError("case_key must be a safe identifier")
return value
@field_validator("relevant_chunk_ids")
@classmethod
def unique_relevance(cls, value: list[uuid.UUID]) -> list[uuid.UUID]:
if len(set(value)) != len(value):
raise ValueError("relevant chunk ids must be unique")
return value
class EvaluationRevisionCreate(EvaluationModel):
revision: str
dataset_revision: str
metrics: list[MetricName] = ["recall_at_5", "recall_at_10", "mrr", "ndcg_at_10"]
top_k: int = Field(default=10, ge=10, le=100)
retrieval_settings: dict[str, Any]
thresholds: dict[str, float] = Field(default_factory=dict)
cases: list[EvaluationCaseCreate] = Field(min_length=1, max_length=500)
@model_validator(mode="after")
def validate_revision(self) -> EvaluationRevisionCreate:
if not _KEY.fullmatch(self.revision) or not _KEY.fullmatch(self.dataset_revision):
raise ValueError("revision identifiers must be safe")
if len(set(self.metrics)) != len(self.metrics):
raise ValueError("metrics must be unique")
if len({case.case_key for case in self.cases}) != len(self.cases):
raise ValueError("case keys must be unique")
return self
class EvaluationSuiteCreate(EvaluationModel):
project_id: uuid.UUID
key: str
name: str = Field(min_length=1, max_length=255)
description: str = Field(min_length=1, max_length=4000)
revision: EvaluationRevisionCreate
@field_validator("key")
@classmethod
def valid_key(cls, value: str) -> str:
if not _KEY.fullmatch(value):
raise ValueError("suite key must be a safe identifier")
return value
class EvaluationSuiteResponse(EvaluationModel):
id: uuid.UUID
project_id: uuid.UUID
key: str
name: str
description: str
latest_revision_id: uuid.UUID
latest_revision: str
case_count: int
critical_case_count: int
created_at: datetime
class EvaluationCaseDefinitionResponse(EvaluationModel):
id: uuid.UUID
suite_revision_id: uuid.UUID
case_key: str
query: str
relevant_chunk_ids: list[str]
relevant_document_ids: list[str]
relevance_grades: dict[str, int]
label_provenance: dict[str, Any]
critical: bool
review_status: str
created_at: datetime
class EvaluationRunCreate(EvaluationModel):
project_id: uuid.UUID
suite_revision_id: uuid.UUID
target_kind: TargetKind
target_index_ref: str = Field(min_length=1, max_length=512)
embedding_space_ref: str = Field(min_length=1, max_length=255)
capability_deployment_id: uuid.UUID | None = None
corpus_revision: str = Field(min_length=1, max_length=128)
retrieval_config_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
environment_fingerprint: dict[str, Any]
class RankedResult(EvaluationModel):
chunk_id: uuid.UUID
document_id: uuid.UUID | None = None
score: float
@field_validator("score")
@classmethod
def finite_score(cls, value: float) -> float:
if not math.isfinite(value):
raise ValueError("score must be finite")
return value
class CandidatePoolEntry(EvaluationModel):
id: str = Field(min_length=1, max_length=255)
document_id: str | None = Field(default=None, max_length=255)
score: float
content_sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
@field_validator("score")
@classmethod
def finite_score(cls, value: float) -> float:
if not math.isfinite(value):
raise ValueError("score must be finite")
return value
class RetrievalCandidatePoolCreate(EvaluationModel):
project_id: uuid.UUID
suite_revision_id: uuid.UUID
evaluation_case_id: uuid.UUID
source_embedding_space: str = Field(min_length=1, max_length=255)
source_index_ref: str = Field(min_length=1, max_length=512)
corpus_revision: str = Field(min_length=1, max_length=128)
retrieval_config_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
ordered_candidates: list[CandidatePoolEntry] = Field(min_length=1, max_length=40)
@model_validator(mode="after")
def unique_ordered_ids(self) -> RetrievalCandidatePoolCreate:
ids = [item.id for item in self.ordered_candidates]
if len(set(ids)) != len(ids):
raise ValueError("candidate pool ids must be unique")
return self
class RetrievalCandidatePoolResponse(EvaluationModel):
id: uuid.UUID
project_id: uuid.UUID
suite_revision_id: uuid.UUID
evaluation_case_id: uuid.UUID
source_embedding_space: str
source_index_ref: str
corpus_revision: str
retrieval_config_digest: str
candidate_count: int
ordered_candidates: list[dict[str, Any]]
fingerprint: str
created_at: datetime
immutable_at: datetime
class RetrievalPipelineIdentityCreate(EvaluationModel):
project_id: uuid.UUID
embedding_space_ref: str = Field(min_length=1, max_length=255)
sparse_config_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
fusion_config_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
reranker_deployment_id: uuid.UUID | None = None
reranker_config_digest: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
candidate_k: Literal[40] = 40
output_k: int = Field(default=10, ge=1, le=10)
configuration: dict[str, Any] = Field(default_factory=dict)
@model_validator(mode="after")
def reranker_identity_is_complete(self) -> RetrievalPipelineIdentityCreate:
if (self.reranker_deployment_id is None) != (self.reranker_config_digest is None):
raise ValueError("reranker deployment and config digest must be supplied together")
return self
class RetrievalPipelineIdentityResponse(EvaluationModel):
id: uuid.UUID
project_id: uuid.UUID
embedding_space_ref: str
sparse_config_digest: str
fusion_config_digest: str
reranker_deployment_id: uuid.UUID | None
reranker_config_digest: str | None
candidate_k: int
output_k: int
identity_digest: str
configuration: dict[str, Any]
migration_class: str
created_at: datetime
immutable_at: datetime
class RerankingRunCreate(EvaluationModel):
project_id: uuid.UUID
suite_revision_id: uuid.UUID
pipeline_identity_id: uuid.UUID
control_pipeline_identity_id: uuid.UUID
candidate_pool_ids: list[uuid.UUID] = Field(min_length=1, max_length=500)
corpus_revision: str = Field(min_length=1, max_length=128)
environment_fingerprint: dict[str, Any]
class RerankingCaseResultCreate(EvaluationModel):
case_id: uuid.UUID
candidate_pool_id: uuid.UUID
ranked_results: list[RankedResult] = Field(min_length=1, max_length=10)
retrieval_latency_ms: float = Field(ge=0, le=3_600_000)
rerank_latency_ms: float = Field(ge=0, le=3_600_000)
total_latency_ms: float = Field(ge=0, le=3_600_000)
error_code: str | None = Field(default=None, max_length=64)
class RerankingRunComplete(EvaluationModel):
results: list[RerankingCaseResultCreate] = Field(min_length=1, max_length=500)
class RerankingRunResponse(EvaluationModel):
id: uuid.UUID
project_id: uuid.UUID
suite_revision_id: uuid.UUID
pipeline_identity_id: uuid.UUID
control_pipeline_identity_id: uuid.UUID
reranker_deployment_id: uuid.UUID | None
status: str
corpus_revision: str
candidate_pool_set_fingerprint: str
environment_fingerprint: dict[str, Any]
environment_digest: str
expected_cases: int
completed_cases: int
error_count: int
aggregate_metrics: dict[str, float]
latency_metrics: dict[str, float]
started_at: datetime | None
completed_at: datetime | None
created_at: datetime
class RerankingCaseResultResponse(EvaluationModel):
id: uuid.UUID
run_id: uuid.UUID
case_id: uuid.UUID
case_key: str
critical: bool
candidate_pool_id: uuid.UUID
ranked_results: list[dict[str, Any]]
relevant_results: list[str]
first_relevant_rank: int | None
metrics: dict[str, float]
retrieval_latency_ms: float
rerank_latency_ms: float
total_latency_ms: float
error_code: str | None
class DiscoveryCandidateAssessmentCreate(EvaluationModel):
model_id: uuid.UUID | None = None
upstream_snapshot_id: uuid.UUID
candidate_key: str
repository_id: str = Field(min_length=3, max_length=255)
resolved_commit_sha: str = Field(pattern=r"^[0-9a-f]{40,64}$")
artifact_evidence: dict[str, Any]
security_state: dict[str, Any]
license_state: dict[str, Any]
gpu_fit: dict[str, Any]
status: Literal[
"shortlisted",
"preflight_passed",
"evaluated",
"discovery_security_blocked",
"discovery_gpu_fit_blocked",
"license_blocked",
"not_selected",
]
rationale: str = Field(min_length=10, max_length=4000)
@field_validator("candidate_key")
@classmethod
def valid_candidate_key(cls, value: str) -> str:
if not _KEY.fullmatch(value):
raise ValueError("candidate_key must be a safe identifier")
return value
class DiscoveryCandidateAssessmentResponse(DiscoveryCandidateAssessmentCreate):
id: uuid.UUID
evidence_fingerprint: str
created_at: datetime
immutable_at: datetime
class EvaluationCaseResultCreate(EvaluationModel):
case_id: uuid.UUID
ranked_results: list[RankedResult] = Field(max_length=100)
latency_ms: float = Field(ge=0, le=3_600_000)
error_code: str | None = Field(default=None, max_length=64)
class EvaluationRunComplete(EvaluationModel):
results: list[EvaluationCaseResultCreate] = Field(min_length=1, max_length=500)
class EvaluationRunResponse(EvaluationModel):
id: uuid.UUID
project_id: uuid.UUID
suite_revision_id: uuid.UUID
target_kind: str
target_index_ref: str
embedding_space_ref: str
capability_deployment_id: uuid.UUID | None
status: str
corpus_revision: str
retrieval_config_digest: str
environment_fingerprint: dict[str, Any]
environment_digest: str
expected_cases: int
completed_cases: int
error_count: int
aggregate_metrics: dict[str, float]
started_at: datetime | None
completed_at: datetime | None
created_at: datetime
class EvaluationCaseResultResponse(EvaluationModel):
id: uuid.UUID
run_id: uuid.UUID
case_id: uuid.UUID
case_key: str
critical: bool
ranked_results: list[dict[str, Any]]
relevant_results: list[str]
first_relevant_rank: int | None
metrics: dict[str, float]
latency_ms: float
error_code: str | None
class EvaluationComparisonCreate(EvaluationModel):
baseline_run_id: uuid.UUID
candidate_run_id: uuid.UUID
require_no_critical_regressions: bool = True
maximum_error_rate: float = Field(default=0.0, ge=0, le=1)
minimum_recall_at_10_delta: float = Field(default=0.0, ge=-1, le=1)
class EvaluationComparisonResponse(EvaluationModel):
id: uuid.UUID
project_id: uuid.UUID
baseline_run_id: uuid.UUID
candidate_run_id: uuid.UUID
comparability: str
comparability_evidence: dict[str, Any]
metric_deltas: dict[str, float]
improved_cases: int
unchanged_cases: int
regressed_cases: int
critical_regressions: int
case_comparisons: list[dict[str, Any]]
promotion_eligibility: str
eligibility_evidence: dict[str, Any]
created_at: datetime
class ModelComparisonCandidateCreate(EvaluationModel):
candidate_key: str
label: str = Field(min_length=1, max_length=255)
status: Literal["evaluated", "blocked", "unknown"]
evaluation_run_id: uuid.UUID | None = None
candidate_deployment_id: uuid.UUID | None = None
embedding_space: str | None = Field(default=None, max_length=255)
artifact_size_bytes: int | None = Field(default=None, ge=0)
latency_ms: dict[str, float] = Field(default_factory=dict)
resource_evidence: dict[str, Any] = Field(default_factory=dict)
migration_impact: dict[str, Any] = Field(default_factory=dict)
security_state: dict[str, Any] = Field(default_factory=dict)
provenance: dict[str, Any] = Field(default_factory=dict)
blockers: list[str] = Field(default_factory=list, max_length=32)
candidate_kind: Literal["embedding_deployment", "retrieval_pipeline"] = "embedding_deployment"
pipeline_identity_id: uuid.UUID | None = None
@field_validator("candidate_key")
@classmethod
def valid_candidate_key(cls, value: str) -> str:
if not _KEY.fullmatch(value):
raise ValueError("candidate_key must be a safe identifier")
return value
@model_validator(mode="after")
def evaluated_run_required(self) -> ModelComparisonCandidateCreate:
if (
self.status == "evaluated"
and self.candidate_kind == "embedding_deployment"
and self.evaluation_run_id is None
):
raise ValueError("evaluated embedding candidate requires evaluation_run_id")
if self.candidate_kind == "retrieval_pipeline" and self.evaluation_run_id is not None:
raise ValueError("retrieval pipeline evidence must come from a fixed-pool run")
if self.status != "evaluated" and self.evaluation_run_id is not None:
raise ValueError("blocked or unknown candidate cannot claim an evaluation run")
if self.candidate_kind == "retrieval_pipeline" and self.pipeline_identity_id is None:
raise ValueError("retrieval pipeline candidate requires pipeline_identity_id")
return self
class ModelComparisonCreate(EvaluationModel):
project_id: uuid.UUID
capability_contract_id: uuid.UUID
suite_revision_id: uuid.UUID
current_run_id: uuid.UUID
title: str = Field(min_length=1, max_length=255)
candidates: list[ModelComparisonCandidateCreate] = Field(min_length=1, max_length=20)
@model_validator(mode="after")
def unique_candidates(self) -> ModelComparisonCreate:
if len({item.candidate_key for item in self.candidates}) != len(self.candidates):
raise ValueError("candidate keys must be unique")
return self
class ModelComparisonResponse(EvaluationModel):
id: uuid.UUID
project_id: uuid.UUID
capability_contract_id: uuid.UUID
suite_revision_id: uuid.UUID
current_run_id: uuid.UUID
title: str
candidates: list[dict[str, Any]]
comparability: str
evidence_fingerprint: str
created_at: datetime
class AdvisorPolicyUpdate(EvaluationModel):
maximum_latency_p95_regression_ratio: float = Field(ge=0, le=10)
minimum_metric_deltas: dict[MetricName, float]
rationale: str = Field(min_length=20, max_length=4000)
class AdvisorPolicyResponse(EvaluationModel):
id: uuid.UUID
key: str
required_evidence_level: str
critical_regression_hard_block: bool
maximum_latency_p95_regression_ratio: float
minimum_metric_deltas: dict[str, float]
require_verified_supply_chain: bool
require_runtime_fit: bool
rationale: str
version: int
created_at: datetime
updated_at: datetime
class AdvisorRecommendationCreate(EvaluationModel):
candidate_key: str
@field_validator("candidate_key")
@classmethod
def valid_candidate_key(cls, value: str) -> str:
if not _KEY.fullmatch(value):
raise ValueError("candidate_key must be a safe identifier")
return value
class AdvisorRecommendationDismiss(EvaluationModel):
dismissed_by: str = Field(min_length=1, max_length=255)
reason: str = Field(min_length=10, max_length=2000)
class AdvisorRecommendationResponse(EvaluationModel):
id: uuid.UUID
comparison_id: uuid.UUID
project_id: uuid.UUID
capability_contract_id: uuid.UUID
policy_id: uuid.UUID
candidate_key: str
current_deployment_id: uuid.UUID | None
candidate_deployment_id: uuid.UUID | None
current_embedding_space: str
candidate_embedding_space: str | None
target_kind: Literal["embedding_deployment", "retrieval_pipeline"]
current_pipeline_identity_id: uuid.UUID | None
candidate_pipeline_identity_id: uuid.UUID | None
verdict: Literal[
"KEEP_CURRENT",
"KEEP_CURRENT_EMBEDDING_ADD_RERANKER_CANDIDATE",
"PROMOTION_ELIGIBLE",
"PROMOTION_NOT_RECOMMENDED",
"REQUIRES_MORE_EVIDENCE",
]
confidence: str
evidence_level: str
quality_deltas: dict[str, float]
latency_deltas: dict[str, Any]
resource_deltas: dict[str, Any]
migration_impact: dict[str, Any]
security_state: dict[str, Any]
key_improvements: list[str]
blockers: list[str]
policy_snapshot: dict[str, Any]
evidence_fingerprint: str
status: str
generated_at: datetime
dismissed_at: datetime | None
dismissed_by: str | None
dismissal_reason: str | None
class EmbeddingMigrationCreate(EvaluationModel):
source_embedding_space: str = Field(min_length=1, max_length=255)
target_embedding_space_id: uuid.UUID
source_index_ref: str = Field(min_length=1, max_length=512)
target_index_ref: str = Field(min_length=1, max_length=512)
corpus_revision: str = Field(min_length=1, max_length=128)
total_chunks: int = Field(ge=1, le=10_000_000)
batch_size: int = Field(default=32, ge=1, le=256)
concurrency: int = Field(default=1, ge=1, le=8)
class MigrationUpdate(EvaluationModel):
status: Literal[
"preflight",
"backfilling",
"validating",
"ready_for_evaluation",
"evaluated",
"promotion_eligible",
"not_eligible",
"failed",
"cancelled",
]
completed_chunks: int = Field(ge=0)
failed_chunks: int = Field(ge=0)
retried_chunks: int = Field(ge=0)
preflight_evidence: dict[str, Any] = Field(default_factory=dict)
progress_evidence: dict[str, Any] = Field(default_factory=dict)
validation_evidence: dict[str, Any] = Field(default_factory=dict)
operational_metrics: dict[str, Any] = Field(default_factory=dict)
failure_code: str | None = Field(default=None, max_length=64)
failure_message: str | None = Field(default=None, max_length=2000)
class EmbeddingMigrationResponse(EvaluationModel):
id: uuid.UUID
project_id: uuid.UUID
source_embedding_space: str
target_embedding_space_id: uuid.UUID
source_index_ref: str
target_index_ref: str
corpus_revision: str
status: str
total_chunks: int
completed_chunks: int
failed_chunks: int
retried_chunks: int
batch_size: int
concurrency: int
priority: str
preflight_evidence: dict[str, Any]
progress_evidence: dict[str, Any]
validation_evidence: dict[str, Any]
operational_metrics: dict[str, Any]
evaluation_eligibility: bool
cancel_requested: bool
failure_code: str | None
failure_message: str | None
started_at: datetime | None
finished_at: datetime | None
created_at: datetime
updated_at: datetime
@@ -0,0 +1,246 @@
from __future__ import annotations
import hashlib
import json
from datetime import UTC, datetime
from typing import Protocol
from pydantic import BaseModel, ConfigDict, Field, model_validator
from modelforge_api.domain.enums import (
AgentHealth,
Availability,
HardwareStatus,
InventorySource,
NodeLiveness,
ObservationSource,
)
class HardwareModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class ObservedValue[T](HardwareModel):
value: T | None = None
availability: Availability
reason: str | None = None
@model_validator(mode="after")
def value_matches_availability(self) -> ObservedValue[T]:
if self.availability is Availability.KNOWN and self.value is None:
raise ValueError("known observations require a value")
if self.availability is not Availability.KNOWN and self.value is not None:
raise ValueError("non-known observations cannot carry a value")
return self
@classmethod
def known(cls, value: T) -> ObservedValue[T]:
return cls(value=value, availability=Availability.KNOWN)
@classmethod
def absent(cls, availability: Availability, reason: str | None = None) -> ObservedValue[T]:
return cls(availability=availability, reason=reason)
class StorageObservation(HardwareModel):
purpose: str
path: str
total_bytes: ObservedValue[int]
used_bytes: ObservedValue[int]
free_bytes: ObservedValue[int]
class HostInventory(HardwareModel):
identity_key: str
identity_source: str
hostname: str
display_name: str
os_name: str
os_version: ObservedValue[str]
architecture: str
kernel_version: ObservedValue[str]
cpu_model: ObservedValue[str]
logical_cpu_count: ObservedValue[int]
physical_core_count: ObservedValue[int]
total_ram_bytes: ObservedValue[int]
available_ram_bytes: ObservedValue[int]
agent_version: str
inventory_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
storage: list[StorageObservation] = Field(default_factory=list)
metadata: dict[str, str] = Field(default_factory=dict)
class AcceleratorInventory(HardwareModel):
device_index: int
device_uuid: str
pci_bus_id: ObservedValue[str]
name: str
vendor: str = "NVIDIA"
architecture: ObservedValue[str]
compute_capability_major: ObservedValue[int]
compute_capability_minor: ObservedValue[int]
total_vram_bytes: ObservedValue[int]
driver_version: ObservedValue[str]
cuda_driver_version: ObservedValue[str]
mig_mode_current: ObservedValue[bool]
inventory_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
source: InventorySource = InventorySource.NVIDIA_NVML
class AcceleratorTelemetry(HardwareModel):
device_uuid: str
observed_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
used_vram_bytes: ObservedValue[int]
free_vram_bytes: ObservedValue[int]
gpu_utilization_percent: ObservedValue[int]
memory_utilization_percent: ObservedValue[int]
temperature_c: ObservedValue[int]
power_draw_w: ObservedValue[float]
power_limit_w: ObservedValue[float]
graphics_clock_mhz: ObservedValue[int]
memory_clock_mhz: ObservedValue[int]
fan_speed_percent: ObservedValue[int]
performance_state: ObservedValue[str]
class NvidiaCollection(HardwareModel):
availability: Availability
reason: str | None = None
inventory: list[AcceleratorInventory] = Field(default_factory=list)
telemetry: list[AcceleratorTelemetry] = Field(default_factory=list)
observed_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
class HardwareSnapshot(HardwareModel):
host: HostInventory
nvidia: NvidiaCollection
@property
def fingerprint(self) -> str:
facts = {
"node_identity": self.host.identity_key,
"os": self.host.os_name,
"os_version": self.host.os_version.model_dump(mode="json"),
"architecture": self.host.architecture,
"kernel": self.host.kernel_version.model_dump(mode="json"),
"accelerators": [
{
"uuid": item.device_uuid,
"name": item.name,
"pci": item.pci_bus_id.model_dump(mode="json"),
"vram": item.total_vram_bytes.model_dump(mode="json"),
"compute_major": item.compute_capability_major.model_dump(mode="json"),
"compute_minor": item.compute_capability_minor.model_dump(mode="json"),
"driver": item.driver_version.model_dump(mode="json"),
"cuda_driver": item.cuda_driver_version.model_dump(mode="json"),
}
for item in sorted(self.nvidia.inventory, key=lambda value: value.device_uuid)
],
}
canonical = json.dumps(facts, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical.encode()).hexdigest()
class HostCollector(Protocol):
def collect(self) -> HostInventory: ...
class AcceleratorCollector(Protocol):
def collect(self) -> NvidiaCollection: ...
class HardwareOverview(HardwareModel):
status: HardwareStatus
inventory_state: HardwareStatus
node_count: int
accelerator_count: int
last_inventory_at: datetime | None
reason: str | None = None
class StorageState(HardwareModel):
id: str
purpose: str
path: str
total_bytes: ObservedValue[int]
used_bytes: ObservedValue[int]
free_bytes: ObservedValue[int]
observed_at: datetime
class AcceleratorState(HardwareModel):
id: str
node_id: str
status: HardwareStatus
status_reason: str | None
device_index: int
device_uuid: str
pci_bus_id: ObservedValue[str]
name: str
vendor: str
architecture: ObservedValue[str]
compute_capability_major: ObservedValue[int]
compute_capability_minor: ObservedValue[int]
total_vram_bytes: ObservedValue[int]
driver_version: ObservedValue[str]
cuda_driver_version: ObservedValue[str]
mig_mode_current: ObservedValue[bool]
first_seen_at: datetime
last_seen_at: datetime | None
inventory_at: datetime | None
telemetry: AcceleratorTelemetry | None
class NodeState(HardwareModel):
id: str
identity_key: str
identity_source: str
hostname: str
display_name: str
status: HardwareStatus
status_reason: str | None
os_name: str | None
os_version: ObservedValue[str]
architecture: str | None
kernel_version: ObservedValue[str]
cpu_model: ObservedValue[str]
logical_cpu_count: ObservedValue[int]
physical_core_count: ObservedValue[int]
total_ram_bytes: ObservedValue[int]
available_ram_bytes: ObservedValue[int]
agent_version: str | None
first_seen_at: datetime
last_seen_at: datetime | None
inventory_at: datetime | None
hardware_fingerprint: str | None
enabled: bool = True
liveness: NodeLiveness = NodeLiveness.OFFLINE
agent_health: AgentHealth = AgentHealth.UNKNOWN
observation_source: ObservationSource = ObservationSource.LOCAL_CONTROL_PLANE
protocol_version: int | None = None
supported_capabilities: list[str] = Field(default_factory=list)
agent_started_at: datetime | None = None
last_heartbeat_at: datetime | None = None
last_inventory_received_at: datetime | None = None
last_telemetry_received_at: datetime | None = None
inventory_age_seconds: int | None = None
telemetry_age_seconds: int | None = None
last_connection_error: str | None = None
role: str | None = None
labels: dict[str, str | bool] = Field(default_factory=dict)
production_eligible: bool = False
lab_eligible: bool = True
benchmark_eligible: bool = False
generation: int = 1
decommissioned_at: datetime | None = None
decommission_reason: str | None = None
decommissioned_by: str | None = None
environment: str | None = None
storage: list[StorageState]
accelerators: list[AcceleratorState]
class HardwareState(HardwareModel):
overview: HardwareOverview
nodes: list[NodeState]
@@ -0,0 +1,75 @@
from __future__ import annotations
from .enums import MigrationStatus, ModelLifecycle, UpgradeClass, VerificationStatus
class InvalidTransition(ValueError):
pass
MODEL_TRANSITIONS: dict[ModelLifecycle, frozenset[ModelLifecycle]] = {
ModelLifecycle.DISCOVERED: frozenset(
{ModelLifecycle.CANDIDATE, ModelLifecycle.REJECTED, ModelLifecycle.DEPRECATED}
),
ModelLifecycle.CANDIDATE: frozenset(
{ModelLifecycle.DOWNLOADING, ModelLifecycle.REJECTED, ModelLifecycle.DEPRECATED}
),
ModelLifecycle.DOWNLOADING: frozenset({ModelLifecycle.QUARANTINED, ModelLifecycle.REJECTED}),
ModelLifecycle.QUARANTINED: frozenset(
{ModelLifecycle.VERIFIED, ModelLifecycle.SECURITY_BLOCKED, ModelLifecycle.LICENSE_BLOCKED}
),
ModelLifecycle.VERIFIED: frozenset({ModelLifecycle.TESTING, ModelLifecycle.INCOMPATIBLE}),
ModelLifecycle.TESTING: frozenset(
{ModelLifecycle.APPROVED, ModelLifecycle.INCOMPATIBLE, ModelLifecycle.REJECTED}
),
ModelLifecycle.APPROVED: frozenset(
{ModelLifecycle.STANDBY, ModelLifecycle.ACTIVE, ModelLifecycle.DEPRECATED}
),
ModelLifecycle.STANDBY: frozenset({ModelLifecycle.ACTIVE, ModelLifecycle.DEPRECATED}),
ModelLifecycle.ACTIVE: frozenset({ModelLifecycle.STANDBY, ModelLifecycle.DEPRECATED}),
ModelLifecycle.DEPRECATED: frozenset({ModelLifecycle.ARCHIVED, ModelLifecycle.ACTIVE}),
ModelLifecycle.ARCHIVED: frozenset(),
ModelLifecycle.REJECTED: frozenset(),
ModelLifecycle.INCOMPATIBLE: frozenset({ModelLifecycle.CANDIDATE}),
ModelLifecycle.SECURITY_BLOCKED: frozenset(
{ModelLifecycle.QUARANTINED, ModelLifecycle.REJECTED}
),
ModelLifecycle.LICENSE_BLOCKED: frozenset(
{ModelLifecycle.QUARANTINED, ModelLifecycle.REJECTED}
),
}
def assert_model_transition(current: ModelLifecycle, target: ModelLifecycle) -> None:
if target not in MODEL_TRANSITIONS[current]:
raise InvalidTransition(f"model lifecycle transition {current} -> {target} is not allowed")
def assert_promotion_allowed(
*,
upgrade_class: UpgradeClass,
verification_status: VerificationStatus,
local_benchmark_ids: list[str],
project_benchmark_ids: list[str],
operator_approval_id: str | None,
rollback_deployment_id: str | None,
migration_status: MigrationStatus | None = None,
) -> None:
blockers: list[str] = []
if verification_status is not VerificationStatus.VERIFIED:
blockers.append("artifact is not verified")
if not local_benchmark_ids:
blockers.append("local benchmark evidence is missing")
if not project_benchmark_ids:
blockers.append("project benchmark evidence is missing")
if not operator_approval_id:
blockers.append("operator approval is missing")
if not rollback_deployment_id:
blockers.append("rollback target is missing")
if (
upgrade_class is UpgradeClass.REQUIRES_REINDEX
and migration_status is not MigrationStatus.READY
):
blockers.append("ready migration is required for reindexing change")
if blockers:
raise InvalidTransition("promotion blocked: " + "; ".join(blockers))
@@ -0,0 +1,412 @@
from __future__ import annotations
import uuid
from datetime import datetime
from enum import StrEnum
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
class LifecycleModel(BaseModel):
model_config = ConfigDict(extra="forbid", from_attributes=True)
class LifecycleEnvironment(StrEnum):
LAB = "LAB"
PRODUCTION = "PRODUCTION"
class DeploymentLifecycleState(StrEnum):
CANDIDATE = "CANDIDATE"
LAB_READY = "LAB_READY"
PROMOTION_ELIGIBLE = "PROMOTION_ELIGIBLE"
CANARY = "CANARY"
LAB_STABLE = "LAB_STABLE"
STABLE = "STABLE"
DRAINING = "DRAINING"
DEPRECATED = "DEPRECATED"
ARCHIVED = "ARCHIVED"
MANUAL_INTERVENTION_REQUIRED = "MANUAL_INTERVENTION_REQUIRED"
class ApprovalStatus(StrEnum):
PENDING = "PENDING"
APPROVED = "APPROVED"
REJECTED = "REJECTED"
BLOCKED = "BLOCKED"
EXPIRED = "EXPIRED"
STALE = "STALE"
REVOKED = "REVOKED"
class PromotionPlanStatus(StrEnum):
DRAFT = "DRAFT"
APPROVED = "APPROVED"
EXECUTING = "EXECUTING"
COMMITTED = "COMMITTED"
ROLLED_BACK = "ROLLED_BACK"
FAILED = "FAILED"
class OperationStage(StrEnum):
PLANNED = "PLANNED"
APPROVED = "APPROVED"
PREPARING = "PREPARING"
ACTIVATING = "ACTIVATING"
VERIFYING = "VERIFYING"
COMMITTED = "COMMITTED"
ROLLING_BACK = "ROLLING_BACK"
ROLLED_BACK = "ROLLED_BACK"
FAILED = "FAILED"
class CanaryStatus(StrEnum):
PLANNED = "PLANNED"
RUNNING = "RUNNING"
READY_FOR_PROMOTION_REVIEW = "READY_FOR_PROMOTION_REVIEW"
ABORTED = "ABORTED"
class RetentionState(StrEnum):
ACTIVE = "ACTIVE"
ROLLBACK_RETAINED = "ROLLBACK_RETAINED"
DEPRECATED = "DEPRECATED"
ARCHIVABLE = "ARCHIVABLE"
ARCHIVED = "ARCHIVED"
class CleanupStatus(StrEnum):
READY = "READY"
BLOCKED = "BLOCKED"
STALE = "STALE"
EXECUTED = "EXECUTED"
ALLOWED_DEPLOYMENT_TRANSITIONS: dict[
DeploymentLifecycleState, frozenset[DeploymentLifecycleState]
] = {
DeploymentLifecycleState.CANDIDATE: frozenset(
{DeploymentLifecycleState.LAB_READY, DeploymentLifecycleState.DEPRECATED}
),
DeploymentLifecycleState.LAB_READY: frozenset(
{
DeploymentLifecycleState.PROMOTION_ELIGIBLE,
DeploymentLifecycleState.CANARY,
DeploymentLifecycleState.DEPRECATED,
}
),
DeploymentLifecycleState.PROMOTION_ELIGIBLE: frozenset(
{DeploymentLifecycleState.CANARY, DeploymentLifecycleState.DEPRECATED}
),
DeploymentLifecycleState.CANARY: frozenset(
{
DeploymentLifecycleState.LAB_READY,
DeploymentLifecycleState.LAB_STABLE,
DeploymentLifecycleState.STABLE,
}
),
DeploymentLifecycleState.LAB_STABLE: frozenset(
{
DeploymentLifecycleState.CANARY,
DeploymentLifecycleState.LAB_READY,
DeploymentLifecycleState.DEPRECATED,
}
),
DeploymentLifecycleState.STABLE: frozenset(
{DeploymentLifecycleState.CANARY, DeploymentLifecycleState.DRAINING}
),
DeploymentLifecycleState.DRAINING: frozenset(
{DeploymentLifecycleState.STABLE, DeploymentLifecycleState.DEPRECATED}
),
DeploymentLifecycleState.DEPRECATED: frozenset({DeploymentLifecycleState.ARCHIVED}),
DeploymentLifecycleState.ARCHIVED: frozenset(),
DeploymentLifecycleState.MANUAL_INTERVENTION_REQUIRED: frozenset(),
}
def assert_deployment_transition(
current: DeploymentLifecycleState,
target: DeploymentLifecycleState,
) -> None:
if target not in ALLOWED_DEPLOYMENT_TRANSITIONS[current]:
raise ValueError(f"lifecycle transition {current} -> {target} is not allowed")
class LifecycleEvidenceBundle(LifecycleModel):
artifact_set_id: uuid.UUID | None = None
model_revision_id: uuid.UUID | None = None
runtime_profile_id: uuid.UUID | None = None
runtime_probe_id: uuid.UUID | None = None
production_execution_approval_id: uuid.UUID | None = None
runtime_image_digest: str | None = Field(default=None, max_length=128)
artifact_digests: list[str] = Field(default_factory=list, max_length=1000)
capability_deployment_id: uuid.UUID | None = None
project_binding_id: uuid.UUID | None = None
project_fit_evidence_id: uuid.UUID | None = None
evaluation_run_ids: list[uuid.UUID] = Field(default_factory=list, max_length=1000)
resource_envelope_id: uuid.UUID | None = None
embedding_space_id: uuid.UUID | None = None
retrieval_pipeline_id: uuid.UUID | None = None
migration_id: uuid.UUID | None = None
rollback_target_ref: str | None = Field(default=None, max_length=255)
integrity: Literal["UNKNOWN", "VERIFIED", "CORRUPT"] = "UNKNOWN"
security: Literal["UNREVIEWED", "APPROVED", "BLOCKED"] = "UNREVIEWED"
license: Literal["UNKNOWN", "APPROVED", "BLOCKED"] = "UNKNOWN"
runtime: Literal["UNPROBED", "PROVEN", "INCOMPATIBLE"] = "UNPROBED"
evaluation: Literal["NOT_EVALUATED", "EVALUATED", "REGRESSED", "PROMOTION_ELIGIBLE"] = (
"NOT_EVALUATED"
)
project_fit: Literal[
"UNKNOWN",
"REQUIRES_MORE_EVIDENCE",
"ELIGIBLE",
"BLOCKED",
"DEFERRED_EXTERNAL_VALIDATION",
"KEEP_LAB",
] = "UNKNOWN"
engineering_integration: Literal["UNKNOWN", "PASS", "INCOMPLETE", "BLOCKED"] = "UNKNOWN"
platform_readiness: Literal["UNKNOWN", "LAB_READY", "PROMOTION_ELIGIBLE", "STABLE"] = (
"UNKNOWN"
)
production_validation: Literal[
"NOT_REQUIRED", "REQUIRED", "DEFERRED_EXTERNAL_VALIDATION", "SATISFIED"
] = "REQUIRED"
scheduler_readiness: Literal["UNKNOWN", "READY", "BLOCKED"] = "UNKNOWN"
critical_regressions: int = Field(default=0, ge=0)
evidence_revisions: dict[str, str] = Field(default_factory=dict)
class ApprovalPolicyCreate(LifecycleModel):
key: str = Field(pattern=r"^[a-z][a-z0-9-]+$", max_length=128)
scope: Literal["LAB_PROMOTION", "CAPABILITY_PRODUCTION", "PROJECT_PRODUCTION"]
requirements: dict[str, Any]
created_by: str = Field(min_length=1, max_length=255)
class ApprovalPolicyResponse(LifecycleModel):
id: uuid.UUID
key: str
revision: int
scope: str
requirements: dict[str, Any]
fingerprint: str
active: bool
created_by: str
created_at: datetime
class LifecycleSubjectCreate(LifecycleModel):
target_type: Literal[
"CAPABILITY_DEPLOYMENT", "PROJECT_BINDING", "ARTIFACT_SET", "LAB_REHEARSAL"
]
target_ref: str = Field(min_length=1, max_length=255)
environment: LifecycleEnvironment
state: DeploymentLifecycleState
class LifecycleSubjectResponse(LifecycleModel):
id: uuid.UUID
target_type: str
target_ref: str
environment: str
state: str
version: int
superseded_by_ref: str | None
created_at: datetime
updated_at: datetime
class ApprovalRequestCreate(LifecycleModel):
policy_key: str = Field(pattern=r"^[a-z][a-z0-9-]+$", max_length=128)
subject_id: uuid.UUID | None = None
target_type: Literal[
"CAPABILITY_DEPLOYMENT", "PROJECT_BINDING", "CAPABILITY", "ARTIFACT_SET", "LAB_REHEARSAL"
]
target_ref: str = Field(min_length=1, max_length=255)
environment: LifecycleEnvironment
requested_transition: DeploymentLifecycleState
evidence: LifecycleEvidenceBundle
requested_by: str = Field(min_length=1, max_length=255)
reason: str = Field(min_length=8, max_length=4000)
expires_at: datetime | None = None
class ApprovalDecision(LifecycleModel):
actor: str = Field(min_length=1, max_length=255)
reason: str = Field(min_length=8, max_length=4000)
class ApprovalRequestResponse(LifecycleModel):
id: uuid.UUID
policy_revision_id: uuid.UUID
policy_key: str
policy_revision: int
version: int
subject_id: uuid.UUID | None
target_type: str
target_ref: str
environment: str
requested_transition: str
evidence_snapshot: dict[str, Any]
evidence_fingerprint: str
status: str
blockers: list[str]
warnings: list[str]
requested_by: str
approved_by: str | None
reason: str
expires_at: datetime | None
stale_at: datetime | None
decided_at: datetime | None
created_at: datetime
class PromotionPlanCreate(LifecycleModel):
approval_request_id: uuid.UUID
subject_id: uuid.UUID
desired_state: DeploymentLifecycleState
migration_class: Literal["transparent", "behavioral", "requires_reindex", "schema_breaking"]
candidate_deployment_id: uuid.UUID | None = None
rollback_target_ref: str = Field(min_length=1, max_length=255)
project_consumers: list[str] = Field(default_factory=list, max_length=1000)
affected_identities: dict[str, Any] = Field(default_factory=dict)
migration_id: uuid.UUID | None = None
canary_strategy: dict[str, Any] = Field(default_factory=dict)
drain_strategy: dict[str, Any] = Field(default_factory=dict)
health_gates: dict[str, Any] = Field(default_factory=dict)
automatic_abort_conditions: list[str] = Field(default_factory=list)
created_by: str = Field(min_length=1, max_length=255)
class PromotionPlanResponse(LifecycleModel):
id: uuid.UUID
approval_request_id: uuid.UUID
subject_id: uuid.UUID
current_state: str
desired_state: str
migration_class: str
candidate_deployment_id: uuid.UUID | None
rollback_target_ref: str
project_consumers: list[str]
affected_identities: dict[str, Any]
impact_analysis: dict[str, Any]
migration_id: uuid.UUID | None
canary_strategy: dict[str, Any]
drain_strategy: dict[str, Any]
health_gates: dict[str, Any]
automatic_abort_conditions: list[str]
plan_fingerprint: str
status: str
version: int
created_by: str
approved_by: str | None
immutable_at: datetime | None
created_at: datetime
class PlanExecutionCreate(LifecycleModel):
executor: str = Field(min_length=1, max_length=255)
idempotency_key: str = Field(min_length=8, max_length=128)
expected_subject_version: int = Field(ge=1)
rehearsal_pause_stage: Literal["PREPARING"] | None = None
class CanaryObservation(LifecycleModel):
request_count: int = Field(ge=0)
error_count: int = Field(ge=0)
latency_p95_ms: float = Field(ge=0)
capability_health: Literal["HEALTHY", "UNHEALTHY"]
scheduler_ready: bool
critical_project_failures: int = Field(default=0, ge=0)
external_pressure: bool = False
worker_healthy: bool = True
@model_validator(mode="after")
def validate_counts(self) -> CanaryObservation:
if self.error_count > self.request_count:
raise ValueError("error_count cannot exceed request_count")
return self
class LifecycleOperationResponse(LifecycleModel):
id: uuid.UUID
promotion_plan_id: uuid.UUID
version: int
stage: str
idempotency_key: str
expected_subject_version: int
requester: str
approver: str
executor: str
failure_code: str | None
failure_details: dict[str, Any]
rollback_duration_ms: float | None
started_at: datetime
finished_at: datetime | None
canary: dict[str, Any] | None = None
class RetentionPolicyCreate(LifecycleModel):
key: str = Field(pattern=r"^[a-z][a-z0-9-]+$", max_length=128)
minimum_rollback_days: int = Field(ge=1, le=3650)
requirements: dict[str, Any] = Field(default_factory=dict)
created_by: str = Field(min_length=1, max_length=255)
class RetentionPolicyResponse(LifecycleModel):
id: uuid.UUID
key: str
revision: int
minimum_rollback_days: int
requirements: dict[str, Any]
fingerprint: str
active: bool
created_by: str
created_at: datetime
class CleanupPlanCreate(LifecycleModel):
target_type: Literal["ARTIFACT_SET", "ARTIFACT_LOCATION"]
target_ref: uuid.UUID
action: Literal["ARCHIVE_METADATA", "RECORD_LOCATION_REMOVAL"]
created_by: str = Field(min_length=1, max_length=255)
class CleanupPlanResponse(LifecycleModel):
id: uuid.UUID
target_type: str
target_ref: str
action: str
dependencies: list[dict[str, Any]]
dependency_digest: str
reclaimable_bytes: int
retention_state: str
blockers: list[str]
status: str
created_by: str
created_at: datetime
executed_at: datetime | None
class CleanupExecutionCreate(LifecycleModel):
executor: str = Field(min_length=1, max_length=255)
confirm: Literal[True]
physical_removal_confirmed: bool = False
class LifecycleEventResponse(LifecycleModel):
id: uuid.UUID
event_type: str
object_type: str
object_ref: str
from_state: str | None
to_state: str | None
actor: str
actor_role: str
policy_revision_id: uuid.UUID | None
evidence_ids: list[str]
reason: str
change_id: str
details: dict[str, Any]
occurred_at: datetime
@@ -0,0 +1,543 @@
from __future__ import annotations
import re
import uuid
from datetime import datetime
from enum import StrEnum
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
class MigrationModel(BaseModel):
model_config = ConfigDict(extra="forbid", from_attributes=True)
class MigrationClass(StrEnum):
TRANSPARENT = "TRANSPARENT"
BEHAVIORAL = "BEHAVIORAL"
REQUIRES_REINDEX = "REQUIRES_REINDEX"
SCHEMA_BREAKING = "SCHEMA_BREAKING"
class MigrationState(StrEnum):
PLANNED = "PLANNED"
PREFLIGHT = "PREFLIGHT"
READY = "READY"
BACKFILLING = "BACKFILLING"
BACKFILL_PAUSED = "BACKFILL_PAUSED"
BACKFILL_COMPLETE = "BACKFILL_COMPLETE"
VALIDATING = "VALIDATING"
VALIDATION_FAILED = "VALIDATION_FAILED"
READY_FOR_SHADOW = "READY_FOR_SHADOW"
SHADOWING = "SHADOWING"
READY_FOR_CUTOVER = "READY_FOR_CUTOVER"
CUTOVER_PREPARING = "CUTOVER_PREPARING"
CUTTING_OVER = "CUTTING_OVER"
VERIFYING_CUTOVER = "VERIFYING_CUTOVER"
CUTOVER_COMMITTED = "CUTOVER_COMMITTED"
ROLLING_BACK = "ROLLING_BACK"
ROLLED_BACK = "ROLLED_BACK"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
MANUAL_INTERVENTION_REQUIRED = "MANUAL_INTERVENTION_REQUIRED"
class CutoverStage(StrEnum):
PREPARING = "PREPARING"
LOCKING = "LOCKING"
SWITCHING = "SWITCHING"
VERIFYING = "VERIFYING"
COMMITTING = "COMMITTING"
COMMITTED = "COMMITTED"
ROLLING_BACK = "ROLLING_BACK"
ROLLED_BACK = "ROLLED_BACK"
FAILED = "FAILED"
class BatchState(StrEnum):
PLANNED = "PLANNED"
RUNNING = "RUNNING"
COMPLETED = "COMPLETED"
RETRYABLE_FAILED = "RETRYABLE_FAILED"
PERMANENTLY_FAILED = "PERMANENTLY_FAILED"
class MigrationFailureCode(StrEnum):
SOURCE_CHANGED = "SOURCE_CHANGED"
STALE_SOURCE = "STALE_SOURCE"
TARGET_CONFLICT = "TARGET_CONFLICT"
CAPABILITY_UNAVAILABLE = "CAPABILITY_UNAVAILABLE"
BATCH_FAILED = "BATCH_FAILED"
VECTOR_INVALID = "VECTOR_INVALID"
WRITE_FAILED = "WRITE_FAILED"
CHECKPOINT_FAILED = "CHECKPOINT_FAILED"
CONTENT_HASH_MISMATCH = "CONTENT_HASH_MISMATCH"
TARGET_SCHEMA_MISMATCH = "TARGET_SCHEMA_MISMATCH"
VALIDATION_FAILED = "VALIDATION_FAILED"
HEALTH_CHECK_FAILED = "HEALTH_CHECK_FAILED"
ROLLBACK_FAILED = "ROLLBACK_FAILED"
STALE_APPROVAL = "STALE_APPROVAL"
ALLOWED_TRANSITIONS: dict[MigrationState, frozenset[MigrationState]] = {
MigrationState.PLANNED: frozenset(
{MigrationState.PREFLIGHT, MigrationState.CANCELLED, MigrationState.FAILED}
),
MigrationState.PREFLIGHT: frozenset(
{MigrationState.READY, MigrationState.FAILED, MigrationState.CANCELLED}
),
MigrationState.READY: frozenset(
{MigrationState.BACKFILLING, MigrationState.CANCELLED, MigrationState.FAILED}
),
MigrationState.BACKFILLING: frozenset(
{
MigrationState.BACKFILL_PAUSED,
MigrationState.BACKFILL_COMPLETE,
MigrationState.CANCELLED,
MigrationState.FAILED,
}
),
MigrationState.BACKFILL_PAUSED: frozenset(
{MigrationState.BACKFILLING, MigrationState.CANCELLED, MigrationState.FAILED}
),
MigrationState.BACKFILL_COMPLETE: frozenset(
{MigrationState.VALIDATING, MigrationState.CANCELLED, MigrationState.FAILED}
),
MigrationState.VALIDATING: frozenset(
{
MigrationState.VALIDATION_FAILED,
MigrationState.READY_FOR_SHADOW,
MigrationState.FAILED,
MigrationState.CANCELLED,
}
),
MigrationState.VALIDATION_FAILED: frozenset(
{MigrationState.VALIDATING, MigrationState.CANCELLED, MigrationState.FAILED}
),
MigrationState.READY_FOR_SHADOW: frozenset(
{MigrationState.SHADOWING, MigrationState.CANCELLED, MigrationState.FAILED}
),
MigrationState.SHADOWING: frozenset(
{MigrationState.READY_FOR_CUTOVER, MigrationState.CANCELLED, MigrationState.FAILED}
),
MigrationState.READY_FOR_CUTOVER: frozenset(
{MigrationState.CUTOVER_PREPARING, MigrationState.CANCELLED, MigrationState.FAILED}
),
MigrationState.CUTOVER_PREPARING: frozenset(
{MigrationState.CUTTING_OVER, MigrationState.FAILED, MigrationState.CANCELLED}
),
MigrationState.CUTTING_OVER: frozenset(
{
MigrationState.VERIFYING_CUTOVER,
MigrationState.ROLLING_BACK,
MigrationState.MANUAL_INTERVENTION_REQUIRED,
}
),
MigrationState.VERIFYING_CUTOVER: frozenset(
{
MigrationState.CUTOVER_COMMITTED,
MigrationState.ROLLING_BACK,
MigrationState.MANUAL_INTERVENTION_REQUIRED,
}
),
MigrationState.CUTOVER_COMMITTED: frozenset({MigrationState.ROLLING_BACK}),
MigrationState.ROLLING_BACK: frozenset(
{MigrationState.ROLLED_BACK, MigrationState.MANUAL_INTERVENTION_REQUIRED}
),
MigrationState.ROLLED_BACK: frozenset(),
MigrationState.FAILED: frozenset(),
MigrationState.CANCELLED: frozenset(),
MigrationState.MANUAL_INTERVENTION_REQUIRED: frozenset(),
}
def assert_migration_transition(before: str, after: str) -> None:
current = MigrationState(before)
target = MigrationState(after)
if target not in ALLOWED_TRANSITIONS[current]:
raise ValueError(f"invalid migration transition {current.value} -> {target.value}")
class AdapterContract(MigrationModel):
key: str = Field(pattern=r"^[a-z0-9][a-z0-9._-]{1,127}$")
version: str = Field(min_length=1, max_length=64)
operations: frozenset[
Literal[
"preflight",
"create_shadow_target",
"enumerate_source_items",
"transform_batch",
"write_batch",
"validate_batch",
"finalize_backfill",
"validate_target",
"shadow_compare",
"cutover",
"rollback",
"inspect_external_state",
]
]
fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
schema_operations: frozenset[str] = Field(default_factory=frozenset, max_length=32)
@field_validator("schema_operations")
@classmethod
def validate_schema_operations(cls, value: frozenset[str]) -> frozenset[str]:
if any(not re.fullmatch(r"[a-z0-9][a-z0-9._-]{1,127}", item) for item in value):
raise ValueError("schema operations must be typed adapter keys")
return value
class SchemaMigrationStep(MigrationModel):
operation: str = Field(min_length=1, max_length=64)
adapter_step: str = Field(default="manual-boundary", min_length=1, max_length=128)
preconditions: list[str] = Field(default_factory=list, max_length=32)
required_application_versions: dict[str, str] = Field(default_factory=dict)
compatibility_window: str = Field(default="unspecified", min_length=1, max_length=255)
rollback_feasible: bool = False
irreversible: bool = False
class MigrationPlanCreate(MigrationModel):
project_id: uuid.UUID
project_binding_id: uuid.UUID
capability_contract_id: uuid.UUID
migration_class: MigrationClass
environment: Literal["LAB", "PRODUCTION"]
adapter: AdapterContract
source_identity: dict[str, Any]
target_identity: dict[str, Any]
source_data_target: str = Field(min_length=1, max_length=512)
target_shadow_target: str = Field(min_length=1, max_length=512)
source_space_ref: str = Field(min_length=1, max_length=255)
target_space_id: uuid.UUID
corpus_revision: str = Field(min_length=1, max_length=128)
migration_policy_revision: str = Field(min_length=1, max_length=128)
validation_policy_revision_id: uuid.UUID
lifecycle_approval_id: uuid.UUID
promotion_plan_id: uuid.UUID | None = None
rollback_target_ref: str = Field(min_length=1, max_length=512)
total_expected_items: int = Field(ge=1, le=10_000_000)
batch_size: int = Field(default=32, ge=1, le=256)
max_in_flight_batches: int = Field(default=1, ge=1, le=8)
concurrency: int = Field(default=1, ge=1, le=8)
priority: Literal["BACKGROUND"] = "BACKGROUND"
target_storage: dict[str, Any]
shadow_policy: dict[str, Any]
cutover_policy: dict[str, Any]
rollback_retention_days: int = Field(default=30, ge=30, le=3650)
environment_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
idempotency_key: str = Field(min_length=8, max_length=128)
created_by: str = Field(min_length=1, max_length=255)
irreversible: bool = False
schema_steps: list[SchemaMigrationStep] = Field(default_factory=list, max_length=32)
@model_validator(mode="after")
def identities_are_isolated(self) -> MigrationPlanCreate:
if self.migration_class is MigrationClass.REQUIRES_REINDEX:
if self.source_data_target == self.target_shadow_target:
raise ValueError("reindex target must be separate from source")
if self.source_space_ref == str(self.target_space_id):
raise ValueError("reindex requires a distinct target embedding space")
if self.migration_class is MigrationClass.SCHEMA_BREAKING and not self.schema_steps:
raise ValueError("schema-breaking plans require typed adapter steps")
return self
class MigrationPlanResponse(MigrationPlanCreate):
id: uuid.UUID
state: MigrationState
version: int
generation: int
plan_fingerprint: str
approval_fingerprint: str
completed_items: int
failed_items: int
retryable_items: int
permanent_failed_items: int
last_cursor: str | None
cancel_requested: bool
failure_code: str | None
failure_details: dict[str, Any]
started_at: datetime | None
completed_at: datetime | None
immutable_at: datetime
created_at: datetime
updated_at: datetime
class MigrationValidationPolicyCreate(MigrationModel):
key: str = Field(min_length=1, max_length=128)
revision: int = Field(ge=1)
required_completeness: float = Field(default=1.0, ge=0, le=1)
allowed_failures: int = Field(default=0, ge=0)
required_evaluation: bool = True
critical_regressions_allowed: int = Field(default=0, ge=0)
maximum_latency_regression_ratio: float | None = Field(default=None, ge=0)
require_project_fit: bool = True
require_external_validation: bool = True
require_security_approved: bool = True
allow_isolated_lab_cutover: bool = False
created_by: str = Field(min_length=1, max_length=255)
class MigrationValidationPolicyResponse(MigrationValidationPolicyCreate):
id: uuid.UUID
fingerprint: str
active: bool
created_at: datetime
immutable_at: datetime
class PreflightReport(MigrationModel):
expected_version: int = Field(ge=1)
adapter_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
source_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
source_exists: bool
source_healthy: bool
source_count: int = Field(ge=0)
target_conflict_free: bool
target_space_valid: bool
capability_healthy: bool
project_credential_valid: bool
storage_sufficient: bool
scheduler_capacity: bool
adapter_available: bool
rollback_source_retained: bool
evaluation_suite_available: bool
lifecycle_approval_current: bool
evidence: dict[str, Any] = Field(default_factory=dict)
@property
def passed(self) -> bool:
return all(
(
self.source_exists,
self.source_healthy,
self.target_conflict_free,
self.target_space_valid,
self.capability_healthy,
self.project_credential_valid,
self.storage_sufficient,
self.scheduler_capacity,
self.adapter_available,
self.rollback_source_retained,
self.evaluation_suite_available,
self.lifecycle_approval_current,
)
)
class BatchReport(MigrationModel):
expected_version: int = Field(ge=1)
generation: int = Field(ge=1)
batch_number: int = Field(ge=0)
cursor_start: str = Field(min_length=1, max_length=255)
cursor_end: str = Field(min_length=1, max_length=255)
item_count: int = Field(ge=1, le=256)
completed_items: int = Field(ge=0, le=256)
failed_items: int = Field(ge=0, le=256)
retryable_items: int = Field(ge=0, le=256)
permanent_failed_items: int = Field(ge=0, le=256)
item_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
result_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
output_shape_valid: bool
finite: bool
target_space_matches: bool
destination_committed: bool
content_hashes_match: bool
duration_ms: float = Field(ge=0)
retries: int = Field(default=0, ge=0, le=20)
error_code: MigrationFailureCode | None = None
bounded_errors: list[dict[str, Any]] = Field(default_factory=list, max_length=20)
@model_validator(mode="after")
def totals_are_consistent(self) -> BatchReport:
if self.completed_items + self.failed_items != self.item_count:
raise ValueError("batch outcome count must equal item count")
if self.retryable_items + self.permanent_failed_items != self.failed_items:
raise ValueError("failed item taxonomy is incomplete")
return self
class StateAction(MigrationModel):
expected_version: int = Field(ge=1)
actor: str = Field(min_length=1, max_length=255)
reason: str = Field(min_length=1, max_length=2000)
class ValidationReport(MigrationModel):
expected_version: int = Field(ge=1)
generation: int = Field(ge=1)
expected_count: int = Field(ge=0)
actual_count: int = Field(ge=0)
missing_count: int = Field(ge=0)
duplicate_count: int = Field(ge=0)
malformed_count: int = Field(ge=0)
non_finite_count: int = Field(ge=0)
wrong_dimension_count: int = Field(ge=0)
content_hash_mismatch_count: int = Field(ge=0)
wrong_space_count: int = Field(ge=0)
index_schema_matches: bool
distance_metric_matches: bool
payload_integrity: bool
target_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
evaluation_run_ids: list[uuid.UUID] = Field(default_factory=list, max_length=32)
comparable: bool
critical_regressions: int = Field(ge=0)
latency_regression_ratio: float | None = Field(default=None, ge=0)
project_fit_eligible: bool
external_validation_satisfied: bool
security_approved: bool
evidence: dict[str, Any] = Field(default_factory=dict)
class ValidationSnapshotResponse(ValidationReport):
id: uuid.UUID
migration_plan_id: uuid.UUID
validation_policy_revision_id: uuid.UUID
snapshot_fingerprint: str
passed: bool
technical_cutover_eligible: bool
project_promotion_eligible: bool
blockers: list[str]
created_at: datetime
immutable_at: datetime
class ShadowReport(MigrationModel):
expected_version: int = Field(ge=1)
generation: int = Field(ge=1)
request_count: int = Field(ge=1)
source_error_count: int = Field(ge=0)
target_error_count: int = Field(ge=0)
source_latency_p95_ms: float = Field(ge=0)
target_latency_p95_ms: float = Field(ge=0)
critical_regressions: int = Field(ge=0)
metrics: dict[str, float]
evidence_refs: list[str] = Field(default_factory=list, max_length=64)
result_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
class CutoverPrepare(MigrationModel):
expected_version: int = Field(ge=1)
idempotency_key: str = Field(min_length=8, max_length=128)
actor: str = Field(min_length=1, max_length=255)
expected_external_source: str = Field(min_length=1, max_length=512)
observed_external_source: str = Field(min_length=1, max_length=512)
external_state_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
configuration_version: str = Field(min_length=1, max_length=128)
class CutoverReport(MigrationModel):
expected_version: int = Field(ge=1)
operation_id: uuid.UUID
generation: int = Field(ge=1)
external_source_before: str = Field(min_length=1, max_length=512)
external_target_after: str = Field(min_length=1, max_length=512)
external_state_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
switch_duration_ms: float = Field(ge=0)
target_reachable: bool
expected_identity: bool
capability_healthy: bool
project_read_path_healthy: bool
error_rate: float = Field(ge=0, le=1)
smoke_query_count: int = Field(ge=0, le=100)
smoke_error_count: int = Field(ge=0, le=100)
evidence: dict[str, Any] = Field(default_factory=dict)
class RollbackReport(MigrationModel):
expected_version: int = Field(ge=1)
operation_id: uuid.UUID
generation: int = Field(ge=1)
restored_external_target: str = Field(min_length=1, max_length=512)
external_state_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
elapsed_ms: float = Field(ge=0)
source_reachable: bool
exact_identity_restored: bool
capability_healthy: bool
project_read_path_healthy: bool
evidence: dict[str, Any] = Field(default_factory=dict)
class ReconciliationReport(MigrationModel):
operation_id: uuid.UUID
generation: int = Field(ge=1)
observed_external_target: str = Field(min_length=1, max_length=512)
external_state_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$")
target_healthy: bool
source_healthy: bool
switch_duration_ms: float | None = Field(default=None, ge=0)
smoke_query_count: int = Field(default=0, ge=0, le=100)
smoke_error_count: int = Field(default=0, ge=0, le=100)
evidence: dict[str, Any] = Field(default_factory=dict)
actor: Literal["migration-reconciler"] = "migration-reconciler"
@model_validator(mode="after")
def smoke_totals_are_consistent(self) -> ReconciliationReport:
if self.smoke_error_count > self.smoke_query_count:
raise ValueError("smoke errors cannot exceed smoke queries")
return self
class MigrationBatchResponse(MigrationModel):
id: uuid.UUID
migration_plan_id: uuid.UUID
batch_number: int
generation: int
cursor_start: str
cursor_end: str
item_count: int
completed_items: int
failed_items: int
retryable_items: int
permanent_failed_items: int
item_fingerprint: str
result_fingerprint: str | None
status: BatchState
attempts: int
duration_ms: float | None
error_code: str | None
bounded_errors: list[dict[str, Any]]
started_at: datetime | None
completed_at: datetime | None
class CutoverOperationResponse(MigrationModel):
id: uuid.UUID
migration_plan_id: uuid.UUID
stage: CutoverStage
idempotency_key: str
generation: int
expected_plan_version: int
source_before: str
target_after: str
external_state_fingerprint: str
health_evidence: dict[str, Any]
failure_code: str | None
failure_details: dict[str, Any]
switch_duration_ms: float | None
rollback_duration_ms: float | None
started_at: datetime
finished_at: datetime | None
class MigrationEventResponse(MigrationModel):
id: uuid.UUID
migration_plan_id: uuid.UUID
operation_id: uuid.UUID | None
event_type: str
before_state: str | None
after_state: str | None
actor: str
policy_revision: str
evidence_refs: list[str]
reason: str
source_identity: dict[str, Any]
target_identity: dict[str, Any]
generation: int
change_id: str
details: dict[str, Any]
occurred_at: datetime
@@ -0,0 +1,60 @@
from __future__ import annotations
import uuid
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class NodeDecommissionModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class DecommissionBlocker(NodeDecommissionModel):
code: str
message: str
record_type: str
count: int = Field(ge=1)
resource_ids: list[str] = Field(default_factory=list)
class DecommissionRecordCount(NodeDecommissionModel):
record_type: str
count: int = Field(ge=0)
action: str
class NodeDecommissionPreview(NodeDecommissionModel):
node_id: uuid.UUID
persisted_identity: str
hostname: str
display_name: str
current_state: dict[str, object]
node_generation: int = Field(ge=1)
dependency_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
safe: bool
blockers: list[DecommissionBlocker]
cleanup: list[DecommissionRecordCount]
preserved: list[DecommissionRecordCount]
dependent_records: list[DecommissionRecordCount]
class NodeDecommissionExecute(NodeDecommissionModel):
expected_generation: int = Field(ge=1)
preview_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
idempotency_key: str = Field(min_length=8, max_length=128)
operator: str = Field(min_length=1, max_length=255)
reason: str = Field(min_length=10, max_length=2000)
confirmation: str = Field(min_length=1, max_length=255)
class NodeDecommissionResult(NodeDecommissionModel):
operation_id: uuid.UUID
node_id: uuid.UUID
persisted_identity: str
status: str
decommissioned_at: datetime
cleanup_summary: dict[str, int]
previous_state: dict[str, object]
credential_revocations: int = Field(ge=0)
idempotent_replay: bool = False
@@ -0,0 +1,399 @@
from __future__ import annotations
import re
import threading
import time
import uuid
from collections import defaultdict
from datetime import datetime
from enum import StrEnum
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
class TelemetryType(StrEnum):
COUNTER = "COUNTER"
GAUGE = "GAUGE"
HISTOGRAM = "HISTOGRAM"
EVENT = "EVENT"
STATE = "STATE"
class SLOState(StrEnum):
HEALTHY = "HEALTHY"
AT_RISK = "AT_RISK"
BREACHED = "BREACHED"
INSUFFICIENT_DATA = "INSUFFICIENT_DATA"
STALE = "STALE"
DISABLED = "DISABLED"
class AlertState(StrEnum):
PENDING = "PENDING"
FIRING = "FIRING"
ACKNOWLEDGED = "ACKNOWLEDGED"
RESOLVED = "RESOLVED"
SUPPRESSED = "SUPPRESSED"
class AlertSeverity(StrEnum):
INFO = "INFO"
WARNING = "WARNING"
CRITICAL = "CRITICAL"
class OperationalModel(BaseModel):
model_config = ConfigDict(extra="forbid", from_attributes=True)
class MetricDefinition(OperationalModel):
name: str = Field(pattern=r"^modelforge_[a-z][a-z0-9_]*$")
type: TelemetryType
help: str = Field(min_length=3, max_length=500)
labels: tuple[str, ...] = ()
@field_validator("labels")
@classmethod
def bounded_labels(cls, value: tuple[str, ...]) -> tuple[str, ...]:
forbidden = {
"request_id",
"artifact_sha",
"query",
"project_uuid",
"filename",
"error_text",
}
if len(value) > 6 or len(set(value)) != len(value) or forbidden.intersection(value):
raise ValueError("metric labels must be unique, bounded and non-sensitive")
if any(not re.fullmatch(r"[a-z][a-z0-9_]*", item) for item in value):
raise ValueError("metric label names must use snake_case")
return value
class MetricRegistry:
"""Process-local Prometheus registry; DB history is deliberately a separate concern."""
def __init__(self) -> None:
self._definitions: dict[str, MetricDefinition] = {}
self._values: dict[tuple[str, tuple[tuple[str, str], ...]], float] = defaultdict(float)
self._histograms: dict[tuple[str, tuple[tuple[str, str], ...]], list[float]] = defaultdict(
list
)
self._lock = threading.Lock()
self.started_at = time.time()
def define(self, definition: MetricDefinition) -> None:
current = self._definitions.get(definition.name)
if current and current != definition:
raise ValueError(f"metric {definition.name} is already defined differently")
self._definitions[definition.name] = definition
def _key(self, name: str, labels: dict[str, str]) -> tuple[str, tuple[tuple[str, str], ...]]:
definition = self._definitions[name]
if set(labels) != set(definition.labels):
raise ValueError(f"metric {name} requires labels {definition.labels}")
if any(len(value) > 128 or "\n" in value for value in labels.values()):
raise ValueError("metric label values must be bounded single-line values")
return name, tuple(sorted(labels.items()))
def increment(self, name: str, labels: dict[str, str], value: float = 1.0) -> None:
if self._definitions[name].type is not TelemetryType.COUNTER or value < 0:
raise ValueError("only counters accept non-negative increments")
with self._lock:
self._values[self._key(name, labels)] += value
def gauge(self, name: str, labels: dict[str, str], value: float) -> None:
if self._definitions[name].type is not TelemetryType.GAUGE:
raise ValueError("only gauges accept current values")
with self._lock:
self._values[self._key(name, labels)] = value
def observe(self, name: str, labels: dict[str, str], value: float) -> None:
if self._definitions[name].type is not TelemetryType.HISTOGRAM or value < 0:
raise ValueError("only histograms accept non-negative observations")
with self._lock:
samples = self._histograms[self._key(name, labels)]
samples.append(value)
if len(samples) > 10_000:
del samples[: len(samples) - 10_000]
def histogram(self, name: str, labels: dict[str, str]) -> list[float]:
with self._lock:
return list(self._histograms.get(self._key(name, labels), []))
def samples(self, name: str) -> list[tuple[dict[str, str], float]]:
with self._lock:
return [
(dict(labels), value)
for (metric, labels), value in self._values.items()
if metric == name
]
@staticmethod
def _label_text(labels: tuple[tuple[str, str], ...]) -> str:
if not labels:
return ""
escaped = [f'{key}="{value.replace(chr(92), chr(92) * 2).replace(chr(34), chr(92) + chr(34))}"' for key, value in labels]
return "{" + ",".join(escaped) + "}"
def render(self) -> str:
buckets = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
lines: list[str] = []
with self._lock:
for name, definition in sorted(self._definitions.items()):
lines.extend((f"# HELP {name} {definition.help}", f"# TYPE {name} {definition.type.value.lower()}"))
for (metric, labels), value in sorted(self._values.items()):
if metric == name:
lines.append(f"{name}{self._label_text(labels)} {value}")
for (metric, labels), values in sorted(self._histograms.items()):
if metric != name:
continue
label_text = self._label_text(labels)
for upper_bound in buckets:
bucket_labels = tuple(sorted((*labels, ("le", str(upper_bound)))))
count = sum(value <= upper_bound for value in values)
lines.append(f"{name}_bucket{self._label_text(bucket_labels)} {count}")
infinite_labels = tuple(sorted((*labels, ("le", "+Inf"))))
lines.append(
f"{name}_bucket{self._label_text(infinite_labels)} {len(values)}"
)
lines.append(f"{name}_count{label_text} {len(values)}")
lines.append(f"{name}_sum{label_text} {sum(values)}")
lines.append(f"modelforge_process_uptime_seconds {max(0.0, time.time() - self.started_at)}")
return "\n".join(lines) + "\n"
metrics = MetricRegistry()
for _definition in (
MetricDefinition(name="modelforge_api_requests_total", type=TelemetryType.COUNTER, help="Bounded API request outcomes", labels=("method", "route_class", "status_class")),
MetricDefinition(name="modelforge_api_request_duration_seconds", type=TelemetryType.HISTOGRAM, help="API request duration", labels=("method", "route_class")),
MetricDefinition(name="modelforge_observability_degraded", type=TelemetryType.GAUGE, help="Historical observability persistence is degraded", labels=()),
):
metrics.define(_definition)
class SLIDefinitionCreate(OperationalModel):
key: str = Field(pattern=r"^[a-z][a-z0-9_.-]{2,127}$")
name: str = Field(min_length=3, max_length=255)
service: str = Field(min_length=2, max_length=128)
capability: str | None = Field(default=None, max_length=128)
measurement: Literal["SUCCESS_RATIO", "LATENCY_P95", "FRESHNESS", "CORRECTNESS_RATIO"]
valid_population: dict[str, Any]
success_condition: dict[str, Any]
default_window_seconds: int = Field(ge=60, le=2_592_000)
class SLOPolicyCreate(OperationalModel):
key: str = Field(pattern=r"^[a-z][a-z0-9_.-]{2,127}$")
sli_definition_id: uuid.UUID
objective: float = Field(gt=0, le=1)
threshold_ms: float | None = Field(default=None, gt=0)
rolling_window_seconds: int = Field(ge=60, le=2_592_000)
minimum_sample_count: int = Field(ge=1, le=1_000_000)
severity: AlertSeverity
environment: Literal["PRODUCTION", "LAB", "BACKGROUND"]
effective_from: datetime
rationale: str = Field(min_length=10, max_length=4000)
created_by: str = Field(default="operator", min_length=1, max_length=255)
class AlertRuleCreate(OperationalModel):
key: str = Field(pattern=r"^[a-z][a-z0-9_.-]{2,127}$")
alert_type: str = Field(pattern=r"^[A-Z][A-Z0-9_]{2,63}$")
signal: str = Field(min_length=3, max_length=128)
slo_policy_id: uuid.UUID | None = None
condition: dict[str, Any]
pending_seconds: int = Field(ge=0, le=86_400)
severity: AlertSeverity
labels: dict[str, str] = Field(default_factory=dict)
cooldown_seconds: int = Field(ge=0, le=604_800)
recovery_condition: dict[str, Any]
created_by: str = Field(default="operator", min_length=1, max_length=255)
@field_validator("labels")
@classmethod
def validate_labels(cls, value: dict[str, str]) -> dict[str, str]:
if len(value) > 8 or any(len(key) > 64 or len(item) > 128 for key, item in value.items()):
raise ValueError("alert labels must be bounded")
return value
class MaintenanceWindowCreate(OperationalModel):
name: str = Field(min_length=3, max_length=255)
starts_at: datetime
ends_at: datetime
matcher: dict[str, str]
reason: str = Field(min_length=5, max_length=2000)
created_by: str = Field(default="operator", min_length=1, max_length=255)
@model_validator(mode="after")
def ordered(self) -> MaintenanceWindowCreate:
if self.ends_at <= self.starts_at:
raise ValueError("maintenance window end must follow start")
return self
class AlertAction(OperationalModel):
actor: str = Field(min_length=1, max_length=255)
reason: str = Field(min_length=3, max_length=2000)
class SLIDefinitionResponse(SLIDefinitionCreate):
id: uuid.UUID
enabled: bool
created_at: datetime
class SLOPolicyResponse(SLOPolicyCreate):
id: uuid.UUID
revision: int
active: bool
fingerprint: str
created_at: datetime
class AlertRuleResponse(AlertRuleCreate):
id: uuid.UUID
revision: int
active: bool
fingerprint: str
created_at: datetime
class MaintenanceWindowResponse(MaintenanceWindowCreate):
id: uuid.UUID
active: bool
created_at: datetime
class AlertHistoryResponse(OperationalModel):
id: uuid.UUID
alert_id: uuid.UUID
from_state: str | None
to_state: str
actor: str
reason: str
evidence: dict[str, Any]
occurred_at: datetime
class IncidentResponse(OperationalModel):
id: uuid.UUID
fingerprint: str
title: str
state: str
severity: str
root_subject_type: str
root_subject_ref: str
correlation: Literal["RELATED", "LIKELY_ROOT", "DOWNSTREAM", "UNKNOWN"]
first_seen_at: datetime
last_seen_at: datetime
resolved_at: datetime | None
class SLOEvaluationResponse(OperationalModel):
id: uuid.UUID
policy_id: uuid.UUID
policy_key: str
policy_revision: int
sli_key: str
environment: str
objective: float
observed_value: float | None
threshold_ms: float | None
sample_count: int
good_count: int
bad_count: int
state: SLOState
window_start: datetime
window_end: datetime
observed_at: datetime
freshness_seconds: float | None
allowed_bad: float | None
consumed_bad: int | None
remaining_bad: float | None
short_burn_rate: float | None
long_burn_rate: float | None
evidence: dict[str, Any]
class AlertResponse(OperationalModel):
id: uuid.UUID
rule_id: uuid.UUID
fingerprint: str
alert_type: str
severity: str
state: AlertState
source: str
subject_type: str
subject_ref: str
summary: str
details: dict[str, Any]
first_seen_at: datetime
last_seen_at: datetime
firing_at: datetime | None
acknowledged_at: datetime | None
acknowledged_by: str | None
resolved_at: datetime | None
suppressed_until: datetime | None
occurrence_count: int
class CapacitySnapshotResponse(OperationalModel):
id: uuid.UUID
observed_at: datetime
received_at: datetime
node_id: uuid.UUID
node_name: str
accelerator_id: uuid.UUID | None
gpu_total_bytes: int | None
gpu_observed_bytes: int | None
gpu_external_bytes: int | None
gpu_managed_resident_bytes: int | None
gpu_leased_bytes: int | None
gpu_reserve_bytes: int | None
gpu_schedulable_bytes: int | None
pressure_state: str
system_ram_total_bytes: int | None
system_ram_available_bytes: int | None
storage_total_bytes: int | None
storage_free_bytes: int | None
availability: str
freshness_seconds: float
class TrendResponse(OperationalModel):
subject: str
sample_count: int
period_start: datetime | None
period_end: datetime | None
status: Literal["AVAILABLE", "INSUFFICIENT_DATA", "STALE"]
metrics: dict[str, float | int | None]
forecast: dict[str, Any]
class OperationsOverview(OperationalModel):
status: Literal["HEALTHY", "DEGRADED", "OBSERVABILITY_DEGRADED"]
observed_at: datetime
active_alerts: list[AlertResponse]
slo_evaluations: list[SLOEvaluationResponse]
capacity: list[CapacitySnapshotResponse]
capability_health: list[dict[str, Any]]
project_health: list[dict[str, Any]]
recent_failures: list[dict[str, Any]]
history_available: bool
def route_class(path: str) -> str:
if path.startswith("/api/v1/capabilities/") or path == "/v1/embeddings":
return "gateway"
if path.startswith("/api/v1/agent/"):
return "agent"
if path.startswith("/api/v1/admin/"):
return "admin"
if path.startswith("/api/v1/health/"):
return "health"
if path == "/metrics":
return "metrics"
return "control_plane"
@@ -0,0 +1,471 @@
"""M15 backup, restore and disaster-recovery contracts.
Recovery is a first-class control-plane concern: what ModelForge owns authoritatively,
what it can rebuild from an exact upstream identity, what is deliberately discarded and
what is never exportable are separate classifications with separate guarantees.
"""
from __future__ import annotations
import re
import uuid
from datetime import datetime
from enum import StrEnum
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
BACKUP_ID_PATTERN = r"^[a-z0-9][a-z0-9-]{6,62}$"
SHA256_PATTERN = r"^[0-9a-f]{64}$"
# ModelForge stores verified logical snapshots. WAL archiving and continuous point-in-time
# recovery are deliberately out of scope for v1; the value is exported so operators and the
# recovery dashboard never have to infer it.
POINT_IN_TIME_SUPPORT: Literal["SUPPORTED", "NOT_SUPPORTED"] = "NOT_SUPPORTED"
class RecoveryAssetClass(StrEnum):
"""Top-level classification that decides whether state must be copied at all."""
AUTHORITATIVE = "AUTHORITATIVE"
REBUILDABLE = "REBUILDABLE"
EPHEMERAL = "EPHEMERAL"
EXTERNAL = "EXTERNAL"
SECRET = "SECRET" # noqa: S105 - asset classification, not a credential
class SecretRecoveryClass(StrEnum):
RESTORABLE_SECRET = "RESTORABLE_SECRET" # noqa: S105 - asset classification, not a credential
ROTATABLE_SECRET = "ROTATABLE_SECRET" # noqa: S105 - asset classification, not a credential
NON_EXPORTABLE_SECRET = "NON_EXPORTABLE_SECRET" # noqa: S105 - asset classification, not a credential
class ArtifactRecoveryClass(StrEnum):
REHYDRATABLE = "REHYDRATABLE"
NON_REHYDRATABLE = "NON_REHYDRATABLE"
DERIVED = "DERIVED"
LOCAL_ONLY = "LOCAL_ONLY"
class ConfigurationClass(StrEnum):
SOURCE_CONTROLLED = "SOURCE_CONTROLLED"
SECRET = "SECRET" # noqa: S105 - asset classification, not a credential
GENERATED = "GENERATED"
HOST_LOCAL = "HOST_LOCAL"
class BackupState(StrEnum):
PLANNED = "PLANNED"
CREATING = "CREATING"
CREATED = "CREATED"
VERIFYING = "VERIFYING"
VERIFIED = "VERIFIED"
FAILED = "FAILED"
EXPIRED = "EXPIRED"
DELETED = "DELETED"
RESTORE_ELIGIBLE_STATES = frozenset({BackupState.VERIFIED})
BACKUP_TERMINAL_STATES = frozenset({BackupState.FAILED, BackupState.EXPIRED, BackupState.DELETED})
class BackupMethod(StrEnum):
POSTGRES_LOGICAL_CUSTOM = "POSTGRES_LOGICAL_CUSTOM"
MANIFEST_ONLY = "MANIFEST_ONLY"
FILE_COPY = "FILE_COPY"
SOURCE_CONTROL_REFERENCE = "SOURCE_CONTROL_REFERENCE"
NOT_BACKED_UP = "NOT_BACKED_UP"
class RestoreMode(StrEnum):
"""VALIDATION never touches an active target; the destructive modes are operator-gated."""
VALIDATION = "VALIDATION"
REPLACEMENT = "REPLACEMENT"
DISASTER_RECOVERY = "DISASTER_RECOVERY"
class RestorePlanState(StrEnum):
DRAFT = "DRAFT"
PREFLIGHT_PASSED = "PREFLIGHT_PASSED"
PREFLIGHT_FAILED = "PREFLIGHT_FAILED"
CONSUMED = "CONSUMED"
class RestoreState(StrEnum):
PLANNED = "PLANNED"
PREFLIGHT = "PREFLIGHT"
RESTORING_DATABASE = "RESTORING_DATABASE"
RESTORING_CONFIGURATION = "RESTORING_CONFIGURATION"
REHYDRATING_ARTIFACTS = "REHYDRATING_ARTIFACTS"
RECONCILING = "RECONCILING"
VALIDATING = "VALIDATING"
READY = "READY"
FAILED = "FAILED"
MANUAL_INTERVENTION_REQUIRED = "MANUAL_INTERVENTION_REQUIRED"
RESTORE_TERMINAL_STATES = frozenset(
{RestoreState.READY, RestoreState.FAILED, RestoreState.MANUAL_INTERVENTION_REQUIRED}
)
RESTORE_PHASE_ORDER: tuple[RestoreState, ...] = (
RestoreState.PREFLIGHT,
RestoreState.RESTORING_DATABASE,
RestoreState.RESTORING_CONFIGURATION,
RestoreState.REHYDRATING_ARTIFACTS,
RestoreState.RECONCILING,
RestoreState.VALIDATING,
RestoreState.READY,
)
class ArtifactRecoveryState(StrEnum):
PLANNED = "PLANNED"
REHYDRATING = "REHYDRATING"
VERIFYING = "VERIFYING"
RECOVERED = "RECOVERED"
BLOCKED = "BLOCKED"
FAILED = "FAILED"
class RecoveryReadiness(StrEnum):
PROTECTED = "PROTECTED"
REHYDRATABLE = "REHYDRATABLE"
ROTATION_REQUIRED = "ROTATION_REQUIRED"
EXTERNAL_DEPENDENCY = "EXTERNAL_DEPENDENCY"
UNPROTECTED = "UNPROTECTED"
class RecoveryFailureCode(StrEnum):
HASH_MISMATCH = "HASH_MISMATCH"
MANIFEST_HASH_MISMATCH = "MANIFEST_HASH_MISMATCH"
MANIFEST_INCOMPLETE = "MANIFEST_INCOMPLETE"
PAYLOAD_MISSING = "PAYLOAD_MISSING"
BACKUP_NOT_RESTORE_ELIGIBLE = "BACKUP_NOT_RESTORE_ELIGIBLE"
ENCRYPTION_KEY_UNAVAILABLE = "ENCRYPTION_KEY_UNAVAILABLE"
DECRYPTION_FAILED = "DECRYPTION_FAILED"
SCHEMA_TOO_NEW = "SCHEMA_TOO_NEW"
SCHEMA_UNKNOWN = "SCHEMA_UNKNOWN"
POSTGRES_VERSION_INCOMPATIBLE = "POSTGRES_VERSION_INCOMPATIBLE"
DESTINATION_NOT_ISOLATED = "DESTINATION_NOT_ISOLATED"
DESTINATION_NOT_EMPTY = "DESTINATION_NOT_EMPTY"
INSUFFICIENT_CAPACITY = "INSUFFICIENT_CAPACITY"
DESTINATION_UNAVAILABLE = "DESTINATION_UNAVAILABLE"
BACKUP_TOOL_UNAVAILABLE = "BACKUP_TOOL_UNAVAILABLE"
BACKUP_TOOL_FAILED = "BACKUP_TOOL_FAILED"
CONCURRENT_OPERATION = "CONCURRENT_OPERATION"
PATH_NOT_ALLOWED = "PATH_NOT_ALLOWED"
ARCHIVE_UNSAFE = "ARCHIVE_UNSAFE"
ARTIFACT_REHYDRATION_BLOCKED = "ARTIFACT_REHYDRATION_BLOCKED"
ARTIFACT_HASH_MISMATCH = "ARTIFACT_HASH_MISMATCH"
ARTIFACT_NOT_REHYDRATABLE = "ARTIFACT_NOT_REHYDRATABLE"
AUDIT_CHAIN_CORRUPT = "AUDIT_CHAIN_CORRUPT"
RECOVERY_RECONCILIATION_REQUIRED = "RECOVERY_RECONCILIATION_REQUIRED"
SOURCE_CONTROL_REVISION_UNAVAILABLE = "SOURCE_CONTROL_REVISION_UNAVAILABLE"
class RecoveryModel(BaseModel):
model_config = ConfigDict(extra="forbid", from_attributes=True)
class RecoveryPolicyCreate(RecoveryModel):
"""A versioned recovery contract per asset class; never scattered inline rules."""
key: str = Field(pattern=r"^[a-z][a-z0-9_.-]{2,127}$")
name: str = Field(min_length=3, max_length=255)
asset_class: RecoveryAssetClass
backup_method: BackupMethod
retention_days: int = Field(ge=1, le=3650)
minimum_verified_backups: int = Field(ge=1, le=100)
rpo_seconds: int | None = Field(default=None, ge=0, le=2_592_000)
rto_target_seconds: int | None = Field(default=None, ge=0, le=2_592_000)
restore_verification: Literal["FULL_RESTORE", "HASH_ONLY", "MANIFEST_ONLY", "NOT_APPLICABLE"]
encryption_required: bool
external_dependency: bool = False
rehydration_allowed: bool = False
secret_class: SecretRecoveryClass | None = None
rationale: str = Field(min_length=10, max_length=4000)
created_by: str = Field(default="operator", min_length=1, max_length=255)
@model_validator(mode="after")
def coherent(self) -> RecoveryPolicyCreate:
if self.asset_class is RecoveryAssetClass.AUTHORITATIVE:
if self.backup_method in {BackupMethod.NOT_BACKED_UP, BackupMethod.MANIFEST_ONLY}:
raise ValueError("authoritative state requires a payload-bearing backup method")
if self.rpo_seconds is None:
raise ValueError("authoritative state requires an explicit RPO target")
if self.asset_class is RecoveryAssetClass.EPHEMERAL and self.backup_method not in {
BackupMethod.NOT_BACKED_UP,
BackupMethod.MANIFEST_ONLY,
}:
raise ValueError("ephemeral state must not claim a payload backup")
if self.asset_class is RecoveryAssetClass.EXTERNAL and not self.external_dependency:
raise ValueError("external state must be marked as an external dependency")
if self.asset_class is RecoveryAssetClass.SECRET and self.secret_class is None:
raise ValueError("secret state requires an explicit secret recovery class")
if self.rehydration_allowed and self.asset_class is not RecoveryAssetClass.REBUILDABLE:
raise ValueError("only rebuildable state may be rehydrated instead of copied")
return self
class RecoveryPolicyResponse(RecoveryPolicyCreate):
id: uuid.UUID
revision: int
active: bool
fingerprint: str
created_at: datetime
class RecoveryAssetResponse(RecoveryModel):
id: uuid.UUID
key: str
name: str
asset_class: RecoveryAssetClass
owner: str
location: str
backup_method: BackupMethod
restore_method: str
rebuild_method: str | None
rpo_seconds: int | None
readiness: RecoveryReadiness
dependencies: list[str]
notes: str
policy_key: str
updated_at: datetime
class BackupSetCreate(RecoveryModel):
backup_id: str = Field(pattern=BACKUP_ID_PATTERN)
reason: str = Field(min_length=5, max_length=2000)
milestone: str | None = Field(default=None, max_length=64)
legal_hold: bool = False
include_artifact_manifest: bool = True
created_by: str = Field(default="operator", min_length=1, max_length=255)
class BackupManifestEntryResponse(RecoveryModel):
id: uuid.UUID
logical_asset_type: str
object_name: str
relative_path: str
size_bytes: int
sha256: str
source_generation: str
schema_version: str | None
dependency_refs: dict[str, Any]
class BackupSetResponse(RecoveryModel):
id: uuid.UUID
backup_id: str
state: BackupState
policy_key: str
policy_revision: int
modelforge_version: str
modelforge_commit: str | None
schema_revision: str | None
environment_fingerprint: dict[str, Any]
database_identity: dict[str, Any]
destination_root: str
manifest_relative_path: str | None
manifest_sha256: str | None
included_asset_classes: list[str]
excluded_asset_classes: list[str]
payload_bytes: int
encrypted: bool
encryption_algorithm: str | None
encryption_key_id: str | None
verification_details: dict[str, Any]
verified_at: datetime | None
failure_code: str | None
failure_reason: str | None
milestone: str | None
legal_hold: bool
restore_eligible: bool
reason: str
created_by: str
started_at: datetime | None
completed_at: datetime | None
expires_at: datetime | None
created_at: datetime
entries: list[BackupManifestEntryResponse] = Field(default_factory=list)
class RestorePlanCreate(RecoveryModel):
backup_set_id: uuid.UUID
mode: RestoreMode
target_environment: Literal["ISOLATED", "STAGING", "PRODUCTION"]
target_label: str = Field(min_length=3, max_length=128)
database_destination: str = Field(min_length=8, max_length=1024)
artifact_strategy: Literal["NONE", "MANIFEST_ONLY", "REHYDRATE_MISSING", "RESTORE_LOCAL"]
secret_strategy: Literal["ROTATE", "RESTORE_HASHES", "MANUAL"]
node_strategy: Literal["REUSE_CREDENTIAL", "RE_ENROLL", "NONE"]
expected_modelforge_version: str | None = Field(default=None, max_length=64)
validation_requirements: dict[str, Any] = Field(default_factory=dict)
reason: str = Field(min_length=10, max_length=4000)
created_by: str = Field(default="operator", min_length=1, max_length=255)
@field_validator("database_destination")
@classmethod
def supported_destination(cls, value: str) -> str:
if not value.startswith("postgresql+psycopg://"):
raise ValueError("restore destinations must be PostgreSQL SQLAlchemy URLs")
return value
@model_validator(mode="after")
def guarded(self) -> RestorePlanCreate:
if self.mode is RestoreMode.VALIDATION and self.target_environment == "PRODUCTION":
raise ValueError("validation restores must never target a production environment")
if self.target_environment == "PRODUCTION" and self.mode is not RestoreMode.DISASTER_RECOVERY:
raise ValueError("production targets require an explicit disaster-recovery restore")
return self
class RestorePlanResponse(RecoveryModel):
id: uuid.UUID
backup_set_id: uuid.UUID
backup_id: str
mode: RestoreMode
state: RestorePlanState
target_environment: str
target_label: str
database_destination_redacted: str
artifact_strategy: str
secret_strategy: str
node_strategy: str
expected_modelforge_version: str | None
preflight: dict[str, Any]
validation_requirements: dict[str, Any]
fingerprint: str
reason: str
created_by: str
created_at: datetime
class RestoreOperationResponse(RecoveryModel):
id: uuid.UUID
plan_id: uuid.UUID
backup_set_id: uuid.UUID
backup_id: str
state: RestoreState
mode: RestoreMode
attempt: int
idempotency_key: str
preflight_result: dict[str, Any]
phase_durations: dict[str, float]
source_fingerprint: dict[str, Any]
restored_fingerprint: dict[str, Any]
fingerprint_diff: dict[str, Any]
validation_result: dict[str, Any]
rpo_seconds: float | None
rto_seconds: float | None
failure_code: str | None
failure_reason: str | None
started_at: datetime
updated_at: datetime
ready_at: datetime | None
class RestoreOperationEventResponse(RecoveryModel):
id: uuid.UUID
restore_operation_id: uuid.UUID
from_state: str | None
to_state: str
phase: str
actor: str
reason: str
evidence: dict[str, Any]
occurred_at: datetime
class RestoreAdvanceRequest(RecoveryModel):
actor: str = Field(default="operator", min_length=1, max_length=255)
reason: str = Field(min_length=5, max_length=2000)
stop_after: RestoreState | None = None
simulate_interruption_after: RestoreState | None = None
class ArtifactRecoveryCreate(RecoveryModel):
artifact_set_id: uuid.UUID
target_storage_root_id: uuid.UUID
reason: str = Field(min_length=10, max_length=2000)
restore_operation_id: uuid.UUID | None = None
created_by: str = Field(default="operator", min_length=1, max_length=255)
class ArtifactRecoveryResponse(RecoveryModel):
id: uuid.UUID
restore_operation_id: uuid.UUID | None
artifact_set_id: uuid.UUID
model_revision_id: uuid.UUID
recovery_class: ArtifactRecoveryClass
state: ArtifactRecoveryState
upstream_repository: str | None
upstream_commit_sha: str | None
target_storage_root_id: uuid.UUID
expected_files: list[dict[str, Any]]
verified_files: list[dict[str, Any]]
bytes_total: int
bytes_recovered: int
download_plan_id: uuid.UUID | None
artifact_job_id: uuid.UUID | None
lineage: dict[str, Any]
duration_seconds: float | None
failure_code: str | None
failure_reason: str | None
started_at: datetime
completed_at: datetime | None
class RecoveryReadinessEntry(RecoveryModel):
asset_key: str
asset_name: str
asset_class: RecoveryAssetClass
readiness: RecoveryReadiness
policy_key: str
rpo_seconds: int | None
detail: str
class RecoveryDashboard(RecoveryModel):
"""Every field is measured; unknown values stay null instead of becoming a fiction."""
observed_at: datetime
point_in_time_support: Literal["SUPPORTED", "NOT_SUPPORTED"]
latest_verified_backup_id: str | None
latest_verified_backup_at: datetime | None
latest_verified_backup_age_seconds: float | None
latest_verified_schema_revision: str | None
backup_states: dict[str, int]
verified_backup_count: int
stale_backup: bool
backup_staleness_threshold_seconds: int
last_restore_rehearsal_at: datetime | None
last_restore_rehearsal_state: str | None
observed_restore_seconds: float | None
observed_rpo_seconds: float | None
protected_asset_count: int
unprotected_assets: list[str]
readiness: list[RecoveryReadinessEntry]
coverage_ratio: float
estimated_protected_bytes: int
estimated_rehydratable_bytes: int
destination_capacity_bytes: int | None
destination_free_bytes: int | None
class BackupCapacityEstimate(RecoveryModel):
bytes_to_copy: int
bytes_manifest_only: int
estimated_protected_bytes: int
available_bytes: int | None
capacity_bytes: int | None
sufficient: bool
detail: str
def redact_database_url(url: str) -> str:
"""Never journal or return a restore DSN that still carries its password."""
return re.sub(r"://([^:/@]+):[^@]*@", r"://\1:***@", url)
@@ -0,0 +1,239 @@
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
from modelforge_api.domain.enums import ArtifactStatus, ModelLifecycle, StorageRootStatus
class RegistryModel(BaseModel):
model_config = ConfigDict(extra="forbid", from_attributes=True)
class Page[Item](RegistryModel):
items: list[Item]
page: int
page_size: int
total: int
pages: int
class ModelCreate(RegistryModel):
key: str = Field(min_length=1, max_length=128, pattern=r"^[a-z0-9][a-z0-9._-]*$")
display_name: str = Field(min_length=1, max_length=255)
description: str | None = None
source_type: Literal["huggingface", "local", "custom"] = "huggingface"
upstream_provider: str = Field(min_length=1, max_length=64)
upstream_source: str = Field(min_length=1, max_length=255)
upstream_metadata: dict[str, Any] = Field(default_factory=dict)
local_metadata: dict[str, Any] = Field(default_factory=dict)
interpretation_metadata: dict[str, Any] = Field(default_factory=dict)
family: str | None = None
modalities: list[str] = Field(default_factory=list)
parameter_metadata: dict[str, Any] = Field(default_factory=dict)
license_metadata: dict[str, Any] = Field(default_factory=lambda: {"status": "unknown"})
lifecycle: ModelLifecycle = ModelLifecycle.CANDIDATE
class ModelUpdate(RegistryModel):
display_name: str | None = Field(default=None, min_length=1, max_length=255)
description: str | None = None
local_metadata: dict[str, Any] | None = None
interpretation_metadata: dict[str, Any] | None = None
lifecycle: ModelLifecycle | None = None
class ModelResponse(ModelCreate):
id: uuid.UUID
created_at: datetime
updated_at: datetime
deprecated_at: datetime | None = None
revision_count: int = 0
artifact_count: int = 0
verification_status: str = "unverified"
deployment_status: str = "not_deployed"
class RevisionCreate(RegistryModel):
upstream_revision: str = Field(min_length=1, max_length=255)
resolved_commit_sha: str = Field(pattern=r"^[0-9a-f]{40,64}$")
metadata_snapshot: dict[str, Any] = Field(default_factory=dict)
class RevisionResponse(RevisionCreate):
id: uuid.UUID
model_id: uuid.UUID
discovered_at: datetime
approved_at: datetime | None = None
immutable_at: datetime
deprecated_at: datetime | None = None
archived_at: datetime | None = None
created_at: datetime
updated_at: datetime
class ArtifactLocationCreate(RegistryModel):
storage_root_id: uuid.UUID
relative_path: str = Field(min_length=1)
status: ArtifactStatus = ArtifactStatus.LOCAL
@field_validator("relative_path")
@classmethod
def safe_relative_path(cls, value: str) -> str:
normalized = value.replace("\\", "/")
if normalized.startswith("/") or ".." in normalized.split("/"):
raise ValueError("relative_path must remain within the storage root")
return normalized
class ArtifactLocationResponse(ArtifactLocationCreate):
id: uuid.UUID
artifact_id: uuid.UUID | None = None
derived_artifact_id: uuid.UUID | None = None
size_bytes: int | None = None
observed_sha256: str | None = None
last_checked_at: datetime | None = None
created_at: datetime
updated_at: datetime
class ArtifactCreate(RegistryModel):
filename: str = Field(min_length=1, max_length=512)
artifact_type: str = Field(min_length=1, max_length=64)
serialization_format: str = Field(min_length=1, max_length=64)
sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
size_bytes: int = Field(ge=0)
status: ArtifactStatus = ArtifactStatus.REMOTE
security_status: str = "unverified"
license_status: str = "unknown"
locations: list[ArtifactLocationCreate] = Field(default_factory=list)
class ArtifactResponse(RegistryModel):
id: uuid.UUID
revision_id: uuid.UUID
filename: str
artifact_type: str
serialization_format: str
sha256: str
size_bytes: int
status: ArtifactStatus
security_status: str
license_status: str
quarantined: bool
verification_details: dict[str, Any]
verified_at: datetime | None = None
immutable_at: datetime | None = None
deprecated_at: datetime | None = None
archived_at: datetime | None = None
created_at: datetime
updated_at: datetime
locations: list[ArtifactLocationResponse] = Field(default_factory=list)
class DerivedArtifactCreate(RegistryModel):
revision_id: uuid.UUID
source_artifact_ids: list[uuid.UUID] = Field(min_length=1)
filename: str = Field(min_length=1, max_length=512)
artifact_type: str = Field(min_length=1, max_length=64)
sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
size_bytes: int = Field(ge=0)
transformation_type: str = Field(min_length=1, max_length=64)
tool: str = Field(min_length=1, max_length=128)
tool_version: str = Field(min_length=1, max_length=128)
configuration: dict[str, Any] = Field(default_factory=dict)
environment_snapshot: dict[str, Any] = Field(default_factory=dict)
status: ArtifactStatus = ArtifactStatus.REMOTE
locations: list[ArtifactLocationCreate] = Field(default_factory=list)
class DerivedSourceResponse(RegistryModel):
artifact_id: uuid.UUID
sha256: str
ordinal: int
class DerivedArtifactResponse(RegistryModel):
id: uuid.UUID
revision_id: uuid.UUID
filename: str
artifact_type: str
sha256: str
size_bytes: int
transformation_type: str
tool: str
tool_version: str
configuration: dict[str, Any]
environment_snapshot: dict[str, Any]
status: ArtifactStatus
immutable_at: datetime | None = None
created_at: datetime
updated_at: datetime
sources: list[DerivedSourceResponse]
locations: list[ArtifactLocationResponse]
class StorageRootCreate(RegistryModel):
compute_node_id: uuid.UUID
name: str = Field(min_length=1, max_length=128)
purpose: str = Field(default="model_artifacts", min_length=1, max_length=64)
path: str = Field(min_length=1)
agent_path: str | None = Field(default=None, min_length=1)
reserve_bytes: int = Field(default=0, ge=0)
reserve_percent: int = Field(default=10, ge=0, le=100)
class StorageRootObservation(RegistryModel):
writable: bool
capacity_bytes: int | None = Field(default=None, ge=0)
free_bytes: int | None = Field(default=None, ge=0)
details: dict[str, Any] = Field(default_factory=dict)
class StorageRootUpdate(RegistryModel):
agent_path: str = Field(min_length=1)
class StorageRootResponse(StorageRootCreate):
id: uuid.UUID
status: StorageRootStatus
writable: bool
capacity_bytes: int | None = None
free_bytes: int | None = None
capacity_observed_at: datetime | None = None
validation_details: dict[str, Any]
deprecated_at: datetime | None = None
created_at: datetime
updated_at: datetime
class CapacityDecision(RegistryModel):
allowed: bool
status: StorageRootStatus
requested_bytes: int
usable_bytes: int | None
reason: str
class VerifyResponse(RegistryModel):
artifact_id: uuid.UUID
status: ArtifactStatus
expected_sha256: str
observed_sha256: str | None
observed_size_bytes: int | None
checked_at: datetime
location_id: uuid.UUID
class DependencyReference(RegistryModel):
resource_type: str
resource_id: uuid.UUID
relation: str
class DependencyConflict(RegistryModel):
message: str
dependencies: list[DependencyReference]
@@ -0,0 +1,223 @@
"""The v1 release contract.
One authoritative product version, and the compatibility ranges that version promises. Before M17
the version existed four times — backend, Node Agent, Runtime Worker and the console each declared
``0.1.0`` independently — which is three opportunities for a release to describe itself wrongly.
The version lives in the repository's ``VERSION`` file. Every packaged manifest is checked against
it rather than trusted to agree, because a manifest that has drifted looks exactly like one that has
not.
Compatibility is stated, not implied. An application refusing to start against a schema it does not
support is a far better outcome than one that starts and writes rows the next version cannot read.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from enum import StrEnum
from pathlib import Path
# --------------------------------------------------------------------------- version
#: Schema revisions this application version can run against, oldest first. The application refuses
#: to serve against anything outside this list rather than guessing.
SUPPORTED_SCHEMA_REVISIONS: tuple[str, ...] = ("20260830_0024",)
#: The revision a clean installation and a completed upgrade must both arrive at.
TARGET_SCHEMA_REVISION = SUPPORTED_SCHEMA_REVISIONS[-1]
#: Schema revisions from which the upgrade tool may migrate to the target. These are deliberately
#: separate from runtime compatibility: the node-decommission code reads columns that do not exist
#: at 0021, so serving on 0021 would be unsafe even though migrating from it is supported.
SUPPORTED_UPGRADE_SOURCE_SCHEMA_REVISIONS: tuple[str, ...] = (
"20260827_0021",
"20260828_0022",
"20260830_0023",
TARGET_SCHEMA_REVISION,
)
#: The oldest release an in-place upgrade to this version is supported from.
MINIMUM_UPGRADE_SOURCE = "v1.0.0"
#: Agent protocol versions this control plane accepts.
SUPPORTED_AGENT_PROTOCOL_VERSIONS: tuple[int, ...] = (1,)
#: The protocol version this control plane itself speaks.
CURRENT_AGENT_PROTOCOL_VERSION = SUPPORTED_AGENT_PROTOCOL_VERSIONS[-1]
#: Release channels. v1 ships one; the enum exists so adding another is a typed change.
RELEASE_CHANNEL = "stable"
#: Minimum PostgreSQL major version. The schema uses partial unique indexes and generated columns
#: that older majors either lack or plan differently.
MINIMUM_POSTGRES_MAJOR = 16
def _read_version_file() -> str:
"""Read the repository's VERSION file, falling back to the packaged distribution metadata."""
here = Path(__file__).resolve()
for parent in here.parents:
candidate = parent / "VERSION"
if candidate.is_file():
return candidate.read_text("utf-8").strip()
# An installed wheel has no VERSION file beside it; the distribution metadata is authoritative
# there, and the packaging test proves the two agree at build time.
from importlib.metadata import PackageNotFoundError, version
try:
return version("modelforge-api")
except PackageNotFoundError: # pragma: no cover - only in a broken install
return "0.0.0"
PRODUCT_VERSION = _read_version_file()
PRODUCT_NAME = "ITWorx ModelForge"
# --------------------------------------------------------------------------- semver
@dataclass(frozen=True, slots=True)
class SemanticVersion:
major: int
minor: int
patch: int
prerelease: str | None = None
@classmethod
def parse(cls, value: str) -> SemanticVersion:
core, _, prerelease = value.partition("-")
parts = core.split(".")
if len(parts) != 3 or not all(part.isdigit() for part in parts):
raise ValueError(f"{value!r} is not a MAJOR.MINOR.PATCH version")
major, minor, patch = (int(part) for part in parts)
return cls(major, minor, patch, prerelease or None)
def __str__(self) -> str:
core = f"{self.major}.{self.minor}.{self.patch}"
return f"{core}-{self.prerelease}" if self.prerelease else core
@property
def is_prerelease(self) -> bool:
return self.prerelease is not None
# --------------------------------------------------------------------------- compatibility
class Compatibility(StrEnum):
"""The four answers a compatibility question can have. There is no fifth, and no silent pass."""
COMPATIBLE = "COMPATIBLE"
TOO_OLD = "TOO_OLD"
TOO_NEW = "TOO_NEW"
UNKNOWN = "UNKNOWN"
def schema_compatibility(revision: str | None) -> Compatibility:
"""Is this application version able to run against that schema revision?"""
if revision is None:
return Compatibility.UNKNOWN
if revision in SUPPORTED_SCHEMA_REVISIONS:
return Compatibility.COMPATIBLE
# Revisions are date-ordered identifiers, so a straight comparison against the oldest and newest
# supported revision tells old from new without a migration graph walk.
if revision < SUPPORTED_SCHEMA_REVISIONS[0]:
return Compatibility.TOO_OLD
if revision > SUPPORTED_SCHEMA_REVISIONS[-1]:
return Compatibility.TOO_NEW
return Compatibility.UNKNOWN
def agent_protocol_compatibility(protocol_version: int | None) -> Compatibility:
"""Can an agent speaking that protocol version talk to this control plane?"""
if protocol_version is None:
return Compatibility.UNKNOWN
if protocol_version in SUPPORTED_AGENT_PROTOCOL_VERSIONS:
return Compatibility.COMPATIBLE
if protocol_version < SUPPORTED_AGENT_PROTOCOL_VERSIONS[0]:
return Compatibility.TOO_OLD
return Compatibility.TOO_NEW
def upgrade_required(compatibility: Compatibility) -> str | None:
"""What the operator has to do, in one sentence, or None when nothing is required."""
match compatibility:
case Compatibility.COMPATIBLE:
return None
case Compatibility.TOO_OLD:
return "upgrade the component to a release that speaks the current protocol"
case Compatibility.TOO_NEW:
return "upgrade the control plane, which is older than the component reporting to it"
case Compatibility.UNKNOWN:
return "the version could not be determined and is refused rather than assumed"
# --------------------------------------------------------------------------- build identity
@dataclass(frozen=True, slots=True)
class BuildIdentity:
"""What a running binary can say about where it came from.
Everything here is either compiled in at build time or read from the environment the image was
built with. Nothing is inferred at runtime, because a build identity that a running process can
talk itself into is worth nothing during an incident.
"""
version: str
source_commit: str | None
built_at: str | None
image_digest: str | None
channel: str
schema_revision: str
agent_protocol_version: int
def as_dict(self) -> dict[str, object]:
return {
"version": self.version,
"source_commit": self.source_commit,
"built_at": self.built_at,
"image_digest": self.image_digest,
"channel": self.channel,
"schema_revision": self.schema_revision,
"agent_protocol_version": self.agent_protocol_version,
}
def _clean(value: str | None) -> str | None:
"""Treat an unsubstituted build argument as absent rather than reporting it as a fact."""
if value is None:
return None
stripped = value.strip()
if not stripped or stripped.lower() in {"unknown", "none", "null"}:
return None
return stripped
def build_identity(
*,
source_commit: str | None = None,
built_at: str | None = None,
image_digest: str | None = None,
) -> BuildIdentity:
return BuildIdentity(
version=PRODUCT_VERSION,
source_commit=_clean(source_commit),
built_at=_clean(built_at),
image_digest=_clean(image_digest),
channel=RELEASE_CHANNEL,
schema_revision=TARGET_SCHEMA_REVISION,
agent_protocol_version=CURRENT_AGENT_PROTOCOL_VERSION,
)
def utc_now_iso() -> str:
return datetime.now(UTC).isoformat()
@@ -0,0 +1,249 @@
from __future__ import annotations
import re
import uuid
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
RuntimeAdapterName = Literal[
"sentence_transformers",
"qwen3_reranker",
"transformers_trocr",
"transformers_siglip2",
"transformers_whisper",
"transformers",
"vllm",
"llama_cpp",
"diffusers",
"custom",
]
CompatibilityStatus = Literal["compatible", "incompatible", "unknown", "requires_probe", "blocked"]
ProbeStatus = Literal[
"queued",
"preparing",
"loading",
"healthchecking",
"ready",
"unloading",
"completed",
"failed",
"cancelled",
]
class RuntimeModel(BaseModel):
model_config = ConfigDict(extra="forbid", from_attributes=True)
class RuntimeEnvironmentCreate(RuntimeModel):
name: str = Field(min_length=1, max_length=255)
adapter: RuntimeAdapterName
runtime_version: str = Field(min_length=1, max_length=128)
image_repository: str = Field(min_length=1, max_length=255)
image_digest: str = Field(pattern=r"^sha256:[a-f0-9]{64}$")
python_version: str = Field(min_length=1, max_length=64)
cuda_runtime_version: str | None = Field(default=None, max_length=64)
package_versions: dict[str, str] = Field(default_factory=dict)
supported_model_types: list[str] = Field(default_factory=list)
supported_formats: list[str] = Field(default_factory=list)
supported_modalities: list[str] = Field(default_factory=list)
network_policy: Literal["offline_control_plane_only"] = "offline_control_plane_only"
class RuntimeEnvironmentResponse(RuntimeEnvironmentCreate):
id: uuid.UUID
fingerprint: str
immutable_at: datetime
created_at: datetime
class RuntimeProfileCreate(RuntimeModel):
name: str = Field(min_length=1, max_length=255)
runtime_environment_id: uuid.UUID
artifact_set_id: uuid.UUID
dtype: Literal["float32", "float16", "bfloat16"] = "bfloat16"
quantization: str | None = Field(default=None, max_length=64)
modality: Literal[
"embedding", "reranking", "text_generation", "vision", "document", "audio", "diffusion"
]
max_sequence_length: int = Field(default=128, ge=1, le=131072)
batch_size: int = Field(default=1, ge=1, le=128)
concurrency: int = Field(default=1, ge=1, le=128)
device_policy: Literal["cuda_required", "cuda_preferred", "cpu_only"] = "cuda_required"
gpu_memory_policy: dict[str, Any] = Field(default_factory=dict)
launch_parameters: dict[str, Any] = Field(default_factory=dict)
environment_variables: dict[str, str] = Field(default_factory=dict)
trust_remote_code: Literal[False] = False
network_egress: Literal[False] = False
@field_validator("environment_variables")
@classmethod
def reject_secrets(cls, value: dict[str, str]) -> dict[str, str]:
forbidden = {"TOKEN", "SECRET", "PASSWORD", "KEY", "CREDENTIAL"}
if any(
forbidden.intersection(filter(None, re.split(r"[^A-Z0-9]+", key.upper())))
for key in value
):
raise ValueError("runtime profile environment cannot contain secrets")
return value
@model_validator(mode="after")
def fixed_probe_shape(self) -> RuntimeProfileCreate:
if self.modality == "embedding" and self.batch_size != 1:
raise ValueError("M4 embedding probes require batch_size=1")
return self
class RuntimeProfileResponse(RuntimeProfileCreate):
id: uuid.UUID
adapter: str
runtime_version: str
image_digest: str
version: int
fingerprint: str
health_contract: dict[str, Any]
immutable_at: datetime
created_at: datetime
class CompatibilityAssessmentCreate(RuntimeModel):
runtime_profile_id: uuid.UUID
compute_node_id: uuid.UUID
class CompatibilityAssessmentResponse(RuntimeModel):
id: uuid.UUID
artifact_set_id: uuid.UUID
runtime_profile_id: uuid.UUID
compute_node_id: uuid.UUID
adapter: str
runtime_version: str
status: CompatibilityStatus
static_result: dict[str, Any]
evidence: dict[str, Any]
blockers: list[str]
warnings: list[str]
required_approvals: list[str]
hardware_facts: dict[str, Any]
artifact_facts: dict[str, Any]
environment_fingerprint: str
stale: bool
stale_reason: str | None
created_at: datetime
class ExecutionApprovalCreate(RuntimeModel):
scope: Literal["lab_execution"] = "lab_execution"
reason: str = Field(min_length=8, max_length=2000)
approved_by: str = Field(min_length=1, max_length=255)
expires_at: datetime | None = None
class ExecutionApprovalResponse(RuntimeModel):
id: uuid.UUID
artifact_set_id: uuid.UUID
scope: str
status: str
evidence_fingerprint: str
reason: str
approved_by: str
approved_at: datetime
expires_at: datetime | None
revoked_at: datetime | None
stale: bool
class RuntimeProbeCreate(RuntimeModel):
compatibility_assessment_id: uuid.UUID
execution_approval_id: uuid.UUID
input_text: Literal["ModelForge runtime compatibility probe"] = (
"ModelForge runtime compatibility probe"
)
class RuntimeProbeResponse(RuntimeModel):
id: uuid.UUID
artifact_set_id: uuid.UUID
runtime_profile_id: uuid.UUID
compute_node_id: uuid.UUID
compatibility_assessment_id: uuid.UUID
execution_approval_id: uuid.UUID
status: ProbeStatus
phase: str | None
attempt_count: int
cancel_requested: bool
load_result: dict[str, Any]
health_result: dict[str, Any]
inference_result: dict[str, Any]
unload_result: dict[str, Any]
measured_resources: dict[str, Any]
runtime_facts: dict[str, Any]
environment_fingerprint: str
failure_code: str | None
failure_message: str | None
logs_reference: str | None
started_at: datetime | None
finished_at: datetime | None
created_at: datetime
updated_at: datetime
class DeploymentCandidateResponse(RuntimeModel):
id: uuid.UUID
artifact_set_id: uuid.UUID
runtime_profile_id: uuid.UUID
compute_node_id: uuid.UUID
compatibility_assessment_id: uuid.UUID
runtime_probe_id: uuid.UUID
channel: Literal["lab"]
status: Literal["lab_ready"]
production: Literal[False]
health_contract: dict[str, Any]
measured_resources: dict[str, Any]
created_at: datetime
class AgentRuntimeProbeLease(RuntimeModel):
probe_id: uuid.UUID
lease_token: str
lease_expires_at: datetime
artifact_set_id: uuid.UUID
revision_sha: str = Field(pattern=r"^[a-f0-9]{40,64}$")
artifact_root: str
artifact_relative_path: str
expected_manifest: dict[str, Any]
runtime_profile: dict[str, Any]
runtime_environment: dict[str, Any]
probe_input: Literal["ModelForge runtime compatibility probe"]
class AgentRuntimeProbeProgress(RuntimeModel):
lease_token: str = Field(min_length=32, max_length=512)
status: Literal["preparing", "loading", "healthchecking", "ready", "unloading"]
details: dict[str, Any] = Field(default_factory=dict)
class AgentRuntimeProbeControl(RuntimeModel):
accepted: bool
cancel_requested: bool
lease_expires_at: datetime
class AgentRuntimeProbeComplete(RuntimeModel):
lease_token: str = Field(min_length=32, max_length=512)
load_result: dict[str, Any]
health_result: dict[str, Any]
inference_result: dict[str, Any]
unload_result: dict[str, Any]
measured_resources: dict[str, Any]
runtime_facts: dict[str, Any]
environment_fingerprint: str = Field(pattern=r"^[a-f0-9]{64}$")
class AgentRuntimeProbeFailure(RuntimeModel):
lease_token: str = Field(min_length=32, max_length=512)
failure_code: str = Field(min_length=1, max_length=64)
failure_message: str = Field(min_length=1, max_length=2000)
details: dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,172 @@
import uuid
from datetime import UTC, datetime
from typing import Any
from pydantic import BaseModel, Field
from .contracts import (
CapabilityCategory,
CapabilityContractManifest,
CapabilityStability,
EvaluationType,
ProjectBindingManifest,
ResourceClass,
)
from .enums import (
HealthStatus,
ModelLifecycle,
VerificationStatus,
)
class HealthResponse(BaseModel):
status: str = "ok"
service: str = "modelforge-api"
version: str
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
class ReadinessResponse(BaseModel):
status: HealthStatus
checks: dict[str, HealthStatus]
version: str
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
class SystemMetadata(BaseModel):
name: str
version: str
environment: str
api_version: str = "v1"
release_channel: str = "stable"
production_inference_available: bool = False
class ReleaseCompatibility(BaseModel):
"""What this build will and will not talk to, stated rather than implied."""
schema_revision: str
supported_schema_revisions: list[str]
agent_protocol_version: int
supported_agent_protocol_versions: list[int]
minimum_upgrade_source: str
minimum_postgres_major: int
class ReleaseInfo(BaseModel):
"""Build identity for the running process.
Deliberately free of anything sensitive: no paths, no configuration values, no credentials —
only what an operator needs to answer "which build is this, and what does it support?".
"""
name: str
version: str
release_channel: str
source_commit: str | None = None
built_at: str | None = None
image_digest: str | None = None
compatibility: ReleaseCompatibility
class ErrorDetail(BaseModel):
code: str
message: str
correlation_id: str
details: dict[str, Any] = Field(default_factory=dict)
class ErrorEnvelope(BaseModel):
error: ErrorDetail
class GPUInfo(BaseModel):
index: int
name: str
uuid: str | None = None
memory_total_mb: int | None = None
memory_used_mb: int | None = None
utilization_gpu_percent: int | None = None
utilization_memory_percent: int | None = None
temperature_c: int | None = None
power_draw_w: float | None = None
telemetry_available: bool = True
error: str | None = None
class CapabilityContractResponse(BaseModel):
key: str
version: int
description: str
contract: CapabilityContractManifest
stable_deployment: dict[str, Any] | None = None
class CapabilityEstateResponse(BaseModel):
capability: str
version: int
category: CapabilityCategory
purpose: str
declared_stability: CapabilityStability
operational_state: str
current_deployment_id: uuid.UUID | None = None
model: str | None = None
revision: str | None = None
runtime: str | None = None
node: str | None = None
resource_class: ResourceClass
measured_required_vram_bytes: int | None = None
consumers: list[str]
privacy_class: str
evaluation_type: EvaluationType
evaluation_state: str
class InstallationDependencyResponse(BaseModel):
capability: str
version: int
deployment_id: uuid.UUID
channel: str
production: bool
project_consumers: list[str]
active_project_consumers: list[str]
project_fit_evidence_ids: list[uuid.UUID]
evaluation_run_ids: list[uuid.UUID]
last_used_at: datetime | None
class ModelInstallationRationaleResponse(BaseModel):
model_id: uuid.UUID
display_name: str
upstream_source: str
installed: bool
installed_bytes: int
dependencies: list[InstallationDependencyResponse]
can_delete: bool
deletion_blockers: list[str]
class CandidateSummary(BaseModel):
id: str
display_name: str
source: str
intended_capabilities: list[str]
proposed_role: str
preferred_runtime: str | None = None
lifecycle: ModelLifecycle = ModelLifecycle.CANDIDATE
verification_status: VerificationStatus = VerificationStatus.UNVERIFIED
deployment_status: str = "not_deployed"
class ProjectBindingResponse(BaseModel):
capability: str
contract_version: int
binding: ProjectBindingManifest
class ProjectResponse(BaseModel):
id: str
name: str
description: str
bindings: list[ProjectBindingResponse]
notes: list[str] = Field(default_factory=list)
@@ -0,0 +1,764 @@
from __future__ import annotations
import base64
import binascii
import math
import re
import uuid
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
CAPABILITY_VERSION_KEY = re.compile(r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+@[1-9][0-9]*$")
ServingOperation = Literal["load", "invoke", "health", "drain", "unload"]
class ServingModel(BaseModel):
model_config = ConfigDict(extra="forbid", from_attributes=True)
class SupplyChainReview(ServingModel):
exact_revision_reviewed: Literal[True]
artifact_hashes_reviewed: Literal[True]
safetensors_only: Literal[True]
remote_code_required: Literal[False]
pickle_present: Literal[False]
scanner_evidence_reviewed: Literal[True]
provenance_complete: Literal[True]
runtime_offline_reviewed: Literal[True]
dependency_provenance_reviewed: Literal[True]
license_reviewed: Literal[True]
license_identifier: str = Field(min_length=1, max_length=255)
class ProductionApprovalCreate(ServingModel):
capability: Literal["rag.embedding"] = "rag.embedding"
contract_version: Literal[1] = 1
approved_by: str = Field(min_length=1, max_length=255)
reason: str = Field(min_length=16, max_length=4000)
supply_chain_review: SupplyChainReview
deployment_config: dict[str, Any] = Field(default_factory=dict)
class ProductionApprovalResponse(ServingModel):
id: uuid.UUID
deployment_candidate_id: uuid.UUID
capability_contract_id: uuid.UUID
artifact_set_id: uuid.UUID
runtime_profile_id: uuid.UUID
compute_node_id: uuid.UUID
deployment_config: dict[str, Any]
supply_chain_evidence: dict[str, Any]
evidence_fingerprint: str
status: str
approved_by: str
reason: str
approved_at: datetime
revoked_at: datetime | None
stale: bool = False
class CapabilityPromotionCreate(ServingModel):
production_approval_id: uuid.UUID
residency_policy: Literal["always_warm", "keep_warm", "load_on_demand"] = "keep_warm"
keep_warm_seconds: int = Field(default=900, ge=5, le=86400)
max_concurrency: Literal[1] = 1
max_queue_depth: int = Field(default=16, ge=1, le=1024)
routing_weight: int = Field(default=100, ge=0, le=100)
rollback_policy: dict[str, Any] = Field(
default_factory=lambda: {"mode": "drain_to_unavailable", "previous_deployment_id": None}
)
class EmbeddingSpaceResponse(ServingModel):
id: uuid.UUID
identity_digest: str
dimension: int
normalized: bool
migration_class: Literal["requires_reindex"]
identity_facts: dict[str, Any]
created_at: datetime
class ResourceEnvelopeResponse(ServingModel):
id: uuid.UUID
runtime_probe_id: uuid.UUID
accelerator_kind: str
accelerator_uuid: str
environment_fingerprint: str
concurrency: int
batch_size: int
max_sequence_length: int
baseline_vram_bytes: int
resident_vram_bytes: int
peak_vram_bytes: int
required_vram_bytes: int
cold_load_time_ms: float
inference_latency_ms: float
stale: bool
stale_reason: str | None
class ResidencyResponse(ServingModel):
id: uuid.UUID
state: str
worker_instance_id: str | None
load_count: int
active_requests: int
measured_resident_vram_bytes: int
external_baseline_vram_bytes: int
health: dict[str, Any]
resident_since: datetime | None
last_used_at: datetime | None
failure_code: str | None
failure_message: str | None
generation: int = 1
transition_reason: str | None = None
class CapabilityDeploymentResponse(ServingModel):
id: uuid.UUID
capability: str
contract_version: int
deployment_candidate_id: uuid.UUID
production_approval_id: uuid.UUID | None
execution_approval_id: uuid.UUID | None
artifact_set_id: uuid.UUID
runtime_profile_id: uuid.UUID
compute_node_id: uuid.UUID
accelerator_id: uuid.UUID
embedding_space: EmbeddingSpaceResponse | None
resource_envelope: ResourceEnvelopeResponse
residency: ResidencyResponse
channel: str
status: str
production: bool
health_status: str
routing_weight: int
fallback_policy: dict[str, Any]
residency_policy: str
keep_warm_seconds: int
max_concurrency: int
max_queue_depth: int
config_fingerprint: str
provenance: dict[str, Any]
rollback_policy: dict[str, Any]
promoted_at: datetime | None
created_at: datetime
class ServiceClientCreate(ServingModel):
name: str = Field(min_length=1, max_length=255, pattern=r"^[a-z][a-z0-9-]*$")
allowed_capabilities: list[str] = Field(min_length=1, max_length=32)
requests_per_minute: int = Field(default=60, ge=1, le=10000)
max_concurrent_requests: int = Field(default=1, ge=1, le=64)
workload_priority: Literal["production", "interactive", "background", "benchmark"] = (
"production"
)
project_key: str | None = Field(default=None, pattern=r"^[a-z][a-z0-9-]*$")
integration_environment: Literal["production", "shadow", "evaluation", "lab"] | None = None
purpose: str | None = Field(default=None, min_length=3, max_length=500)
credential_expires_at: datetime | None = None
@field_validator("allowed_capabilities")
@classmethod
def validate_capability_scopes(cls, value: list[str]) -> list[str]:
if len(set(value)) != len(value) or any(
not CAPABILITY_VERSION_KEY.fullmatch(v) for v in value
):
raise ValueError("capability scopes must be unique versioned capability keys")
return value
@model_validator(mode="after")
def validate_project_scope(self) -> ServiceClientCreate:
project_fields = (self.project_key, self.integration_environment, self.purpose)
if any(value is not None for value in project_fields) and not all(
value is not None for value in project_fields
):
raise ValueError("project_key, integration_environment and purpose are required together")
if self.project_key is not None and len(self.allowed_capabilities) != 1:
raise ValueError("a project service identity must bind exactly one capability")
return self
class ServiceClientResponse(ServingModel):
id: uuid.UUID
name: str
status: str
allowed_capabilities: list[str]
requests_per_minute: int
max_concurrent_requests: int
workload_priority: str
project_key: str | None
project_binding_id: uuid.UUID | None
integration_environment: str | None
purpose: str | None
credential_prefix: str | None
credential_created_at: datetime | None
credential_expires_at: datetime | None
credential_revoked_at: datetime | None
last_used_at: datetime | None
created_at: datetime
class ServiceClientCreated(ServiceClientResponse):
credential: str
class ProjectFitEvidenceCreate(ServingModel):
project_key: str = Field(pattern=r"^[a-z][a-z0-9-]*$")
capability: str = Field(pattern=r"^[a-z][a-z0-9.]*@[1-9][0-9]*$")
environment: Literal["production", "shadow", "evaluation", "lab"]
recommendation: Literal[
"PROMOTION_ELIGIBLE", "KEEP_LAB", "BLOCKED", "REQUIRES_MORE_EVIDENCE"
]
evidence_class: Literal[
"OWNER_PHOTO", "PUBLIC_PHYSICAL_CAPTURE", "CATALOG_REFERENCE"
]
engineering_integration: Literal["PASS", "INCOMPLETE", "BLOCKED"]
production_validation: Literal[
"DEFERRED_EXTERNAL_VALIDATION", "REQUIRED", "SATISFIED"
]
production_action: Literal["NONE"] = "NONE"
case_count: int = Field(ge=0, le=1_000_000)
metric_values: dict[str, float | int | str | bool | None]
critical_errors: int = Field(ge=0)
blockers: list[str] = Field(default_factory=list, max_length=100)
evidence: dict[str, Any] = Field(default_factory=dict)
capability_deployment_id: uuid.UUID | None = None
class ProjectFitEvidenceResponse(ServingModel):
id: uuid.UUID
project_key: str
capability: str
environment: str
recommendation: str
evidence_class: str
engineering_integration: str
production_validation: str
production_action: str
case_count: int
metric_values: dict[str, Any]
critical_errors: int
blockers: list[str]
evidence_digest: str
evidence: dict[str, Any]
capability_deployment_id: uuid.UUID | None
created_at: datetime
class ProjectIntegrationUsage(ServingModel):
request_volume: int
successful_requests: int
error_count: int
last_used_at: datetime | None
latency_p50_ms: float | None
latency_p95_ms: float | None
class ProjectIntegrationResponse(ServingModel):
project_key: str
project_name: str
capability: str
environment: str
state: str
purpose: str
client_id: uuid.UUID
client_name: str
deployment_id: uuid.UUID | None
resource_impact_bytes: int | None
usage: ProjectIntegrationUsage
project_fit: ProjectFitEvidenceResponse | None
class EmbeddingInvokeRequest(ServingModel):
input: str | list[str]
input_type: Literal["raw", "query", "document"] = "raw"
@model_validator(mode="after")
def validate_input(self) -> EmbeddingInvokeRequest:
values = [self.input] if isinstance(self.input, str) else self.input
if not values or any(not item.strip() for item in values):
raise ValueError("input must contain at least one non-empty string")
return self
def inputs(self) -> list[str]:
return [self.input] if isinstance(self.input, str) else self.input
class CapabilityExperimentCreate(ServingModel):
route_key: str = Field(min_length=3, max_length=128, pattern=r"^[a-z][a-z0-9-]+$")
execution_approval_id: uuid.UUID
capability: Literal[
"rag.embedding",
"rag.reranking",
"document.ocr",
"vision.embedding",
"speech.transcription",
] = "rag.embedding"
purpose: str = Field(min_length=16, max_length=2000)
expected_dimension: int = Field(default=1024, ge=32, le=8192)
residency_policy: Literal["keep_warm", "load_on_demand"] = "keep_warm"
keep_warm_seconds: int = Field(default=300, ge=5, le=86400)
max_queue_depth: int = Field(default=16, ge=1, le=1024)
class CapabilityExperimentResponse(ServingModel):
id: uuid.UUID
route_key: str
capability: str
contract_version: int
capability_deployment_id: uuid.UUID
status: str
purpose: str
evidence: dict[str, Any]
deployment: CapabilityDeploymentResponse
created_at: datetime
class GatewayTiming(ServingModel):
validation_ms: float = 0.0
resolution_ms: float = 0.0
scheduling_ms: float = 0.0
payload_ms: float = 0.0
dispatch_ms: float = 0.0
worker_preprocess_ms: float = 0.0
worker_serialize_ms: float = 0.0
worker_total_ms: float = 0.0
completion_transport_ms: float = 0.0
result_ms: float = 0.0
gateway_ms: float
# Backwards-compatible alias for the scheduler/lease wait used by M5/M6.
queue_ms: float
load_ms: float
inference_ms: float
total_ms: float
class GatewayExecution(ServingModel):
cold: bool
node: str
residency: str
load_count: int
timings: GatewayTiming
class GatewayUsage(ServingModel):
input_count: int
input_tokens: int
class EmbeddingInvokeResponse(ServingModel):
capability: Literal["rag.embedding@1"] = "rag.embedding@1"
dimension: int = Field(ge=1, le=65_536)
normalized: Literal[True] = True
embedding_space_id: uuid.UUID
data: list[list[float]]
request_id: uuid.UUID
execution: GatewayExecution
usage: GatewayUsage
class StableEmbeddingInvokeResponse(EmbeddingInvokeResponse):
"""Public stable contract; lab experiment routes may use another dimension."""
dimension: Literal[1024] = 1024
class RerankDocument(ServingModel):
id: str = Field(min_length=1, max_length=255)
text: str = Field(min_length=1, max_length=16_384)
class RerankingInvokeRequest(ServingModel):
query: str = Field(min_length=1, max_length=4_000)
documents: list[RerankDocument] = Field(min_length=1, max_length=40)
top_n: int = Field(default=10, ge=1, le=10)
@model_validator(mode="after")
def validate_reranking_input(self) -> RerankingInvokeRequest:
if not self.query.strip() or any(not document.text.strip() for document in self.documents):
raise ValueError("query and document text must be non-empty")
if len({document.id for document in self.documents}) != len(self.documents):
raise ValueError("document ids must be unique")
if self.top_n > len(self.documents):
raise ValueError("top_n cannot exceed the document count")
if len(self.query) + sum(len(document.text) for document in self.documents) > 262_144:
raise ValueError("rerank request is too large")
return self
class RerankingResult(ServingModel):
id: str
score: float
rank: int = Field(ge=1, le=10)
@field_validator("score")
@classmethod
def finite_score(cls, value: float) -> float:
if not math.isfinite(value):
raise ValueError("reranking score must be finite")
return value
class RerankingInvokeResponse(ServingModel):
capability: Literal["rag.reranking@1"] = "rag.reranking@1"
results: list[RerankingResult]
request_id: uuid.UUID
execution: GatewayExecution
usage: GatewayUsage
def _bounded_base64(value: str, maximum_bytes: int) -> str:
try:
decoded = base64.b64decode(value, validate=True)
except (binascii.Error, ValueError) as exc:
raise ValueError("content must be canonical base64") from exc
if not decoded or len(decoded) > maximum_bytes:
raise ValueError(f"decoded content must be between 1 and {maximum_bytes} bytes")
return value
class OCRInvokeRequest(ServingModel):
content_base64: str
media_type: Literal["image/png", "image/jpeg"]
language_hint: Literal["nl", "en", "auto"] = "auto"
@field_validator("content_base64")
@classmethod
def bounded_content(cls, value: str) -> str:
return _bounded_base64(value, 8 * 1024 * 1024)
class OCRBlock(ServingModel):
text: str
order: int = Field(ge=0)
bbox: list[int] | None = Field(default=None, min_length=4, max_length=4)
confidence: float | None = Field(default=None, ge=0, le=1)
class OCRPage(ServingModel):
page: Literal[1] = 1
width: int = Field(ge=1, le=4096)
height: int = Field(ge=1, le=4096)
blocks: list[OCRBlock]
class OCRInvokeResponse(ServingModel):
capability: Literal["document.ocr@1"] = "document.ocr@1"
text: str
pages: list[OCRPage] = Field(min_length=1, max_length=1)
confidence: float | None = Field(default=None, ge=0, le=1)
request_id: uuid.UUID
execution: GatewayExecution
class VisionEmbeddingItem(ServingModel):
image_base64: str | None = None
media_type: Literal["image/png", "image/jpeg"] | None = None
text: str | None = Field(default=None, min_length=1, max_length=4096)
@model_validator(mode="after")
def exactly_one_modality(self) -> VisionEmbeddingItem:
image = self.image_base64 is not None
text = self.text is not None
if image == text or image != (self.media_type is not None):
raise ValueError("each item must contain exactly one typed image or text")
if self.image_base64 is not None:
_bounded_base64(self.image_base64, 8 * 1024 * 1024)
return self
class VisionEmbeddingInvokeRequest(ServingModel):
items: list[VisionEmbeddingItem] = Field(min_length=1, max_length=4)
@model_validator(mode="after")
def bounded_total(self) -> VisionEmbeddingInvokeRequest:
encoded = sum(len(item.image_base64 or "") for item in self.items)
if encoded > 11_184_812:
raise ValueError("combined image payload exceeds 8 MiB")
return self
class VisionEmbeddingInvokeResponse(ServingModel):
capability: Literal["vision.embedding@1"] = "vision.embedding@1"
dimension: int = Field(ge=32, le=8192)
normalized: Literal[True] = True
embedding_space_id: uuid.UUID
data: list[list[float]] = Field(min_length=1, max_length=4)
request_id: uuid.UUID
execution: GatewayExecution
class SpeechTranscriptionInvokeRequest(ServingModel):
audio_base64: str
media_type: Literal["audio/wav"] = "audio/wav"
language: Literal["nl", "en", "fr", "auto"] = "auto"
@field_validator("audio_base64")
@classmethod
def bounded_audio(cls, value: str) -> str:
return _bounded_base64(value, 16 * 1024 * 1024)
class TranscriptionSegment(ServingModel):
start_seconds: float = Field(ge=0)
end_seconds: float = Field(ge=0)
text: str
class SpeechTranscriptionInvokeResponse(ServingModel):
capability: Literal["speech.transcription@1"] = "speech.transcription@1"
text: str
language: str
duration_seconds: float = Field(gt=0, le=120)
segments: list[TranscriptionSegment] = Field(default_factory=list)
request_id: uuid.UUID
execution: GatewayExecution
class OpenAIEmbeddingRequest(ServingModel):
model: Literal["rag.embedding", "rag.embedding@1"]
input: str | list[str]
class OpenAIEmbeddingItem(ServingModel):
object: Literal["embedding"] = "embedding"
index: int
embedding: list[float]
class OpenAIUsage(ServingModel):
prompt_tokens: int
total_tokens: int
class OpenAIEmbeddingResponse(ServingModel):
object: Literal["list"] = "list"
data: list[OpenAIEmbeddingItem]
model: Literal["rag.embedding"] = "rag.embedding"
usage: OpenAIUsage
class SchedulerBudgetResponse(ServingModel):
compute_node_id: uuid.UUID
node_name: str
accelerator_id: uuid.UUID
accelerator_uuid: str
accelerator_name: str
total_vram_bytes: int
observed_used_vram_bytes: int
external_vram_bytes: int
resident_vram_bytes: int
leased_vram_bytes: int
safety_reserve_bytes: int
schedulable_free_vram_bytes: int
pressure: bool
pressure_state: Literal["NORMAL", "ELEVATED", "HIGH", "CRITICAL"] = "NORMAL"
attribution_confidence: Literal["KNOWN", "ESTIMATED", "UNKNOWN"] = "UNKNOWN"
invariant_delta_bytes: int = 0
policy_revision: str = "m10-v1"
observed_at: datetime | None
class PlacementPlanRequest(ServingModel):
priority: Literal["production", "interactive", "background", "lab"] = "interactive"
deadline_remaining_ms: float | None = Field(default=None, gt=0, le=900_000)
class PlacementEvictionResponse(ServingModel):
deployment_id: uuid.UUID
capability: str
expected_reclaimed_bytes: int
reason: str
class PlacementPlanResponse(ServingModel):
id: uuid.UUID | None = None
deployment_id: uuid.UUID
capability: str
node_id: uuid.UUID
accelerator_id: uuid.UUID
policy_revision: str
verdict: Literal[
"ADMIT",
"ADMIT_AFTER_EVICTION",
"QUEUE",
"REJECT_CAPACITY",
"REJECT_HEALTH",
"REJECT_POLICY",
]
reason_codes: list[str]
required_vram_bytes: int
headroom_before_bytes: int
headroom_after_bytes: int
expected_cold_load_ms: float
evictions: list[PlacementEvictionResponse] = Field(default_factory=list)
decision_fingerprint: str
dry_run: bool
created_at: datetime
class SchedulerPolicyResponse(ServingModel):
revision: str
active: bool
configuration: dict[str, Any]
created_at: datetime
class SchedulerMetricsResponse(ServingModel):
counters: dict[str, int]
gauges: dict[str, int | str]
queue_seconds: dict[str, float]
class SchedulerPolicyUpdate(ServingModel):
lab_paused: bool
class CoResidencyEvidenceResponse(ServingModel):
left_deployment_id: uuid.UUID
left_capability: str
right_deployment_id: uuid.UUID
right_capability: str
left_alone_bytes: int
right_alone_bytes: int
expected_combined_bytes: int
measured_combined_bytes: int | None
status: Literal["PROVEN_SAFE", "EXPECTED_SAFE", "NOT_SAFE", "UNKNOWN"]
evidence: dict[str, Any] = Field(default_factory=dict)
measured_at: datetime | None
class CoResidencyEvidenceCreate(ServingModel):
left_deployment_id: uuid.UUID
right_deployment_id: uuid.UUID
@model_validator(mode="after")
def distinct_deployments(self) -> CoResidencyEvidenceCreate:
if self.left_deployment_id == self.right_deployment_id:
raise ValueError("co-residency evidence requires two distinct deployments")
return self
class ResidencyPolicyUpdate(ServingModel):
residency_policy: Literal["always_warm", "keep_warm", "load_on_demand", "lab_only"]
keep_warm_seconds: int = Field(default=900, ge=5, le=86400)
class GatewayRequestResponse(ServingModel):
request_id: uuid.UUID
capability: str
client: str | None
status: str
cold: bool | None
queue_time_ms: float | None
load_time_ms: float | None
inference_time_ms: float | None
total_latency_ms: float | None
failure_code: str | None
latency_breakdown: dict[str, float] = Field(default_factory=dict)
created_at: datetime
class AgentServingJobLease(ServingModel):
job_id: uuid.UUID
lease_token: str = Field(min_length=32, max_length=512)
lease_expires_at: datetime
operation: ServingOperation
deployment_id: uuid.UUID
artifact_set_id: uuid.UUID
revision_sha: str = Field(pattern=r"^[a-f0-9]{40,64}$")
artifact_root: str
artifact_relative_path: str
expected_manifest: dict[str, Any]
runtime_profile: dict[str, Any]
runtime_environment: dict[str, Any]
capability: Literal[
"rag.embedding",
"rag.reranking",
"document.ocr",
"vision.embedding",
"speech.transcription",
] = "rag.embedding"
embedding_space_id: uuid.UUID | None = None
expected_dimension: int | None = Field(default=None, ge=32, le=8192)
normalize: Literal[True] | None = True
input: list[str] | None = None
input_type: Literal["raw", "query", "document"] = "raw"
rerank_query: str | None = None
rerank_documents: list[RerankDocument] | None = None
top_n: int | None = Field(default=None, ge=1, le=10)
modality_payload: dict[str, Any] | None = None
request_id: uuid.UUID | None = None
@model_validator(mode="after")
def validate_capability_payload(self) -> AgentServingJobLease:
if self.operation != "invoke":
return self
if self.capability == "rag.embedding":
if self.embedding_space_id is None or self.expected_dimension is None:
raise ValueError("embedding invoke requires embedding identity")
if self.input is None or self.rerank_query is not None:
raise ValueError("embedding invoke payload is invalid")
elif self.capability == "rag.reranking" and (
self.rerank_query is None
or self.rerank_documents is None
or self.top_n is None
or self.input is not None
):
raise ValueError("reranking invoke payload is invalid")
elif self.capability not in {"rag.embedding", "rag.reranking"} and (
self.modality_payload is None or self.input is not None or self.rerank_query is not None
):
raise ValueError("modality invoke requires a typed modality payload")
return self
class AgentServingJobComplete(ServingModel):
lease_token: str = Field(min_length=32, max_length=512)
worker_instance_id: str = Field(min_length=1, max_length=255)
state: str
result: dict[str, Any] = Field(default_factory=dict)
metrics: dict[str, Any] = Field(default_factory=dict)
health: dict[str, Any] = Field(default_factory=dict)
runtime_facts: dict[str, Any] = Field(default_factory=dict)
class AgentServingJobFailure(ServingModel):
lease_token: str = Field(min_length=32, max_length=512)
worker_instance_id: str = Field(min_length=1, max_length=255)
failure_code: str = Field(min_length=1, max_length=64)
failure_message: str = Field(min_length=1, max_length=2000)
state: str = "failed"
details: dict[str, Any] = Field(default_factory=dict)
class AgentResidentState(ServingModel):
deployment_id: uuid.UUID
artifact_set_id: uuid.UUID
runtime_profile_id: uuid.UUID
runtime_environment_fingerprint: str = Field(pattern=r"^[a-f0-9]{64}$")
revision_sha: str = Field(pattern=r"^[a-f0-9]{40,64}$")
state: Literal["loading", "warm", "busy", "draining", "unloading", "failed"]
load_count: int = Field(ge=1)
resident_vram_bytes: int = Field(ge=0)
health: dict[str, Any] = Field(default_factory=dict)
class AgentServingStateReport(ServingModel):
worker_instance_id: str = Field(min_length=1, max_length=255)
deployment_id: uuid.UUID | None
state: Literal["cold", "loading", "warm", "busy", "draining", "unloading", "failed"]
load_count: int = Field(ge=0)
health: dict[str, Any] = Field(default_factory=dict)
observed_at: datetime
generation: int = Field(default=1, ge=1)
residencies: list[AgentResidentState] = Field(default_factory=list, max_length=32)
class AgentServingStateAck(ServingModel):
accepted: bool = True
desired_operation: Literal["none", "unload"] = "none"
@@ -0,0 +1,3 @@
from .collectors import NvidiaNvmlCollector, SystemHostCollector, build_nvml_collector
__all__ = ["NvidiaNvmlCollector", "SystemHostCollector", "build_nvml_collector"]
@@ -0,0 +1,339 @@
from __future__ import annotations
import platform
import socket
import sys
import uuid
from collections.abc import Callable
from contextlib import suppress
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, TypeVar
import psutil # type: ignore[import-untyped]
from modelforge_api import __version__
from modelforge_api.domain.enums import Availability
from modelforge_api.domain.hardware import (
AcceleratorInventory,
AcceleratorTelemetry,
HostInventory,
NvidiaCollection,
ObservedValue,
StorageObservation,
)
T = TypeVar("T")
class NodeIdentityProvider:
"""Stable node identity: explicit override, OS machine ID, then persisted UUID."""
def __init__(
self,
identity_file: Path,
explicit_identity: str | None = None,
force_persisted: bool = False,
) -> None:
self.identity_file = identity_file
self.explicit_identity = explicit_identity
self.force_persisted = force_persisted
def resolve(self) -> tuple[str, str]:
if self.explicit_identity:
return self.explicit_identity, "configured"
if not self.force_persisted:
system_id = self._system_machine_id()
if system_id:
return str(uuid.uuid5(uuid.NAMESPACE_OID, system_id)), "os_machine_id"
try:
if self.identity_file.exists():
return self.identity_file.read_text(encoding="utf-8").strip(), "persisted_uuid"
value = str(uuid.uuid4())
self.identity_file.parent.mkdir(parents=True, exist_ok=True)
self.identity_file.write_text(value, encoding="utf-8")
return value, "persisted_uuid"
except OSError as exc:
raise RuntimeError("unable to establish stable node identity") from exc
@staticmethod
def _system_machine_id() -> str | None:
# `sys.platform`, not `os.name`: a type checker narrows on the former and not on the latter,
# so under `os.name` this block was analysed on Linux too — where winreg has no attributes.
# The images run on Linux, so the platform the release actually ships on was the one the
# type check could not see. They mean the same thing at runtime.
if sys.platform == "win32":
try:
import winreg
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
r"SOFTWARE\Microsoft\Cryptography",
) as key:
return str(winreg.QueryValueEx(key, "MachineGuid")[0])
except (OSError, ImportError):
return None
for path in (Path("/etc/machine-id"), Path("/var/lib/dbus/machine-id")):
try:
value = path.read_text(encoding="utf-8").strip()
if value:
return value
except OSError:
continue
return None
class SystemHostCollector:
def __init__(
self,
identity_provider: NodeIdentityProvider,
storage_paths: dict[str, Path],
) -> None:
self.identity_provider = identity_provider
self.storage_paths = storage_paths
@staticmethod
def _known_or_unknown(value: T | None, reason: str) -> ObservedValue[T]:
return (
ObservedValue.known(value)
if value is not None
else ObservedValue.absent(Availability.UNKNOWN, reason)
)
def collect(self) -> HostInventory:
identity_key, identity_source = self.identity_provider.resolve()
memory = psutil.virtual_memory()
cpu_model = platform.processor().strip() or None
physical = psutil.cpu_count(logical=False)
logical = psutil.cpu_count(logical=True)
storage: list[StorageObservation] = []
for purpose, path in self.storage_paths.items():
try:
usage = psutil.disk_usage(str(path))
storage.append(
StorageObservation(
purpose=purpose,
path=str(path.resolve()),
total_bytes=ObservedValue.known(usage.total),
used_bytes=ObservedValue.known(usage.used),
free_bytes=ObservedValue.known(usage.free),
)
)
except OSError as exc:
reason = f"{type(exc).__name__}: path unavailable"
unavailable = ObservedValue[int].absent(Availability.UNAVAILABLE, reason)
storage.append(
StorageObservation(
purpose=purpose,
path=str(path),
total_bytes=unavailable,
used_bytes=unavailable,
free_bytes=unavailable,
)
)
release = platform.release()
environment = "wsl2" if "microsoft" in release.lower() else "native"
return HostInventory(
identity_key=identity_key,
identity_source=identity_source,
hostname=socket.gethostname(),
display_name=socket.gethostname(),
os_name=platform.system() or "unknown",
os_version=self._known_or_unknown(platform.version() or None, "OS version unavailable"),
architecture=platform.machine() or "unknown",
kernel_version=self._known_or_unknown(release or None, "kernel unavailable"),
cpu_model=self._known_or_unknown(cpu_model, "CPU model unavailable"),
logical_cpu_count=self._known_or_unknown(logical, "logical CPU count unavailable"),
physical_core_count=self._known_or_unknown(physical, "physical core count unavailable"),
total_ram_bytes=ObservedValue.known(memory.total),
available_ram_bytes=ObservedValue.known(memory.available),
agent_version=__version__,
storage=storage,
metadata={"environment": environment},
)
class NvidiaNvmlCollector:
def __init__(self, api: Any) -> None:
self.api = api
self.nvml_error = getattr(api, "NVMLError", Exception)
self.not_supported = getattr(api, "NVMLError_NotSupported", ())
def _optional(
self, call: Callable[[], T], transform: Callable[[T], Any] | None = None
) -> ObservedValue[Any]:
try:
value = call()
return ObservedValue.known(transform(value) if transform else value)
except self.not_supported:
return ObservedValue.absent(Availability.UNSUPPORTED, "NVML metric not supported")
except self.nvml_error as exc:
return ObservedValue.absent(Availability.TEMPORARILY_FAILED, type(exc).__name__)
@staticmethod
def _text(value: Any) -> str:
return value.decode("utf-8", errors="replace") if isinstance(value, bytes) else str(value)
@staticmethod
def _cuda_version(value: int) -> str:
return f"{value // 1000}.{(value % 1000) // 10}"
def _architecture(self, handle: Any) -> ObservedValue[str]:
def transform(value: Any) -> str:
for name in dir(self.api):
if name.startswith("NVML_DEVICE_ARCH_") and getattr(self.api, name) == value:
return name.removeprefix("NVML_DEVICE_ARCH_").lower()
return f"nvml_arch_{value}"
function = getattr(self.api, "nvmlDeviceGetArchitecture", None)
if function is None:
return ObservedValue.absent(Availability.UNSUPPORTED, "architecture API unavailable")
return self._optional(lambda: function(handle), transform)
def collect(self) -> NvidiaCollection:
initialized = False
try:
self.api.nvmlInit()
initialized = True
count = self.api.nvmlDeviceGetCount()
driver = self._optional(self.api.nvmlSystemGetDriverVersion, self._text)
cuda_function = getattr(self.api, "nvmlSystemGetCudaDriverVersion_v2", None) or getattr(
self.api, "nvmlSystemGetCudaDriverVersion", None
)
cuda_driver = (
self._optional(cuda_function, self._cuda_version)
if cuda_function
else ObservedValue.absent(Availability.UNSUPPORTED, "CUDA driver API unavailable")
)
inventory: list[AcceleratorInventory] = []
telemetry: list[AcceleratorTelemetry] = []
failures: list[str] = []
observed_at = datetime.now(UTC)
for index in range(count):
try:
handle = self.api.nvmlDeviceGetHandleByIndex(index)
device_uuid = self._text(self.api.nvmlDeviceGetUUID(handle))
name = self._text(self.api.nvmlDeviceGetName(handle))
memory = self.api.nvmlDeviceGetMemoryInfo(handle)
pci = self._optional(
lambda: self.api.nvmlDeviceGetPciInfo(handle),
lambda value: self._text(value.busId),
)
compute = self._optional(
lambda: self.api.nvmlDeviceGetCudaComputeCapability(handle)
)
major = (
ObservedValue.known(int(compute.value[0]))
if compute.availability is Availability.KNOWN and compute.value is not None
else ObservedValue.absent(compute.availability, compute.reason)
)
minor = (
ObservedValue.known(int(compute.value[1]))
if compute.availability is Availability.KNOWN and compute.value is not None
else ObservedValue.absent(compute.availability, compute.reason)
)
mig_function = getattr(self.api, "nvmlDeviceGetMigMode", None)
mig = (
self._optional(lambda: mig_function(handle), lambda value: bool(value[0]))
if mig_function
else ObservedValue.absent(Availability.UNSUPPORTED, "MIG API unavailable")
)
inventory.append(
AcceleratorInventory(
device_index=index,
device_uuid=device_uuid,
pci_bus_id=pci,
name=name,
architecture=self._architecture(handle),
compute_capability_major=major,
compute_capability_minor=minor,
total_vram_bytes=ObservedValue.known(int(memory.total)),
driver_version=driver,
cuda_driver_version=cuda_driver,
mig_mode_current=mig,
inventory_at=observed_at,
)
)
utilization = self._optional(
lambda: self.api.nvmlDeviceGetUtilizationRates(handle)
)
gpu_util = (
ObservedValue.known(int(utilization.value.gpu))
if utilization.availability is Availability.KNOWN
and utilization.value is not None
else ObservedValue.absent(utilization.availability, utilization.reason)
)
mem_util = (
ObservedValue.known(int(utilization.value.memory))
if utilization.availability is Availability.KNOWN
and utilization.value is not None
else ObservedValue.absent(utilization.availability, utilization.reason)
)
telemetry.append(
AcceleratorTelemetry(
device_uuid=device_uuid,
observed_at=observed_at,
used_vram_bytes=ObservedValue.known(int(memory.used)),
free_vram_bytes=ObservedValue.known(int(memory.free)),
gpu_utilization_percent=gpu_util,
memory_utilization_percent=mem_util,
temperature_c=self._optional(
lambda: self.api.nvmlDeviceGetTemperature(
handle, self.api.NVML_TEMPERATURE_GPU
),
int,
),
power_draw_w=self._optional(
lambda: self.api.nvmlDeviceGetPowerUsage(handle),
lambda value: round(value / 1000, 3),
),
power_limit_w=self._optional(
lambda: self.api.nvmlDeviceGetEnforcedPowerLimit(handle),
lambda value: round(value / 1000, 3),
),
graphics_clock_mhz=self._optional(
lambda: self.api.nvmlDeviceGetClockInfo(
handle, self.api.NVML_CLOCK_GRAPHICS
),
int,
),
memory_clock_mhz=self._optional(
lambda: self.api.nvmlDeviceGetClockInfo(
handle, self.api.NVML_CLOCK_MEM
),
int,
),
fan_speed_percent=self._optional(
lambda: self.api.nvmlDeviceGetFanSpeed(handle), int
),
performance_state=self._optional(
lambda: self.api.nvmlDeviceGetPerformanceState(handle),
lambda value: f"P{value}",
),
)
)
except self.nvml_error as exc:
failures.append(f"device {index}: {type(exc).__name__}")
availability = Availability.KNOWN if not failures else Availability.TEMPORARILY_FAILED
return NvidiaCollection(
availability=availability,
reason="; ".join(failures)
or ("no NVIDIA devices detected" if count == 0 else None),
inventory=inventory,
telemetry=telemetry,
observed_at=observed_at,
)
except self.nvml_error as exc:
return NvidiaCollection(
availability=Availability.UNAVAILABLE, reason=type(exc).__name__
)
finally:
if initialized:
with suppress(self.nvml_error):
self.api.nvmlShutdown()
def build_nvml_collector() -> NvidiaNvmlCollector:
import pynvml # type: ignore[import-untyped]
return NvidiaNvmlCollector(pynvml)
+660
View File
@@ -0,0 +1,660 @@
import asyncio
import math
import time
import uuid
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
from contextlib import asynccontextmanager, contextmanager, suppress
from typing import Any, cast
import structlog
from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.concurrency import run_in_threadpool
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from modelforge_api import __version__
from modelforge_api.api.authorization import (
AccessBoundary,
access_boundary_for_request,
authenticate_operator_token,
request_body_limit_bytes,
required_capability_for_request,
)
from modelforge_api.api.request_limits import RequestBodyLimitMiddleware
from modelforge_api.api.routes import (
acquisition,
agent,
catalog,
evaluation,
hardware,
health,
lifecycle,
migrations,
observability,
recovery,
registry,
runtime,
serving,
)
from modelforge_api.db import engine, get_session
from modelforge_api.domain.observability import metrics, route_class
from modelforge_api.domain.release import build_identity
from modelforge_api.services.evaluation import EvaluationError
from modelforge_api.services.hardware_polling import HardwarePollingService
from modelforge_api.services.lifecycle import LifecycleError, LifecycleService
from modelforge_api.services.manifest_registry import ManifestRegistry
from modelforge_api.services.migration_engine import (
MigrationEngineError,
MigrationEngineService,
)
from modelforge_api.services.node_agent import (
AgentProtocolError,
NodeAgentService,
NodeAuthenticationEvidence,
)
from modelforge_api.services.node_decommission import NodeDecommissionError
from modelforge_api.services.node_liveness import NodeLivenessPollingService
from modelforge_api.services.observability import ObservabilityError, ObservabilityService
from modelforge_api.services.observability_polling import ObservabilityPollingService
from modelforge_api.services.project_registry import sync_project_registry
from modelforge_api.services.recovery import RecoveryError, RecoveryService
from modelforge_api.services.registry import RegistryError, seed_candidate_registry
from modelforge_api.services.serving import (
CapabilityAuthenticationEvidence,
ServingError,
ServingService,
)
from modelforge_api.services.serving_reconciliation import ServingReconciliationService
from modelforge_api.services.startup_validation import enforce_startup
from modelforge_api.settings import Settings, get_settings
settings = get_settings()
structlog.configure(processors=[structlog.processors.JSONRenderer()])
logger = structlog.get_logger()
def interactive_api_enabled_for(environment: str) -> bool:
"""Keep schema explorers off deployed/test surfaces unless development is explicit."""
return environment == "development"
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
# Configuration and schema compatibility are checked before anything else runs. In production a
# problem here stops the process; elsewhere it is logged, because finding out at startup rather
# than at first use is the point either way.
identity = build_identity(
source_commit=settings.build_commit,
built_at=settings.build_timestamp,
image_digest=settings.build_image_digest,
)
logger.info("modelforge_starting", **identity.as_dict(), environment=settings.env)
validation = enforce_startup(settings, engine)
for problem in validation.problems:
logger.error(
"startup_configuration_problem",
code=str(problem.code),
setting=problem.setting,
detail=problem.message,
)
for problem in validation.warnings:
logger.warning(
"startup_configuration_warning",
code=str(problem.code),
setting=problem.setting,
detail=problem.message,
)
if settings.lifecycle_reconciliation_enabled:
with Session(engine) as session:
lifecycle_service = LifecycleService(session)
lifecycle_service.ensure_defaults()
reconciled = lifecycle_service.reconcile_incomplete()
if reconciled:
logger.warning("lifecycle_operations_reconciled", count=reconciled)
if settings.migration_reconciliation_enabled:
with Session(engine) as session:
migration_service = MigrationEngineService(session)
migration_service.ensure_defaults()
pending_migrations = migration_service.pending_reconciliation_count()
if pending_migrations:
# External state is deliberately never guessed during startup. An operator or
# adapter must report observed truth through the reconciliation endpoint.
logger.warning(
"migration_cutovers_require_reconciliation", count=pending_migrations
)
if settings.recovery_reconciliation_enabled:
with Session(engine) as session:
recovery_service = RecoveryService(session, settings, "control_plane", "startup")
recovery_service.ensure_defaults()
interrupted_backups = recovery_service.reconcile_interrupted_backups()
interrupted_restores = recovery_service.reconcile_interrupted_restores()
if interrupted_backups or interrupted_restores:
# An interrupted backup is never restore eligible and an interrupted restore is
# never silently resumed; both surface for explicit operator review.
logger.warning(
"recovery_operations_reconciled",
interrupted_backups=interrupted_backups,
interrupted_restores=interrupted_restores,
)
if settings.registry_seed_on_startup:
with Session(engine) as session:
manifests = ManifestRegistry(settings.config_root)
seed_candidate_registry(session, manifests)
sync_result = sync_project_registry(session, manifests)
if sync_result.unavailable_contracts:
logger.warning(
"project_bindings_waiting_for_contracts",
bindings=sync_result.unavailable_contracts,
)
poller = HardwarePollingService(settings)
hardware_task = (
asyncio.create_task(poller.run()) if settings.hardware_refresh_on_startup else None
)
liveness = NodeLivenessPollingService(settings)
liveness_task = (
asyncio.create_task(liveness.run()) if settings.node_liveness_monitor_enabled else None
)
serving_reconciliation = ServingReconciliationService(settings, engine)
serving_task = (
asyncio.create_task(serving_reconciliation.run())
if settings.serving_reconciliation_enabled
else None
)
observability_poller = ObservabilityPollingService(settings, engine)
if settings.observability_monitor_enabled:
with Session(engine) as session:
ObservabilityService(session, settings).ensure_defaults()
observability_task = (
asyncio.create_task(observability_poller.run())
if settings.observability_monitor_enabled
else None
)
yield
if observability_task:
observability_poller.stop()
await observability_task
if serving_task:
serving_reconciliation.stop()
await serving_task
if liveness_task:
liveness.stop()
await liveness_task
if hardware_task:
poller.stop()
await hardware_task
interactive_api_enabled = interactive_api_enabled_for(settings.env)
app = FastAPI(
title="ITWorx ModelForge API",
version=__version__,
description="Local AI ModelOps and GPU control-plane API",
lifespan=lifespan,
docs_url="/docs" if interactive_api_enabled else None,
redoc_url="/redoc" if interactive_api_enabled else None,
openapi_url="/openapi.json" if interactive_api_enabled else None,
)
app.include_router(health.router)
app.include_router(lifecycle.router)
app.include_router(migrations.router)
app.include_router(observability.router)
app.include_router(recovery.router)
app.include_router(agent.router)
app.include_router(hardware.router)
app.include_router(catalog.router)
app.include_router(registry.router)
app.include_router(acquisition.router)
app.include_router(runtime.router)
app.include_router(serving.router)
app.include_router(evaluation.router)
@app.exception_handler(LifecycleError)
async def lifecycle_error(request: Request, exc: LifecycleError) -> JSONResponse:
correlation_id = getattr(request.state, "correlation_id", "unknown")
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"correlation_id": correlation_id,
}
},
)
@app.exception_handler(MigrationEngineError)
async def migration_engine_error(request: Request, exc: MigrationEngineError) -> JSONResponse:
correlation_id = getattr(request.state, "correlation_id", "unknown")
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"correlation_id": correlation_id,
"details": {},
}
},
)
@app.exception_handler(RecoveryError)
async def recovery_error(request: Request, exc: RecoveryError) -> JSONResponse:
correlation_id = getattr(request.state, "correlation_id", "unknown")
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"correlation_id": correlation_id,
"details": exc.details,
}
},
)
@app.exception_handler(ObservabilityError)
async def observability_error(request: Request, exc: ObservabilityError) -> JSONResponse:
correlation_id = getattr(request.state, "correlation_id", "unknown")
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"correlation_id": correlation_id,
"details": {},
}
},
)
@contextmanager
def _authentication_session(application: FastAPI) -> Iterator[Session]:
"""Use a test/application session override when present, otherwise a short owned session."""
override = application.dependency_overrides.get(get_session)
if override is None:
with Session(engine) as session:
yield session
return
provided = override()
if isinstance(provided, Session):
yield provided
return
iterator = iter(cast(Any, provided))
session = next(iterator)
if not isinstance(session, Session):
raise TypeError("get_session override did not provide a SQLAlchemy Session")
try:
yield session
finally:
with suppress(StopIteration):
next(iterator)
def _authenticate_node_before_body(
application: FastAPI,
request_settings: Settings,
authorization: str | None,
) -> NodeAuthenticationEvidence:
service_override = application.dependency_overrides.get(agent.get_agent_service)
if service_override is not None:
service = service_override()
if not isinstance(service, NodeAgentService):
raise TypeError("node authentication service override has an invalid type")
_identity, evidence = service.authenticate_with_evidence(authorization)
service.session.commit()
return evidence
with _authentication_session(application) as session:
service = NodeAgentService(session, request_settings)
_identity, evidence = service.authenticate_with_evidence(authorization)
session.commit()
return evidence
def _authenticate_capability_before_body(
application: FastAPI,
request_settings: Settings,
authorization: str | None,
capability: str,
) -> CapabilityAuthenticationEvidence:
service_override = application.dependency_overrides.get(serving.get_serving_service)
if service_override is not None:
service = service_override()
if not isinstance(service, ServingService):
raise TypeError("capability authentication service override has an invalid type")
_client, evidence = service.authenticate_with_evidence(authorization, capability)
return evidence
with _authentication_session(application) as session:
service = ServingService(
session,
request_settings,
ManifestRegistry(request_settings.config_root),
None,
)
_client, evidence = service.authenticate_with_evidence(authorization, capability)
return evidence
def _authentication_error(
status_code: int,
code: str,
message: str,
correlation_id: str,
details: dict[str, Any] | None = None,
) -> JSONResponse:
return JSONResponse(
status_code=status_code,
content={
"error": {
"code": code,
"message": message,
"correlation_id": correlation_id,
"details": details or {},
}
},
)
@app.middleware("http")
async def correlation_middleware(
request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
started = time.perf_counter()
correlation_id = request.headers.get("x-correlation-id") or str(uuid.uuid4())
request.state.correlation_id = correlation_id
boundary = access_boundary_for_request(
request.method,
request.url.path,
)
settings_provider = cast(
Callable[[], Settings],
request.app.dependency_overrides.get(get_settings, get_settings),
)
request_settings = settings_provider()
request.state.request_body_limit_bytes = request_body_limit_bytes(
request_settings,
boundary,
)
response: Response | None = None
if boundary is AccessBoundary.CONTROL_PLANE:
try:
request.state.principal = authenticate_operator_token(
request_settings,
request.headers.get("x-modelforge-admin-token"),
)
except HTTPException as exc:
response = _authentication_error(
exc.status_code,
f"http_{exc.status_code}",
str(exc.detail),
correlation_id,
)
elif boundary is AccessBoundary.NODE:
try:
request.state.node_authentication = await run_in_threadpool(
_authenticate_node_before_body,
request.app,
request_settings,
request.headers.get("authorization"),
)
except AgentProtocolError as exc:
response = _authentication_error(
exc.status_code,
exc.code,
str(exc),
correlation_id,
)
except SQLAlchemyError:
response = _authentication_error(
503,
"machine_authentication_unavailable",
"node authentication backend is unavailable",
correlation_id,
)
elif boundary is AccessBoundary.CAPABILITY_CLIENT:
capability = required_capability_for_request(request.method, request.url.path)
if capability is None:
response = _authentication_error(
503,
"machine_authentication_unavailable",
"capability authentication policy is unavailable",
correlation_id,
)
else:
try:
request.state.capability_authentication = await run_in_threadpool(
_authenticate_capability_before_body,
request.app,
request_settings,
request.headers.get("authorization"),
capability,
)
except ServingError as exc:
response = _authentication_error(
exc.status_code,
exc.code,
str(exc),
correlation_id,
exc.details,
)
except SQLAlchemyError:
response = _authentication_error(
503,
"machine_authentication_unavailable",
"capability authentication backend is unavailable",
correlation_id,
)
if response is None:
response = await call_next(request)
duration_seconds = time.perf_counter() - started
response.headers["x-correlation-id"] = correlation_id
body_error_status_code = getattr(
request.state,
"request_body_error_status_code",
None,
)
effective_status_code = (
body_error_status_code
if isinstance(body_error_status_code, int)
else response.status_code
)
labels = {
"method": request.method,
"route_class": route_class(request.url.path),
}
metrics.increment(
"modelforge_api_requests_total",
{**labels, "status_class": f"{effective_status_code // 100}xx"},
)
metrics.observe("modelforge_api_request_duration_seconds", labels, duration_seconds)
logger.info(
"http_request",
correlation_id=correlation_id,
method=request.method,
path=request.url.path,
status_code=effective_status_code,
duration_ms=duration_seconds * 1000,
)
return response
# Starlette wraps user middleware in reverse registration order. Register CORS after the
# request limiter and correlation/authentication middleware so it remains outermost and decorates
# pre-body errors as well as responses produced by routing and exception handlers. The pure ASGI
# limiter stays outside BaseHTTPMiddleware so empty request frames cannot be normalized away before
# its progress counters observe them; authentication still runs before the first wrapped receive.
app.add_middleware(RequestBodyLimitMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origin_list,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
MAX_ECHOED_INPUT_CHARACTERS = 200
def _renderable(value: object) -> object:
"""Render a rejected value so the error response can always be serialised.
A body may legally contain values JSON cannot round-trip — `NaN` and `Infinity` are accepted by
Python's parser but rejected by the serialiser — and echoing one raw made the error handler
itself fail, turning a 422 into a server error. Rejected input is also unbounded by definition,
so it is truncated rather than mirrored back in full.
"""
if isinstance(value, float) and not math.isfinite(value):
return repr(value)
if isinstance(value, str):
return value[:MAX_ECHOED_INPUT_CHARACTERS]
if isinstance(value, bytes):
return value[:MAX_ECHOED_INPUT_CHARACTERS].decode("utf-8", "replace")
if isinstance(value, list | tuple):
return [_renderable(item) for item in value[:20]]
if isinstance(value, dict):
return {str(key): _renderable(item) for key, item in list(value.items())[:20]}
if isinstance(value, bool | int | float | type(None)):
return value
return repr(value)[:MAX_ECHOED_INPUT_CHARACTERS]
@app.exception_handler(RequestValidationError)
async def validation_error(request: Request, exc: RequestValidationError) -> JSONResponse:
correlation_id = getattr(request.state, "correlation_id", "unknown")
return JSONResponse(
status_code=422,
content={
"error": {
"code": "request_validation_failed",
"message": "Request validation failed",
"correlation_id": correlation_id,
"details": {
"errors": [
{key: _renderable(value) for key, value in error.items() if key != "ctx"}
for error in exc.errors()
]
},
}
},
)
@app.exception_handler(HTTPException)
async def http_error(request: Request, exc: HTTPException) -> JSONResponse:
correlation_id = getattr(request.state, "correlation_id", "unknown")
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": f"http_{exc.status_code}",
"message": str(exc.detail),
"correlation_id": correlation_id,
"details": {},
}
},
)
@app.exception_handler(AgentProtocolError)
async def agent_protocol_error(request: Request, exc: AgentProtocolError) -> JSONResponse:
correlation_id = getattr(request.state, "correlation_id", "unknown")
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": str(exc),
"correlation_id": correlation_id,
"details": {},
}
},
)
@app.exception_handler(NodeDecommissionError)
async def node_decommission_error(request: Request, exc: NodeDecommissionError) -> JSONResponse:
correlation_id = getattr(request.state, "correlation_id", "unknown")
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"correlation_id": correlation_id,
"details": exc.details,
}
},
)
@app.exception_handler(RegistryError)
async def registry_error(request: Request, exc: RegistryError) -> JSONResponse:
correlation_id = getattr(request.state, "correlation_id", "unknown")
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": str(exc),
"correlation_id": correlation_id,
"details": exc.details,
}
},
)
@app.exception_handler(ServingError)
async def serving_error(request: Request, exc: ServingError) -> JSONResponse:
correlation_id = getattr(request.state, "correlation_id", "unknown")
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": str(exc),
"correlation_id": correlation_id,
"details": exc.details,
}
},
)
@app.exception_handler(EvaluationError)
async def evaluation_error(request: Request, exc: EvaluationError) -> JSONResponse:
correlation_id = getattr(request.state, "correlation_id", "unknown")
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": str(exc),
"correlation_id": correlation_id,
"details": exc.details,
}
},
)
@app.get("/")
def root() -> dict[str, str]:
return {
"name": "ITWorx ModelForge",
"version": __version__,
"docs": "/docs" if interactive_api_enabled else "disabled",
"api": "/api/v1",
}
@@ -0,0 +1,3 @@
from .models import Base
__all__ = ["Base"]
@@ -0,0 +1,123 @@
from __future__ import annotations
import uuid
from datetime import datetime
from typing import TypeVar
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from modelforge_api.persistence.models import (
ArtifactInspection,
ArtifactJob,
ArtifactSet,
DownloadPlan,
DownloadPlanFile,
UpstreamFile,
UpstreamSnapshot,
)
T = TypeVar("T")
class AcquisitionRepository:
def __init__(self, session: Session) -> None:
self.session = session
def add(self, entity: T) -> T:
self.session.add(entity)
self.session.flush()
return entity
def latest_snapshot(self, model_id: uuid.UUID) -> UpstreamSnapshot | None:
return self.session.scalar(
select(UpstreamSnapshot)
.where(UpstreamSnapshot.model_id == model_id)
.order_by(UpstreamSnapshot.observed_at.desc(), UpstreamSnapshot.id.desc())
.limit(1)
)
def snapshot(self, snapshot_id: uuid.UUID) -> UpstreamSnapshot | None:
return self.session.get(UpstreamSnapshot, snapshot_id)
def files(self, snapshot_id: uuid.UUID) -> list[UpstreamFile]:
return list(
self.session.scalars(
select(UpstreamFile)
.where(UpstreamFile.snapshot_id == snapshot_id)
.order_by(UpstreamFile.path)
)
)
def artifact_set(self, artifact_set_id: uuid.UUID) -> ArtifactSet | None:
return self.session.get(ArtifactSet, artifact_set_id)
def artifact_set_for_variant(self, revision_id: uuid.UUID, variant: str) -> ArtifactSet | None:
return self.session.scalar(
select(ArtifactSet).where(
ArtifactSet.revision_id == revision_id, ArtifactSet.variant_key == variant
)
)
def artifact_sets(self, revision_id: uuid.UUID) -> list[ArtifactSet]:
return list(
self.session.scalars(
select(ArtifactSet)
.where(ArtifactSet.revision_id == revision_id)
.order_by(ArtifactSet.variant_key)
)
)
def plan(self, plan_id: uuid.UUID) -> DownloadPlan | None:
return self.session.get(DownloadPlan, plan_id)
def plan_by_key(self, key: str) -> DownloadPlan | None:
return self.session.scalar(select(DownloadPlan).where(DownloadPlan.idempotency_key == key))
def plan_files(self, plan_id: uuid.UUID) -> list[DownloadPlanFile]:
return list(
self.session.scalars(
select(DownloadPlanFile)
.where(DownloadPlanFile.plan_id == plan_id)
.order_by(DownloadPlanFile.ordinal)
)
)
def job(self, job_id: uuid.UUID) -> ArtifactJob | None:
return self.session.get(ArtifactJob, job_id)
def job_for_plan(self, plan_id: uuid.UUID) -> ArtifactJob | None:
return self.session.scalar(select(ArtifactJob).where(ArtifactJob.plan_id == plan_id))
def jobs(self, limit: int = 100) -> list[ArtifactJob]:
return list(
self.session.scalars(
select(ArtifactJob).order_by(ArtifactJob.created_at.desc()).limit(limit)
)
)
def claimable_job(self, node_id: uuid.UUID, now: datetime) -> ArtifactJob | None:
return self.session.scalar(
select(ArtifactJob)
.where(
ArtifactJob.compute_node_id == node_id,
ArtifactJob.cancel_requested.is_(False),
or_(
ArtifactJob.status == "queued",
(ArtifactJob.status.in_(("claimed", "downloading", "verifying", "promoting")))
& (ArtifactJob.lease_expires_at < now),
),
)
.order_by(ArtifactJob.created_at)
.with_for_update(skip_locked=True)
.limit(1)
)
def inspections(self, job_id: uuid.UUID) -> list[ArtifactInspection]:
return list(
self.session.scalars(
select(ArtifactInspection)
.where(ArtifactInspection.job_id == job_id)
.order_by(ArtifactInspection.file_path, ArtifactInspection.inspection_type)
)
)
@@ -0,0 +1,37 @@
"""Immutable names for the PostgreSQL audit privilege boundary.
Migration 0024 owns the DDL. Runtime code keeps only the callable contract and catalog policy
names here; it never receives the owner/migration credential or an arbitrary privileged SQL path.
"""
from __future__ import annotations
AUDIT_OWNER_ROLE = "modelforge"
AUDIT_RUNTIME_ROLE = "modelforge_runtime"
AUDIT_FUNCTION_SCHEMA = "modelforge_audit"
AUDIT_APPEND_FUNCTION = "append_event_v2"
AUDIT_MUTATION_TRIGGER_FUNCTION = "enforce_owner_mutation"
AUDIT_EVENT_MUTATION_TRIGGER = "trg_modelforge_audit_events_owner"
AUDIT_EVENT_TRUNCATE_TRIGGER = "trg_modelforge_audit_events_truncate_owner"
AUDIT_HEAD_MUTATION_TRIGGER = "trg_modelforge_audit_head_owner"
AUDIT_HEAD_TRUNCATE_TRIGGER = "trg_modelforge_audit_head_truncate_owner"
AUDIT_FUNCTION_SEARCH_PATH = "pg_catalog"
# ``pg_proc.prosrc`` hashes for the exact migration-0024 function bodies. Production startup
# attests these as well as owner/grants/search_path: a same-signature replacement is not the
# canonical boundary. The static migration test derives both values from its immutable SQL so a
# body edit cannot silently drift this copy.
AUDIT_APPEND_BODY_SHA256 = "c9154911f1b1b70f77fe2642de156c98358cd0423f0c3e2a75bc4f1ee4409a86"
AUDIT_GUARD_BODY_SHA256 = "8af5fc31f3b2c729445baaae902eccd9cb3d70b257d0a19e6399f538ee43c6bf"
POSTGRES_APPEND_AUDIT_SQL = """
select event_id, sequence, event_hash, occurred_at
from modelforge_audit.append_event_v2(
cast(:event_id as uuid), cast(:occurred_at as timestamptz),
:correlation_id, :actor_type, :actor_id, :action, :resource_type,
:resource_id, :outcome, cast(:details_json as jsonb),
:expected_event_count, :expected_last_sequence, :expected_last_event_hash,
:expected_hash_format, :expected_v2_start_sequence,
:expected_legacy_prefix_count, :expected_legacy_prefix_seal
)
""".strip()
@@ -0,0 +1,126 @@
from __future__ import annotations
import uuid
from typing import TypeVar
from sqlalchemy import select
from sqlalchemy.orm import Session
from modelforge_api.persistence.models import (
Accelerator,
AcceleratorTelemetryLatest,
ComputeNode,
HardwareInventoryRun,
HostTelemetryLatest,
NodeCredential,
NodeEnrollment,
StorageVolumeState,
)
T = TypeVar("T")
class HardwareRepository:
def __init__(self, session: Session) -> None:
self.session = session
def node_by_key(self, key: str) -> ComputeNode | None:
return self.session.scalar(select(ComputeNode).where(ComputeNode.key == key))
def node_by_key_for_update(self, key: str) -> ComputeNode | None:
return self.session.scalar(
select(ComputeNode).where(ComputeNode.key == key).with_for_update()
)
def nodes(self) -> list[ComputeNode]:
return list(self.session.scalars(select(ComputeNode).order_by(ComputeNode.hostname)))
def node(self, node_id: uuid.UUID) -> ComputeNode | None:
return self.session.get(ComputeNode, node_id)
def node_for_update(self, node_id: uuid.UUID) -> ComputeNode | None:
return self.session.scalar(
select(ComputeNode).where(ComputeNode.id == node_id).with_for_update()
)
def accelerators(self, node_id: uuid.UUID | None = None) -> list[Accelerator]:
query = select(Accelerator).order_by(Accelerator.device_index)
if node_id is not None:
query = query.where(Accelerator.compute_node_id == node_id)
return list(self.session.scalars(query))
def accelerator(self, accelerator_id: uuid.UUID) -> Accelerator | None:
return self.session.get(Accelerator, accelerator_id)
def accelerator_by_uuid(self, node_id: uuid.UUID, device_uuid: str) -> Accelerator | None:
return self.session.scalar(
select(Accelerator).where(
Accelerator.compute_node_id == node_id, Accelerator.device_uuid == device_uuid
)
)
def host_telemetry(self, node_id: uuid.UUID) -> HostTelemetryLatest | None:
return self.session.scalar(
select(HostTelemetryLatest).where(HostTelemetryLatest.compute_node_id == node_id)
)
def accelerator_telemetry(self, accelerator_id: uuid.UUID) -> AcceleratorTelemetryLatest | None:
return self.session.scalar(
select(AcceleratorTelemetryLatest).where(
AcceleratorTelemetryLatest.accelerator_id == accelerator_id
)
)
def storage(self, node_id: uuid.UUID) -> list[StorageVolumeState]:
return list(
self.session.scalars(
select(StorageVolumeState)
.where(StorageVolumeState.compute_node_id == node_id)
.order_by(StorageVolumeState.purpose)
)
)
def storage_by_key(
self, node_id: uuid.UUID, purpose: str, path: str
) -> StorageVolumeState | None:
return self.session.scalar(
select(StorageVolumeState).where(
StorageVolumeState.compute_node_id == node_id,
StorageVolumeState.purpose == purpose,
StorageVolumeState.path == path,
)
)
def latest_run(self) -> HardwareInventoryRun | None:
return self.session.scalar(
select(HardwareInventoryRun).order_by(HardwareInventoryRun.started_at.desc()).limit(1)
)
def enrollment(self, enrollment_id: uuid.UUID) -> NodeEnrollment | None:
return self.session.get(NodeEnrollment, enrollment_id)
def enrollment_by_hash(self, token_hash: str) -> NodeEnrollment | None:
return self.session.scalar(
select(NodeEnrollment).where(NodeEnrollment.token_hash == token_hash)
)
def enrollments(self) -> list[NodeEnrollment]:
return list(
self.session.scalars(select(NodeEnrollment).order_by(NodeEnrollment.created_at.desc()))
)
def credential(self, credential_id: uuid.UUID) -> NodeCredential | None:
return self.session.get(NodeCredential, credential_id)
def active_credential_for_node(self, node_id: uuid.UUID) -> NodeCredential | None:
return self.session.scalar(
select(NodeCredential).where(
NodeCredential.compute_node_id == node_id,
NodeCredential.revoked_at.is_(None),
)
)
def add(self, entity: T) -> T:
self.session.add(entity)
self.session.flush()
return entity
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,288 @@
from __future__ import annotations
import uuid
from typing import Any, TypeVar
from sqlalchemy import func, select
from sqlalchemy.orm import Session, selectinload
from modelforge_api.persistence.models import (
ArtifactLocation,
DerivedArtifact,
DerivedArtifactSource,
Model,
ModelArtifact,
ModelRevision,
RuntimeProfile,
StorageRoot,
)
T = TypeVar("T")
class RegistryRepository:
"""Persistence-only operations for the M2 registry aggregate."""
def __init__(self, session: Session) -> None:
self.session = session
def page_models(
self,
*,
page: int,
page_size: int,
search: str | None = None,
lifecycle: str | None = None,
source_type: str | None = None,
) -> tuple[list[Model], int]:
query = select(Model)
count_query = select(func.count()).select_from(Model)
filters: list[Any] = []
if search:
pattern = f"%{search.lower()}%"
filters.append(
func.lower(Model.display_name).like(pattern)
| func.lower(Model.key).like(pattern)
| func.lower(Model.upstream_source).like(pattern)
)
if lifecycle:
filters.append(Model.lifecycle == lifecycle)
if source_type:
filters.append(Model.source_type == source_type)
if filters:
query = query.where(*filters)
count_query = count_query.where(*filters)
total = int(self.session.scalar(count_query) or 0)
items = list(
self.session.scalars(
query.options(selectinload(Model.revisions).selectinload(ModelRevision.artifacts))
.order_by(Model.display_name, Model.id)
.offset((page - 1) * page_size)
.limit(page_size)
)
)
return items, total
def model(self, model_id: uuid.UUID) -> Model | None:
return self.session.scalar(
select(Model)
.where(Model.id == model_id)
.options(selectinload(Model.revisions).selectinload(ModelRevision.artifacts))
)
def model_by_key(self, key: str) -> Model | None:
return self.session.scalar(select(Model).where(Model.key == key))
def model_by_source(self, source: str) -> Model | None:
return self.session.scalar(select(Model).where(Model.upstream_source == source))
def revision(self, revision_id: uuid.UUID) -> ModelRevision | None:
return self.session.get(ModelRevision, revision_id)
def revision_by_commit(self, model_id: uuid.UUID, commit: str) -> ModelRevision | None:
return self.session.scalar(
select(ModelRevision).where(
ModelRevision.model_id == model_id,
ModelRevision.resolved_commit_sha == commit,
)
)
def revisions(self, model_id: uuid.UUID) -> list[ModelRevision]:
return list(
self.session.scalars(
select(ModelRevision)
.where(ModelRevision.model_id == model_id)
.order_by(ModelRevision.discovered_at.desc())
)
)
def artifact(self, artifact_id: uuid.UUID) -> ModelArtifact | None:
return self.session.scalar(
select(ModelArtifact)
.where(ModelArtifact.id == artifact_id)
.options(selectinload(ModelArtifact.locations))
)
def artifact_by_digest(self, revision_id: uuid.UUID, digest: str) -> ModelArtifact | None:
return self.session.scalar(
select(ModelArtifact).where(
ModelArtifact.revision_id == revision_id, ModelArtifact.sha256 == digest
)
)
def artifacts(self, revision_id: uuid.UUID) -> list[ModelArtifact]:
return list(
self.session.scalars(
select(ModelArtifact)
.where(ModelArtifact.revision_id == revision_id)
.options(selectinload(ModelArtifact.locations))
.order_by(ModelArtifact.filename)
)
)
def derived(self, derived_id: uuid.UUID) -> DerivedArtifact | None:
return self.session.scalar(
select(DerivedArtifact)
.where(DerivedArtifact.id == derived_id)
.options(
selectinload(DerivedArtifact.source_artifacts),
selectinload(DerivedArtifact.locations),
)
)
def derived_for_revision(self, revision_id: uuid.UUID) -> list[DerivedArtifact]:
return list(
self.session.scalars(
select(DerivedArtifact)
.where(DerivedArtifact.revision_id == revision_id)
.options(
selectinload(DerivedArtifact.source_artifacts),
selectinload(DerivedArtifact.locations),
)
.order_by(DerivedArtifact.filename)
)
)
def derived_sources(self, derived_id: uuid.UUID) -> list[DerivedArtifactSource]:
return list(
self.session.scalars(
select(DerivedArtifactSource)
.where(DerivedArtifactSource.derived_artifact_id == derived_id)
.order_by(DerivedArtifactSource.ordinal)
)
)
def storage_root(self, root_id: uuid.UUID) -> StorageRoot | None:
return self.session.get(StorageRoot, root_id)
def storage_roots(self, node_id: uuid.UUID | None = None) -> list[StorageRoot]:
query = select(StorageRoot).order_by(StorageRoot.name)
if node_id:
query = query.where(StorageRoot.compute_node_id == node_id)
return list(self.session.scalars(query))
def location(self, location_id: uuid.UUID) -> ArtifactLocation | None:
return self.session.get(ArtifactLocation, location_id)
def add(self, entity: T) -> T:
self.session.add(entity)
self.session.flush()
return entity
def dependencies(self, resource_type: str, resource_id: uuid.UUID) -> list[dict[str, str]]:
dependencies: list[dict[str, str]] = []
if resource_type == "model":
ids = self.session.scalars(
select(ModelRevision.id).where(ModelRevision.model_id == resource_id)
)
dependencies.extend(
{
"resource_type": "model_revision",
"resource_id": str(item),
"relation": "revision",
}
for item in ids
)
elif resource_type == "model_revision":
ids = self.session.scalars(
select(ModelArtifact.id).where(ModelArtifact.revision_id == resource_id)
)
dependencies.extend(
{
"resource_type": "model_artifact",
"resource_id": str(item),
"relation": "artifact",
}
for item in ids
)
derived_ids = self.session.scalars(
select(DerivedArtifact.id).where(DerivedArtifact.revision_id == resource_id)
)
dependencies.extend(
{
"resource_type": "derived_artifact",
"resource_id": str(item),
"relation": "derived",
}
for item in derived_ids
)
elif resource_type == "model_artifact":
links = self.session.scalars(
select(DerivedArtifactSource.derived_artifact_id).where(
DerivedArtifactSource.source_artifact_id == resource_id
)
)
dependencies.extend(
{
"resource_type": "derived_artifact",
"resource_id": str(item),
"relation": "source",
}
for item in links
)
locations = self.session.scalars(
select(ArtifactLocation.id).where(ArtifactLocation.artifact_id == resource_id)
)
dependencies.extend(
{
"resource_type": "artifact_location",
"resource_id": str(item),
"relation": "location",
}
for item in locations
)
artifact = self.session.get(ModelArtifact, resource_id)
if artifact:
profiles = self.session.scalars(
select(RuntimeProfile.id).where(
RuntimeProfile.artifact_sha256 == artifact.sha256
)
)
dependencies.extend(
{
"resource_type": "runtime_profile",
"resource_id": str(item),
"relation": "digest",
}
for item in profiles
)
elif resource_type == "derived_artifact":
sources = self.session.scalars(
select(DerivedArtifactSource.source_artifact_id).where(
DerivedArtifactSource.derived_artifact_id == resource_id
)
)
dependencies.extend(
{
"resource_type": "model_artifact",
"resource_id": str(item),
"relation": "source_lineage",
}
for item in sources
)
locations = self.session.scalars(
select(ArtifactLocation.id).where(
ArtifactLocation.derived_artifact_id == resource_id
)
)
dependencies.extend(
{
"resource_type": "artifact_location",
"resource_id": str(item),
"relation": "location",
}
for item in locations
)
elif resource_type == "storage_root":
locations = self.session.scalars(
select(ArtifactLocation.id).where(ArtifactLocation.storage_root_id == resource_id)
)
dependencies.extend(
{
"resource_type": "artifact_location",
"resource_id": str(item),
"relation": "location",
}
for item in locations
)
return dependencies
@@ -0,0 +1,64 @@
from __future__ import annotations
from typing import Any
from sqlalchemy import select
from sqlalchemy.orm import Session
from modelforge_api.services.audit import AuditContext, AuditWriter
from .models import AuditEvent, Base, Model
class Repository[Entity: Base]:
def __init__(self, session: Session, entity_type: type[Entity]) -> None:
self.session = session
self.entity_type = entity_type
def get(self, entity_id: Any) -> Entity | None:
return self.session.get(self.entity_type, entity_id)
def list(self) -> list[Entity]:
return list(self.session.scalars(select(self.entity_type)))
def add(self, entity: Entity) -> Entity:
self.session.add(entity)
self.session.flush()
return entity
class ModelRepository(Repository[Model]):
def __init__(self, session: Session) -> None:
super().__init__(session, Model)
class AuditRepository:
def __init__(self, session: Session) -> None:
self.session = session
def append(
self,
*,
correlation_id: str,
actor_type: str,
actor_id: str,
action: str,
resource_type: str,
resource_id: str | None,
outcome: str,
details: dict[str, Any] | None = None,
) -> AuditEvent:
return AuditWriter(
self.session,
context=AuditContext(
correlation_id=correlation_id,
actor_type=actor_type,
actor_id=actor_id,
),
).write(
action=action,
resource_type=resource_type,
resource_id=resource_id,
outcome=outcome,
details=details or {},
)

Some files were not shown because too many files have changed in this diff Show More