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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ctl/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ requires-python = ">=3.11"
license = { text = "Apache-2.0" }
dependencies = [
"mas-runtime",
"mas-library-standard",
"click>=8.0",
"pyyaml>=6.0",
"python-dotenv>=1.0",
Expand Down Expand Up @@ -44,5 +45,13 @@ packages = ["src/mas"]
# are excluded from the published wheel.
exclude = ["src/mas/ctl/testing/**"]

# Package a copy of the canonical runtime/lab manifest schemas inside mas.ctl so
# overlay merging and manifest validation still work when installed as a wheel
# (e.g. via 'uv tool install'), where there is no repo checkout to read
# docs/schemas/ from. See mas.ctl.validate.schemas.schema_root().
[tool.hatch.build.targets.wheel.force-include]
"../docs/schemas" = "src/mas/ctl/_schemas"

[tool.uv.sources]
mas-runtime = { workspace = true }
mas-library-standard = { workspace = true }
18 changes: 7 additions & 11 deletions ctl/src/mas/ctl/overlay/merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
from functools import lru_cache
import logging
from copy import deepcopy
from pathlib import Path
from typing import Any

import yaml

from mas.ctl.validate.schemas import schema_root

logger = logging.getLogger(__name__)


Expand All @@ -21,10 +22,10 @@
# single edit in a single file (docs/schemas/runtime/<kind>.schema.yaml),
# not a second hand-maintained overlay-*-patch fragment kept in sync by hand.
_OVERLAY_PATCH_SCHEMA_FILES: dict[str, str] = {
"Agent": "docs/schemas/runtime/agent.schema.yaml",
"MAS": "docs/schemas/runtime/mas.schema.yaml",
"Flavour": "docs/schemas/runtime/flavour.schema.yaml",
"Infra": "docs/schemas/runtime/infra.schema.yaml",
"Agent": "runtime/agent.schema.yaml",
"MAS": "runtime/mas.schema.yaml",
"Flavour": "runtime/flavour.schema.yaml",
"Infra": "runtime/infra.schema.yaml",
}

# Where to start reading "properties" from, per schema file, relative to that
Expand All @@ -38,14 +39,9 @@
}


def _repo_root() -> Path:
# .../mas-lab/ctl/src/mas/ctl/overlay/merge.py -> parents[5] == repo root
return Path(__file__).resolve().parents[5]


@lru_cache(maxsize=8)
def _load_yaml_schema(rel_path: str) -> dict[str, Any]:
schema_path = _repo_root() / rel_path
schema_path = schema_root() / rel_path
data = yaml.safe_load(schema_path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}

Expand Down
25 changes: 23 additions & 2 deletions ctl/src/mas/ctl/validate/schemas.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
# Copyright (c) 2026 Cisco Systems, Inc. and its affiliates
# SPDX-License-Identifier: Apache-2.0
"""Manifest JSON Schema loader — schemas live under docs/schemas/."""
"""Manifest JSON Schema loader — schemas live under docs/schemas/.

Two layouts are supported so this works both in a source checkout (dev /
editable install, where ``docs/schemas`` sits a few parents above this file)
and when installed as a built wheel (e.g. via ``uv tool install``, where
there is no repo root — the schemas are instead packaged inside ``mas.ctl``
itself via ``force-include`` and resolved through ``importlib.resources``).
"""

from __future__ import annotations

from functools import lru_cache
from importlib import resources
from pathlib import Path
from typing import Any

import yaml

_SCHEMA_ROOT = Path(__file__).resolve().parents[5] / "docs" / "schemas"

def _repo_checkout_schema_root() -> Path | None:
# .../mas-lab/ctl/src/mas/ctl/validate/schemas.py -> parents[5] == repo root
candidate = Path(__file__).resolve().parents[5] / "docs" / "schemas"
return candidate if candidate.is_dir() else None


def _packaged_schema_root() -> Path:
# Falls back to the copy of docs/schemas packaged inside mas.ctl (see
# [tool.hatch.build.targets.wheel.force-include] in ctl/pyproject.toml).
return Path(str(resources.files("mas.ctl") / "_schemas"))


_SCHEMA_ROOT = _repo_checkout_schema_root() or _packaged_schema_root()

_KIND_MAP: dict[str, str] = {
"agent": "runtime/agent.schema.yaml",
Expand Down
7 changes: 7 additions & 0 deletions lab/components/core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,12 @@ dependencies = [
[tool.hatch.build.targets.wheel]
packages = ["src/mas"]

# Package a copy of the lab manifest schemas inside mas.lab.schemas so
# mas-lab-core still works when installed as a wheel (e.g. via
# 'uv tool install'), where there is no repo checkout to read
# docs/schemas/lab from. See mas.lab.schemas.paths.lab_schema_dir().
[tool.hatch.build.targets.wheel.force-include]
"../../../docs/schemas/lab" = "src/mas/lab/schemas/_schemas"

[project.entry-points."mas.lab.controller.plugins"]
local-inproc = "mas.lab.controllers.local_inproc:LocalInprocController"
20 changes: 16 additions & 4 deletions lab/components/core/src/mas/lab/schemas/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""Resolve manifest schema files from installed package dependencies."""
from __future__ import annotations

from importlib import resources
from pathlib import Path


Expand All @@ -13,14 +14,25 @@ def runtime_schema_dir() -> Path:
return _SCHEMA_ROOT / "runtime"


def _repo_root() -> Path:
"""Repository root (contains docs/schemas/)."""
return Path(__file__).resolve().parents[7]
def _repo_checkout_root() -> Path | None:
"""Repository root (contains docs/schemas/), if running from a checkout."""
candidate = Path(__file__).resolve().parents[7]
return candidate if (candidate / "docs" / "schemas").is_dir() else None


def _packaged_lab_schema_dir() -> Path:
# Falls back to the copy of docs/schemas/lab packaged inside
# mas.lab.schemas (see [tool.hatch.build.targets.wheel.force-include] in
# lab/components/core/pyproject.toml).
return Path(str(resources.files("mas.lab.schemas") / "_schemas"))


def lab_schema_dir() -> Path:
"""YAML/JSON schemas for mas-lab manifests (experiment, pipeline, dataset, lab-config)."""
return _repo_root() / "docs" / "schemas" / "lab"
repo_root = _repo_checkout_root()
if repo_root is not None:
return repo_root / "docs" / "schemas" / "lab"
return _packaged_lab_schema_dir()


def lab_artefact_schema_dir() -> Path:
Expand Down
8 changes: 8 additions & 0 deletions lab/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ Issues = "https://github.com/outshift-open/mas-lab/issues"

[project.scripts]
mas-lab = "mas.lab.cli:app"
mas-ctl = "mas.ctl.cli.main:main"

[tool.hatch.build.targets.wheel]
packages = ["src/mas"]

# Package a copy of the top-level examples/ tree inside mas.lab so
# 'mas-lab init' still works when installed as a wheel (e.g. via
# 'uv tool install'), where there is no repo checkout to read examples/
# from. See mas.lab.cli.commands.init._templates_root().
[tool.hatch.build.targets.wheel.force-include]
"../examples" = "src/mas/lab/_examples"
16 changes: 14 additions & 2 deletions lab/src/mas/lab/cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from __future__ import annotations

import re
from importlib import resources
from pathlib import Path
from typing import Any

Expand All @@ -21,9 +22,20 @@
_DEFAULT_TARGET_MODEL = "gpt-4o-mini"


def _templates_root() -> Path:
def _repo_checkout_examples_root() -> Path | None:
# lab/src/mas/lab/cli/commands/init.py -> repo root/examples
return Path(__file__).resolve().parents[6] / "examples"
candidate = Path(__file__).resolve().parents[6] / "examples"
return candidate if candidate.is_dir() else None


def _packaged_examples_root() -> Path:
# Falls back to the copy of examples/ packaged inside mas.lab (see
# [tool.hatch.build.targets.wheel.force-include] in lab/pyproject.toml).
return Path(str(resources.files("mas.lab") / "_examples"))


def _templates_root() -> Path:
return _repo_checkout_examples_root() or _packaged_examples_root()


def _load_template(rel_path: str) -> str:
Expand Down
3 changes: 3 additions & 0 deletions library-standard/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,8 @@ include = [
[tool.hatch.build.targets.wheel]
packages = ["src/mas"]

[tool.hatch.build.targets.wheel.force-include]
"library.yaml" = "src/mas/library/standard/library.yaml"

[tool.uv.sources]
mas-runtime = { workspace = true }
40 changes: 21 additions & 19 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading