Skip to content
Merged
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
12 changes: 11 additions & 1 deletion .github/workflows/arm64-verify-runner-proof.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
strategy:
fail-fast: false
matrix:
host: [host-101, host-105]
host: [host-101, host-105, host-200]
# Pinned to the architecture AND the class, in the fleet's own vocabulary.
# Both labels are load-bearing: `omnibase-verify` alone would also match an
# amd64 verify runner on the primary host, and this job's whole subject is
Expand Down Expand Up @@ -136,7 +136,17 @@ jobs:
# The credential half. Named, hashed and length-checked -- never printed.
# A verify job that cannot read the lab kubeconfig fails at its first
# cluster call with an error that looks like a cluster problem.
#
# SKIPPED on host-200 (OMN-19657), deliberately: that runner is pinned to
# one lane's per-merge verify job, the same shape as host-202's, and
# reads the dev-200 compose lane's docker daemon through the
# host-gateway alias -- it carries no lab kubeconfig or operator-env
# mount by design (see docker-compose.runners-omninode-studio-runner.yml,
# "WHAT IS DELIBERATELY ABSENT"). host-202 is out of this workflow only
# because it is amd64, not because it is exempt from this rule; a future
# arm64 lane-pinned runner should skip this step the same way.
- name: Assert the lab credentials are readable by the job user
if: matrix.host != 'host-200'
run: |
set -euo pipefail
: "${KUBECONFIG:?KUBECONFIG must be set on a verify-class runner}"
Expand Down
15 changes: 15 additions & 0 deletions config/runner_fleet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@ hosts:
runner_name_prefix: omnipc2-verify-runner
expected_count: 1
classes: [verify]
# .200 -- Mac Studio M2 Ultra, arm64, always-on (OMN-19657, operator approval
# ledger 2026-09-25T18:27:30Z, answering the open question at ledger
# 2026-09-25T10:29:02Z). One verify runner, labelled host-200, for the
# dev-200 lane's per-merge verify job: that job reads the dev-200 compose
# lane's docker daemon (OMN-19543) and so must run on this host, the same
# reason host-201/host-202 are pinned to their own lanes. Docker Desktop,
# same macOS shape as .101/.105 (no group_add, socket GID fixed by the
# entrypoint's root phase). Always awake by construction -- it is the
# primary work box, not a machine that sleeps -- so no pmset or LaunchDaemon
# keep-awake is declared for it.
- host: stickybeatz-studio.tail75df5e.ts.net
arch: arm64
runner_name_prefix: omninode-studio-runner
expected_count: 1
classes: [verify]
# Docker subnet-pool alerting (OMN-12566). The runner host's default address
# pool can subnet a bounded number of networks before `docker network create`
# fails with "all predefined address pools have been fully subnetted". Alert at
Expand Down
10 changes: 9 additions & 1 deletion config/skip_count_baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ suites:
repo: omnibase_infra
job: Tests (Split n/m)
mode: nodeids
max_skips: 586
max_skips: 587
baseline_collected: 38874
provenance:
measured_at: '2026-09-18'
Expand Down Expand Up @@ -105,6 +105,13 @@ suites:
node_ids:
- tests.integration.migrations.test_omninode_internal_live_acl_gate_omn17886::test_fresh_build_matches_the_topology_modulo_the_shrink_only_allowlist
- tests.integration.migrations.test_omninode_internal_live_acl_omn17886::test_live_catalog_green_red_and_default_rule
- measured_at: '2026-09-26'
measured_by: OMN-19543
delta: 1
reason: >-
One positive control across a seam that has not landed yet, the same shape as the OMN-19507 entry above for the .202 runner: test_the_dev_200_route_labels_have_a_runner_to_land_on in tests/ci/test_studio_verify_runner_omn19657.py. It asserts that the verify labels the deploy-lane routing table declares for the dev-200 instance are satisfiable by the new .200 (stickybeatz-studio) verify runner this PR adds. The dev-200 instance declares no verify block in config/deploy_lane_routing.yaml until a sibling PR lands one, so on this base the test skips with a named reason instead of passing without looking. The other tests in the module assert the runner's compose labels, that host-200 is its own exclusive label, the absence of forbidden mounts (lab kubeconfig, operator env, OMNI_HOME clone bind), and its CPU/memory caps, all from committed state, and run in every job. The skip ends in the same change that gives dev-200 a verify block: the test then runs, and that change removes this id from this baseline.
node_ids:
- tests.ci.test_studio_verify_runner_omn19657::test_the_dev_200_route_labels_have_a_runner_to_land_on
measurement_command: gh run download 35402899669 --repo OmniNode-ai/omnibase_infra --pattern 'test-results-*' --dir <dir> && uv run python scripts/ci/skip_count_ratchet.py --print-observed --suite omnibase_infra/test-parallel --junit <dir>/*/junit-*.xml
source_runs:
- '35402899669'
Expand Down Expand Up @@ -704,3 +711,4 @@ suites:
- tests.unit.tools.test_python_source_topic_extraction::test_actual_topic_constants_extractable
- tests.unit.topology.test_validator_ro_principal_omn17792::test_validator_ro_reaches_a_cluster_only_where_the_seam_is_invoked
- tests.unit.validators.test_topology_validator::test_canonical_contract_file_is_valid
- tests.ci.test_studio_verify_runner_omn19657::test_the_dev_200_route_labels_have_a_runner_to_land_on
144 changes: 144 additions & 0 deletions docker/docker-compose.runners-omninode-studio-runner.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# SPDX-FileCopyrightText: 2026 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT
#
# OMN-19657 -- the .200 host's verify runner, for the dev-200 lane.
#
# WHY A SEPARATE FILE. Same reason as the .101, .105 and .202 files: compose
# interpolates the whole file before selecting services, so each host keeps
# its own fail-closed variable contract. See config/runner_fleet.yaml's
# `hosts:` comment for the inventory row this file realises.
#
# WHAT THIS HOST IS. A Mac Studio (M2 Ultra), arm64, Docker Desktop, always
# on (it is the primary work box, not a machine that sleeps -- no pmset or
# LaunchDaemon keep-awake is needed or declared here). It takes the VERIFY
# class only, the same reasoning host-101/host-105/host-202 share: this class
# is cloud/lane-touching and carries no architecture assumption, so it is the
# one that can sit on an arm64 host without any workflow changing. It is
# deliberately NOT labelled `omnibase-ci`: an action-class job must never be
# placed on the host that serves the always-on gpt-oss-120b planner
# (:8130) and the dev-200 compose lane.
#
# WHAT THIS RUNNER IS FOR. The per-merge verify job for a merge the deploy
# agent routed to dev-200 (config/deploy_lane_routing.yaml, once that
# instance's `verify:` block lands per OMN-19543/OMN-19507) reads that lane's
# docker daemon and its published ports. Both live on .200, so the job must
# run on .200 -- the same reason `host-202` is on omnipc2-verify-runner-1 and
# `host-201` is on omninode-verify-runner-1. Shaped after the .202 file, not
# the .101/.105 ones: like dev-202's verify job, this reads one lane's docker
# state through the host gateway alias, not cloud APIs, so it carries no
# kubeconfig or operator-env credential mount.
#
# VERIFY-RUNNER REACH. docker-compose.dev-200.yml publishes the dev-200 lane's
# ports LOOPBACK-ONLY (127.0.0.1:42xxx), documented there ("VERIFY-RUNNER
# REACH"): on Docker Desktop, a container's `host.docker.internal` is
# forwarded to the HOST's loopback interface, not its LAN address -- measured
# 2026-09-25, a server on 127.0.0.1:42999 answered a busybox container at
# `host.docker.internal:42999` but refused the same probe over
# 192.168.86.200:42999. So `host.docker.internal` (via the `host-gateway`
# alias below) is the only path a verify job on this runner has into the
# dev-200 lane, exactly like .202's runner has into dev-202.
#
# MACOS DIFFERENCES FROM THE PRIMARY (LINUX) HOST, each deliberate, same as
# the .101/.105 files:
# - no `group_add`. That literal GID is the primary host's docker group.
# Docker Desktop's socket inside the VM is root-owned and the image's
# entrypoint already fixes the socket GID in its root phase before
# dropping to `runner`, so a static GID would be wrong here and is not
# needed.
# - `/var/run/docker.sock` on this host is a symlink into the user's Docker
# Desktop socket. Bound at the classic path so nothing inside the
# container has to know that.
#
# WHAT IS DELIBERATELY ABSENT, relative to omninode-verify-runner-1 and the
# .101/.105 files, each line load-bearing:
# - no operator env file and no lab credentials directory. The dev-200
# verify job reads the lane through the docker socket and the host
# gateway, and its bus credentials (if any) arrive as Actions secrets.
# Nothing on .200 needs to be copied into this container.
# - no OMNI_HOME or clone bind, no DEPLOY_RUNNER_TOKEN release-train wiring,
# no write surface into a compose lane. The docker socket is a read use
# here (revision labels, `docker exec` health and psql reads), the same
# use the .201 and .202 verify runners make of theirs.
#
# STANDING IT UP. Same recipe as .202's runner (see that file's own header):
# not `deploy-runners.sh --add`, which is scoped to services declared in the
# PRIMARY host's compose file. From this host (jonah@stickybeatz-studio, this
# is the Mac the fleet artifacts already live on as the omni_home canonical
# clone):
#
# RUNNER_DIR=$HOME/.omnibase/runners
# mkdir -p "$RUNNER_DIR"
# rsync docker/runners/{entrypoint.sh,runner-job-started.sh,healthcheck.sh}
# and this file to "$RUNNER_DIR/docker/", the fleet image built or loaded as
# omninode-runner:latest, then
#
# STUDIO_RUNNER_TOKEN=<org registration token, minted by an operator; see
# OMN-19657 return note -- this lane does not mint one> \
# docker compose -p omninode-studio-runner \
# -f "$RUNNER_DIR/docker/docker-compose.runners-omninode-studio-runner.yml" \
# up -d --no-deps omninode-studio-runner-1
#
# The explicit project name matters: without `-p`, compose names the project
# after the directory (`docker`), a name every other per-host file in that
# directory would share.
#
# VERIFICATION (read-only, once registered):
# gh api orgs/OmniNode-ai/actions/runners --paginate \
# --jq '.runners[] | select(.name=="omninode-studio-runner-1")'
# -- expect status "online", labels self-hosted/omnibase-verify/host-200/
# linux/arm64/arch-arm64.
# docker exec omninode-studio-runner-1 sh -c \
# 'id; docker info --format "{{.ServerVersion}} {{.OSType}}/{{.Architecture}}"'
# -- expect the job user to read the socket and see the host's own Docker
# Desktop daemon.
# A workflow_dispatch of .github/workflows/arm64-verify-runner-proof.yml
# with the `host-200` matrix leg is the same four-assertion proof the
# .101/.105 hosts get (arch, toolchain, credentials -- this host mounts
# none, so that step is skipped by the job's own guard -- and the docker
# socket).
services:
omninode-studio-runner-1:
image: omninode-runner:latest
container_name: omninode-studio-runner-1
restart: unless-stopped
mem_limit: 2g
memswap_limit: 4g
cpus: "1.0"
pids_limit: 2048
environment:
# `arm64` plus the explicit OMN-17477 arch label, the same vocabulary
# the .101/.105 hosts use. `host-200` mirrors `host-201`/`host-202`: a
# job pinning a HOST and a job pinning an ARCHITECTURE each get their
# own label instead of overloading one to mean both.
RUNNER_LABELS: self-hosted,omnibase-verify,host-200,linux,arm64,arch-arm64
RUNNER_NAME: omninode-studio-runner-1
GITHUB_ORG_URL: https://github.com/OmniNode-ai
RUNNER_TOKEN: ${STUDIO_RUNNER_TOKEN:-}
RUNNER_GROUP: omnibase-deploy
ACTIONS_RUNNER_HOOK_JOB_STARTED: /usr/local/bin/runner-job-started.sh
# Inside this container `localhost` is the container itself, not .200.
LANE_PROBE_HOST: host.docker.internal
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./runners/entrypoint.sh:/usr/local/bin/entrypoint.sh:ro
- ./runners/runner-job-started.sh:/usr/local/bin/runner-job-started.sh:ro
- ./runners/healthcheck.sh:/usr/local/bin/healthcheck.sh:ro
# OMN-19070: the disk-admission self-pause marker directory every
# runner running the job-started hook mounts.
- ${RUNNER_DISK_ADMISSION_PAUSE_HOST_DIR:-./state/disk-admission-pause}:/home/runner/.onex-disk-admission-pause
- omninode-studio-runner-1-creds:/home/runner/.runner-creds
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD-SHELL", "/usr/local/bin/healthcheck.sh"]
interval: 30s
timeout: 12s
retries: 3
start_period: 120s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
omninode-studio-runner-1-creds:
119 changes: 119 additions & 0 deletions tests/ci/test_studio_verify_runner_omn19657.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT

"""OMN-19657 -- the .200 verify runner is where the dev-200 verify job lands.

Mirrors ``test_omnipc2_verify_runner_omn19507.py``: the dev-200 instance (once
its ``verify:`` block lands, OMN-19543/OMN-19507) sends its per-merge verify
job to ``[self-hosted, omnibase-verify, host-200]``. That job reads the
dev-200 lane's docker daemon and, through ``host.docker.internal``, the ports
the lane publishes on the docker bridge gateway (loopback-only on Docker
Desktop). So the runner that satisfies those labels must:

* carry every one of those labels, and be the only declared runner carrying
``host-200``;
* never carry ``omnibase-ci`` (the action fleet) or ``omnibase-deploy``-only
duties that are not this lane's, so no other job class lands on .200;
* mount the docker socket and carry the host-gateway alias, and nothing that
hands a job a write surface or a credential (no operator env, no clone
tree, no lab kubeconfig -- this runner is lane-pinned, not a general
cloud-touching verify host).
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import pytest
import yaml

pytestmark = pytest.mark.ci

ROOT = Path(__file__).resolve().parents[2]
DOCKER = ROOT / "docker"
COMPOSE = DOCKER / "docker-compose.runners-omninode-studio-runner.yml"
SERVICE = "omninode-studio-runner-1"
DEV_200_VERIFY_LABELS = {"self-hosted", "omnibase-verify", "host-200"}
FORBIDDEN_LABELS = {"omnibase-ci", "omnibase-deploy", "omnipc2-customer"}


def _services(path: Path) -> dict[str, dict[str, Any]]:
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
return data.get("services") or {}


def _labels(service: dict[str, Any]) -> set[str]:
env = service.get("environment") or {}
return set(str(env.get("RUNNER_LABELS", "")).split(",")) - {""}


def _runner() -> dict[str, Any]:
services = _services(COMPOSE)
assert SERVICE in services, sorted(services)
return services[SERVICE]


def test_studio_verify_runner_carries_the_dev_200_verify_labels() -> None:
labels = _labels(_runner())
assert labels >= DEV_200_VERIFY_LABELS, sorted(labels)
assert not (labels & FORBIDDEN_LABELS), sorted(labels & FORBIDDEN_LABELS)


def test_studio_verify_runner_carries_its_own_architecture_labels() -> None:
# M2 Ultra is arm64; a runner claiming the wrong architecture label hands
# arch-pinned jobs to a CPU that cannot run them.
labels = _labels(_runner())
assert {"arm64", "arch-arm64"} <= labels, sorted(labels)
assert "amd64" not in labels and "arch-amd64" not in labels, sorted(labels)


def test_studio_verify_runner_is_the_only_runner_labelled_host_200() -> None:
carriers = []
for path in sorted(DOCKER.glob("docker-compose.runners*.yml")):
for name, service in _services(path).items():
if "host-200" in _labels(service or {}):
carriers.append(f"{path.name}:{name}")
assert carriers == [f"{COMPOSE.name}:{SERVICE}"], (
f"host-200 must name exactly one declared runner, found {carriers}"
)


def test_studio_verify_runner_reads_the_lane_and_nothing_more() -> None:
runner = _runner()
volumes = [str(v) for v in runner.get("volumes") or []]
assert "/var/run/docker.sock:/var/run/docker.sock" in volumes
assert "host.docker.internal:host-gateway" in (runner.get("extra_hosts") or [])
assert (runner.get("environment") or {}).get(
"LANE_PROBE_HOST"
) == "host.docker.internal"
joined = "\n".join(volumes)
for absent in ("operator.env", "OMNI_HOME", "lab-credentials", ".omnibase/.env"):
assert absent not in joined, f"{absent!r} must not be mounted: {volumes}"
# Capped like the .202 lane-pinned runner: this host also serves the
# always-on planner and the dev-200 lane.
assert runner.get("cpus") == "1.0"
assert runner.get("mem_limit") == "2g"


def test_studio_verify_runner_declares_no_group_add() -> None:
# macOS/Docker Desktop difference from the primary (Linux) host, same as
# the .101/.105 files: the entrypoint's root phase fixes the socket GID,
# so a static group_add here would be wrong.
assert "group_add" not in _runner()


def test_the_dev_200_route_labels_have_a_runner_to_land_on() -> None:
"""Positive control across the seam: once the routing table declares the
dev-200 instance's verify block, its labels are satisfiable by this
runner. Skips today because that block has not landed yet (OMN-19543/
OMN-19507); this is the seam test, not the trigger to land it."""
routing = ROOT / "config" / "deploy_lane_routing.yaml"
if not routing.is_file():
pytest.skip("the routing table is not on this base yet")
table = yaml.safe_load(routing.read_text(encoding="utf-8")) or {}
instance = (table.get("instances") or {}).get("dev-200")
if instance is None or "verify" not in instance:
pytest.skip("the routing table declares no dev-200 verify block yet")
wanted = set(instance["verify"]["runner_labels"])
assert wanted <= _labels(_runner()), (sorted(wanted), sorted(_labels(_runner())))
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def _config() -> ModelRunnerFleetConfig:
("stickybeatz-2.tail75df5e.ts.net", "arm64", 1),
("omnibook.tail75df5e.ts.net", "arm64", 1),
("192.168.86.202", "amd64", 1),
("stickybeatz-studio.tail75df5e.ts.net", "arm64", 1),
)


Expand Down
Loading