f5fe7d975a
Per the codegen-deferred follow-up (internal#718 issue-comment 52157)
the CTO selected Path B: ship a registry-projection artifact alongside
the hand-authored providers/models block, with a subset-relation gate,
WITHOUT retiring or codegen'ing the hand-authored block. This preserves
the federation contract.
Added (all additive; zero runtime behavior change):
* .gitea/workflows/sync-providers-yaml.yml
Mirrors molecule-core's cross-repo canonical-sync gate. Fetches
controlplane internal/providers/providers.yaml via /raw (Gitea
1.22.6 quirk; /contents returns JSON+base64 envelope) and byte-
compares against the local synced copy.
* internal/providers/providers.yaml
Synced copy of the canonical registry. NOT hand-edited; the sync
workflow keeps it in step with controlplane main.
* internal/providers/registry_projection.py
Generator + verifier. Reads the synced providers.yaml, extracts
the registry's view of THIS template's runtime, and emits the
projection artifact. Also runs the SUBSET assertion: every
(provider, model) the registry claims this runtime serves must
be servable by the template's hand-authored authoritative source
(top-level `providers:` block for claude-code/hermes/codex;
`runtime_config.models[]` for openclaw; top-level `models[]` for
langgraph). Federation fail-open for runtimes absent from the
registry (langgraph today).
* internal/providers/registry-projection.json
Checked-in projection artifact for this template's runtime.
Auto-regenerated; the verify gate fails RED on drift.
* internal/providers/README.md
Documents the informational status + the subset contract.
* .gitea/workflows/verify-providers-projection.yml
Regenerates the artifact in CI and asserts (1) byte-identity
against the checked-in copy and (2) registry ⊆ template
hand-authored block.
* tests/test_registry_projection.py
TDD coverage: positive (current state is a subset or xfailed
with the known violation list), negative (injected fake
(provider, model) pair MUST be rejected — gate has teeth).
The hand-authored block stays AUTHORITATIVE. This gate only stops the
registry from claiming the template serves something it doesn't.
Not regressed (verified):
* cp#362 — untouched, no controlplane code edited
* P1 ResolveUpstream — untouched, no proxy code edited
* P2-B billing — untouched, no billing code edited
* P3 — untouched, no canvas/templates-from-registry code edited
* P4 PR-2 (422 hard-reject) — untouched, no workspace-server validation edited
* P4 closure follow-up (LLM_PROVIDER removed) — untouched, no LLM_PROVIDER read/write
* Template runtime behavior — untouched, no adapter / config.yaml `providers:`
/ `runtime_config` / `models[]` edited
internal#718 cross-link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
4.8 KiB
YAML
117 lines
4.8 KiB
YAML
name: verify-providers-projection
|
|
|
|
# Registry-projection drift + subset gate (internal#718 P4 Path B,
|
|
# CTO 2026-05-27 "scope-trimmed; preserve federation contract").
|
|
#
|
|
# This is the molecule-core verify-providers-gen analogue for workspace
|
|
# templates. Two invariants:
|
|
#
|
|
# 1. ARTIFACT DRIFT: regenerate registry-projection.json from the synced
|
|
# providers.yaml in memory; RED if it differs from the checked-in
|
|
# artifact. Catches a synced-providers.yaml update without a paired
|
|
# `registry_projection.py generate` run, AND catches a hand-edit of
|
|
# the artifact (it carries a DO NOT EDIT header).
|
|
#
|
|
# 2. SUBSET RELATION: for this template's runtime, every (provider,
|
|
# model) pair the registry claims must also be servable by the
|
|
# template's hand-authored authoritative source (top-level `providers:`
|
|
# block for claude-code/hermes/codex; runtime_config.models[] for
|
|
# openclaw; top-level models[] for langgraph). RED if the registry
|
|
# over-claims. Fails OPEN if this runtime is not in the registry at
|
|
# all (federation contract — mirrors molecule-ci
|
|
# `validate-workspace-template.py` `check_full_providers_block`).
|
|
#
|
|
# The hand-authored block stays AUTHORITATIVE. This gate only stops the
|
|
# registry from claiming the template serves something it doesn't.
|
|
#
|
|
# ENFORCEMENT POSTURE (matches molecule-core verify-providers-gen):
|
|
# standalone workflow; NOT in branch protection initially. Soak first;
|
|
# promote to required check in a follow-up once the registry data
|
|
# stabilizes (especially the claude-code colon-form non-anthropic pairs
|
|
# flagged in the Path B PR body).
|
|
|
|
on:
|
|
pull_request:
|
|
types: [opened, synchronize, reopened]
|
|
push:
|
|
branches: [main]
|
|
|
|
env:
|
|
GITHUB_SERVER_URL: https://git.moleculesai.app
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: verify-providers-projection-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
verify:
|
|
name: Regenerate projection, fail on drift, assert registry ⊆ template
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 6
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Install deps
|
|
run: |
|
|
set -euo pipefail
|
|
python -m pip install --quiet --upgrade pip
|
|
python -m pip install --quiet pyyaml
|
|
|
|
- name: Read this template's runtime id from config.yaml
|
|
id: rt
|
|
run: |
|
|
set -euo pipefail
|
|
RT=$(python -c "import yaml,sys;print(yaml.safe_load(open('config.yaml')).get('runtime',''))")
|
|
if [ -z "$RT" ]; then
|
|
echo "::error::config.yaml has no 'runtime:' field — projection gate cannot run."
|
|
exit 1
|
|
fi
|
|
echo "runtime=$RT" >> "$GITHUB_OUTPUT"
|
|
echo "Resolved template runtime: $RT"
|
|
|
|
- name: Verify projection artifact is in sync with the synced providers.yaml
|
|
run: |
|
|
set -euo pipefail
|
|
python internal/providers/registry_projection.py verify "${{ steps.rt.outputs.runtime }}"
|
|
|
|
- name: Belt-and-braces — regenerate in place and assert clean tree
|
|
run: |
|
|
set -euo pipefail
|
|
python internal/providers/registry_projection.py generate "${{ steps.rt.outputs.runtime }}"
|
|
if ! git diff --quiet -- internal/providers/registry-projection.json; then
|
|
echo "::error::internal/providers/registry-projection.json drifted from regeneration."
|
|
echo "Run 'python internal/providers/registry_projection.py generate ${{ steps.rt.outputs.runtime }}' and commit the result."
|
|
git --no-pager diff -- internal/providers/registry-projection.json | head -120
|
|
exit 1
|
|
fi
|
|
echo "OK — projection artifact is in sync."
|
|
|
|
- name: Subset assertion — registry ⊆ template hand-authored block
|
|
run: |
|
|
set -euo pipefail
|
|
python internal/providers/registry_projection.py subset "${{ steps.rt.outputs.runtime }}"
|
|
|
|
- name: Unit tests for the projection module
|
|
run: |
|
|
set -euo pipefail
|
|
python -m pip install --quiet pytest
|
|
# --rootdir=tests pins pytest's collection root to tests/ so a
|
|
# repo-root __init__.py that imports adapter.py (which depends
|
|
# on the in-container molecule_runtime wheel, absent on the
|
|
# gate runner) does not get auto-collected. This isolates the
|
|
# projection-gate tests from the wider in-container test
|
|
# surface — the only thing this gate needs to verify is the
|
|
# projection module itself.
|
|
pytest -q tests/test_registry_projection.py \
|
|
--rootdir=tests \
|
|
--import-mode=importlib \
|
|
-p no:cacheprovider
|