Publish curated ITWorx-ModelForge source from 990401737f96c8d46a9f7b0d84994516cadcb6a0

This commit is contained in:
Public Source Publisher committed 2026-09-30 09:23:17 +00:00
1 parent 7082ab955a
commit 848f0d8f8a
434 files changed
+67298 -4315

No files matched your search

+56 -21
View File
@@ -1,27 +1,59 @@
FROM python:3.12-slim-trixie@sha256:09f7da3bc104798d0afb40bc08d23ab2da20a76130cec1f2ef170848f5d85217 AS builder
# Canonical's maintained glibc-compatible Python runtime is kept as a named stage so the builder
# can preserve its passwd/group contract without adding a shell or package manager to the final
# image.
FROM ubuntu/python:3.12-24.04@sha256:d5b4c1e1da458c6e10da9d7f42276d8d640ed5a0d87b1cced734a9434c531a62 AS runtime-base
# The builder is deliberately separate from the runtime. The agent only needs the Python
# application packages at runtime; a full Debian build environment must not become part of the
# shipped image or its vulnerability surface.
FROM python:3.12-slim-trixie@sha256:e5c9fa26ffb76e11e0f054f30dc2523a2f9693f0c36c0cf1e39b27e152d899fc AS builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /build
COPY config/security/build-toolchain.lock /tmp/modelforge-build-toolchain.lock
COPY backend ./backend
COPY node-agent ./node-agent
RUN python -m venv /opt/venv \
&& /opt/venv/bin/pip install --no-cache-dir --upgrade pip "setuptools>=78.1.1" "msgpack>=1.2.1" \
&& /opt/venv/bin/pip install --no-cache-dir ./backend ./node-agent \
&& /opt/venv/bin/pip check \
&& /opt/venv/bin/python -m pip uninstall --yes pip setuptools
FROM python:3.12-slim-trixie@sha256:09f7da3bc104798d0afb40bc08d23ab2da20a76130cec1f2ef170848f5d85217
# Install into a plain target directory so the final image can use Canonical's maintained,
# glibc-compatible chiselled Python runtime. Hash-locked build tooling is removed inside the
# builder only; no installer or build-backend metadata is carried into the runtime image. The
# target install ignores the builder venv so a build-only package cannot mask a runtime dependency.
# The import check exercises the same dependency closure that the final image receives without
# contacting the control plane or touching a model repository.
RUN python -m venv /opt/build-venv \
&& /opt/build-venv/bin/python -m pip install --no-cache-dir --disable-pip-version-check \
--require-hashes --only-binary=:all: --index-url https://pypi.org/simple \
-r /tmp/modelforge-build-toolchain.lock \
&& /opt/build-venv/bin/python -m pip install --no-cache-dir --disable-pip-version-check \
--no-build-isolation --ignore-installed --no-compile \
--target /opt/site-packages ./backend ./node-agent \
&& PYTHONPATH=/opt/site-packages /opt/build-venv/bin/python -m pip check \
&& PYTHONPATH=/opt/site-packages /opt/build-venv/bin/python -c \
"import httpx, psutil, pynvml, modelforge_api, modelforge_node_agent" \
&& /opt/build-venv/bin/python -m pip uninstall --yes pip setuptools wheel editables hatchling pathspec \
pluggy tomlkit trove-classifiers
# Prepare the fixed runtime contract without requiring a shell or package manager in the final
# image. The numeric IDs are intentional: Compose and existing volumes rely on UID 100/GID 101.
RUN mkdir -p /runtime/app /runtime/data/state /runtime/data/hf-cache \
/runtime/data/artifacts /runtime/data/quarantine \
&& chown -R 100:101 /runtime/app /runtime/data
# Preserve the canonical runtime's passwd/group entries and add the existing ModelForge identity
# with its established home directory. The final image remains shell-less and runs numerically as
# UID 100/GID 101, while libraries that resolve the process user still see the same contract.
COPY --from=runtime-base /etc/passwd /runtime/etc/passwd
COPY --from=runtime-base /etc/group /runtime/etc/group
RUN python -c "from pathlib import Path; p=Path('/runtime/etc/passwd'); s=p.read_text(); p.write_text(s if '\\nmodelforge-agent:' in s else s.rstrip('\\n')+'\\nmodelforge-agent:x:100:101::/app:/usr/sbin/nologin\\n'); p=Path('/runtime/etc/group'); s=p.read_text(); p.write_text(s if '\\nmodelforge-agent:' in s else s.rstrip('\\n')+'\\nmodelforge-agent:x:101:\\n')"
FROM runtime-base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:${PATH}"
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade --yes \
&& rm -rf /var/lib/apt/lists/* \
&& /usr/local/bin/python -m pip uninstall --yes pip setuptools
PYTHONPATH=/opt/site-packages \
HOME=/app
# 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
@@ -36,17 +68,20 @@ LABEL org.opencontainers.image.description="Outbound-only compute node agent for
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.source="https://git.example.com/example/modelforge"
LABEL org.opencontainers.image.vendor="ITWorx"
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder --chown=100:101 /opt/site-packages /opt/site-packages
COPY --from=builder --chown=100:101 /runtime/app /app
COPY --from=builder --chown=100:101 /runtime/data /data
COPY --from=builder /runtime/etc/passwd /etc/passwd
COPY --from=builder /runtime/etc/group /etc/group
RUN groupadd --system --gid 101 modelforge-agent \
&& useradd --system --uid 100 --gid 101 --home-dir /app --create-home modelforge-agent \
&& mkdir -p /data/state /data/hf-cache /data/artifacts /data/quarantine \
&& chown -R modelforge-agent:modelforge-agent /data
WORKDIR /app
USER modelforge-agent
USER 100:101
CMD ["modelforge-node-agent"]
# Canonical's rock image has a Pebble entrypoint. The agent has no init supervisor requirement,
# so explicitly remove it and invoke the reviewed Python module directly.
ENTRYPOINT []
CMD ["python3", "-m", "modelforge_node_agent.main"]
+6 -4
View File
@@ -1,10 +1,12 @@
[build-system]
requires = ["setuptools>=80"]
requires = [
"setuptools==84.0.0",
]
build-backend = "setuptools.build_meta"
[project]
name = "modelforge-node-agent"
version = "1.2.2"
version = "1.3.0-rc.1"
license = "AGPL-3.0-or-later"
requires-python = ">=3.12"
dependencies = [
@@ -13,13 +15,13 @@ dependencies = [
# Ships as one product with the control plane, so this tracks VERSION exactly. A
# stale pin here made the Node Agent image unbuildable the moment the product
# version moved, which is a release failure that only appears at build time.
"modelforge-api==1.2.2",
"modelforge-api==1.3.0-rc.1",
"pydantic-settings==2.15.0",
]
[project.optional-dependencies]
dev = [
"pytest==8.4.2",
"pytest==9.0.3",
"ruff==0.16.5",
"mypy==1.20.2",
]
@@ -1 +1 @@
__version__ = "1.2.2"
__version__ = "1.3.0-rc.1"
+10 -2
View File
@@ -5,6 +5,8 @@ import os
from dataclasses import asdict, dataclass
from pathlib import Path
CREDENTIAL_SHARED_READ_MODE = 0o640
@dataclass
class AgentPersistentState:
@@ -36,11 +38,17 @@ class AgentStateStore:
def credential(self) -> str | None:
try:
value = self.credential_file.read_text(encoding="utf-8").strip()
return value or None
if not value:
return None
# The runtime worker has a separate non-root identity. It joins the dedicated agent
# group solely to read this credential from the shared, read-only state volume.
# Normalising an older 0600 credential here makes ordinary Agent upgrades safe too.
os.chmod(self.credential_file, CREDENTIAL_SHARED_READ_MODE)
return value
except OSError:
return None
def save_credential(self, credential: str) -> None:
self.credential_file.parent.mkdir(parents=True, exist_ok=True)
self.credential_file.write_text(credential, encoding="utf-8")
os.chmod(self.credential_file, 0o600)
os.chmod(self.credential_file, CREDENTIAL_SHARED_READ_MODE)
+19
View File
@@ -1,6 +1,7 @@
import asyncio
import logging
from pathlib import Path
from unittest.mock import call, patch
import httpx
import pytest
@@ -182,6 +183,24 @@ def test_first_enrollment_and_restart_reuse_persisted_credential(tmp_path: Path)
assert restarted.credential == first.credential
def test_persisted_credential_is_group_readable_for_the_runtime_worker(tmp_path: Path) -> None:
with patch("modelforge_node_agent.state.os.chmod") as chmod:
agent = build_agent(tmp_path, FakeTransport())
agent.ensure_enrolled()
credential_file = agent.settings.credential_file
assert chmod.call_args_list == [call(credential_file, 0o640)]
# An upgrade can retain the stricter historical mode. The Agent owns the file and repairs
# only that mode at startup so the separately non-root runtime worker can read, never
# write, its node credential.
restarted = build_agent(tmp_path, FakeTransport(), token=None)
assert restarted.credential == agent.credential
assert chmod.call_args_list == [
call(credential_file, 0o640),
call(credential_file, 0o640),
]
def test_agent_advertises_typed_runtime_worker_capabilities(tmp_path: Path) -> None:
capabilities = build_agent(tmp_path, FakeTransport()).metadata().supported_capabilities
assert "runtime.probe.v1" in capabilities