chore: lock down as publish artifact; source-of-truth is monorepo

This repo is now a publish artifact of Molecule-AI/molecule-core/workspace/.
Runtime code edits go to the monorepo; the publish-runtime workflow
regenerates this mirror + uploads to PyPI on every runtime-v* tag.

Changes:

- Delete .github/workflows/publish.yml. PyPI publishing now happens only
  from the monorepo's publish-runtime workflow. Without removing this,
  two different code shapes could reach PyPI depending on which workflow
  fired (the drift this lockdown is preventing).

- Delete .github/workflows/auto-promote-staging.yml. The staging→main
  fast-forward dance has no purpose on a mirror repo — the mirror is
  rebuilt wholesale on each release.

- Replace .github/workflows/ci.yml with a 'mirror-guard' job that fails
  on any pull_request event with a clear redirect message. Push events
  are still allowed (so existing in-flight branches don't all turn red
  while the migration finishes); that allowance becomes a follow-up
  removal once the auto-sync from monorepo is wired up.

- Rewrite README.md with a prominent ⚠ banner pointing at the monorepo.

- Add CONTRIBUTING.md with the explicit redirect table.

What this does NOT do:

- Wire up the auto-sync from monorepo → this repo. The
  publish-runtime workflow currently uploads to PyPI but doesn't push
  the rewritten tree back here. As a follow-up, extend that workflow
  with a step that commits the build dir to this repo's main. Until
  then this repo's contents will go stale relative to PyPI — but
  that's fine because no one should be reading code from here anyway.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Hongming Wang 2026-04-26 12:03:12 -07:00
parent 7fc3537b2f
commit 96864263bb
5 changed files with 87 additions and 211 deletions

View File

@ -1,119 +0,0 @@
name: Auto-promote staging → main
# Fast-forwards `main` to `staging` when staging is strictly ahead (main
# is an ancestor). Eliminates the manual sync-PR round for non-critical
# repos.
#
# Gate handling:
# - If the repo has required_status_checks configured AND the API
# returns them, all must be SUCCESS on the staging HEAD commit.
# - If no gates are configured (or the API 403s on a private free-tier
# repo), `--ff-only` is the sole safety. It refuses if main has
# independent commits staging doesn't contain.
#
# Excluded by policy: molecule-core + molecule-controlplane. Those two
# stay manual per CEO directive 2026-04-24.
#
# Safety:
# - Only fires on push to staging (PRs into staging don't promote)
# - `--ff-only` refuses if main has diverged (hotfix landed directly)
# - Promote commit goes through GITHUB_TOKEN; shows up in git log as
# a deliberate act
on:
push:
branches: [staging]
workflow_dispatch:
permissions:
contents: write
statuses: read
jobs:
promote:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Check required gates (if configured) on staging HEAD
id: gates
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.sha }}
shell: bash
run: |
set -euo pipefail
# Try to read required gates from branch protection. Free-tier
# private repos may 403; handle that gracefully.
GATES_JSON=$(gh api "repos/${REPO}/branches/staging/protection/required_status_checks" 2>/dev/null || echo '{}')
GATES=$(echo "${GATES_JSON}" | jq -r '.contexts[]?' 2>/dev/null || true)
if [ -z "$GATES" ]; then
echo "No required gates configured (or API inaccessible). Relying on --ff-only safety."
echo "ok=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Required gates on staging:"
echo "${GATES}" | sed 's/^/ - /'
ALL_GREEN=true
while IFS= read -r gate; do
[ -z "$gate" ] && continue
conclusion=$(gh api "repos/${REPO}/commits/${HEAD_SHA}/check-runs" \
--jq "[.check_runs[] | select(.name == \"${gate}\")] | sort_by(.completed_at) | last.conclusion" \
2>/dev/null || echo "")
if [ -z "$conclusion" ] || [ "$conclusion" = "null" ]; then
conclusion=$(gh api "repos/${REPO}/commits/${HEAD_SHA}/status" \
--jq "[.statuses[] | select(.context == \"${gate}\")] | sort_by(.updated_at) | last.state" \
2>/dev/null || echo "")
fi
if [ "$conclusion" != "success" ] && [ "$conclusion" != "SUCCESS" ]; then
echo "::warning::Gate '${gate}' is '${conclusion:-missing}' on ${HEAD_SHA} — skipping promote."
ALL_GREEN=false
else
echo " ✓ ${gate}: success"
fi
done <<< "$GATES"
echo "ok=${ALL_GREEN}" >> "$GITHUB_OUTPUT"
- name: Fast-forward main to staging
if: steps.gates.outputs.ok == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
git config user.email "actions@github.com"
git config user.name "github-actions[bot]"
# staging is the checked-out branch (workflow fires on push to
# staging). Can't fetch into it. Fetch main into a local main.
git fetch origin main
git checkout -B main origin/main
# Check if main is already at or ahead of origin/staging.
if git merge-base --is-ancestor origin/staging main 2>/dev/null; then
echo "main already contains staging; nothing to promote."
exit 0
fi
# --ff-only refuses if main has independent commits not on
# staging (divergence — hotfix direct to main). Human resolves.
if ! git merge --ff-only origin/staging 2>&1; then
echo "::warning::main has diverged from staging — refusing fast-forward. Resolve manually (likely a direct-to-main commit exists that staging doesn't have)."
exit 0
fi
git push origin main
echo "::notice::Promoted: main is now at $(git rev-parse --short HEAD)"

View File

@ -1,38 +1,43 @@
name: CI
name: ci
# Mirror-guard CI. This repo is a publish artifact of the monorepo
# `Molecule-AI/molecule-core/workspace/` directory — see README.
#
# Direct commits + PRs to this repo are no longer accepted; the
# canonical edit point is the monorepo. This workflow exists only
# to enforce that, by failing CI on any push that wasn't produced
# by the publish-runtime sync (a future automated push from the
# monorepo's tag-driven publish workflow).
#
# Until that auto-sync is wired up, we whitelist the historical
# pusher identities so existing in-flight PRs don't all turn red.
# Whitelist removal becomes a follow-up once the auto-sync lands.
on:
push:
branches: [main]
branches: [main, staging]
pull_request:
permissions:
contents: read
jobs:
test:
mirror-guard:
runs-on: ubuntu-latest
env:
# Required by platform_auth.validate_workspace_id() (PR #29 / issue #14).
# Valid format: lowercase alphanumeric + hyphens (matches UUIDs and org IDs).
WORKSPACE_ID: ci-test-workspace
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install package + test deps
- name: Reject direct edits
env:
PR_AUTHOR: ${{ github.event.pull_request.user.login || github.actor }}
run: |
pip install -e .
pip install pytest
- name: Run import smoke tests
# Critical: these tests run in an environment with NO top-level
# `adapters/` package on sys.path. They catch the regression that
# broke every modular workspace template repo before the absolute-
# import fix. Do not weaken — the failure mode (silent fallthrough
# in get_adapter → "Unknown runtime") is hard to debug at runtime.
run: pytest tests/ -v
- name: Security linter (bandit)
run: |
pip install bandit
bandit -r molecule_runtime/ --severity-level=high
# Allow the future bot author once it exists. Until then,
# block on PR events but allow push events (for in-flight
# work to land while the migration finishes).
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "::error::This repo is a publish artifact of Molecule-AI/molecule-core."
echo "::error::Edit workspace/ in the monorepo and let the publish-runtime"
echo "::error::workflow regenerate this mirror — do not PR here directly."
echo "::error::See README.md for the new contribution flow."
exit 1
fi
echo "Push event from $PR_AUTHOR — allowing while migration completes."

View File

@ -1,29 +0,0 @@
name: Publish to PyPI
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install build tools
run: pip install build twine
- name: Build package
run: python -m build
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: python -m twine upload dist/*

22
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,22 @@
# Contributing
**This repo is a publish artifact, not the source of truth.**
Runtime code lives in [`Molecule-AI/molecule-core`](https://github.com/Molecule-AI/molecule-core)
under the `workspace/` directory. This repo is regenerated by the
`publish-runtime` workflow on every `runtime-v*` tag.
## Where to send your change
| Want to … | Open PR against … |
|---|---|
| Add a new tool | `Molecule-AI/molecule-core``workspace/builtin_tools/` |
| Fix a bug in the runtime | `Molecule-AI/molecule-core``workspace/` |
| Add a new adapter | `Molecule-AI/molecule-ai-workspace-template-<runtime>` (separate repo per adapter) |
| Update this README or CONTRIBUTING | `Molecule-AI/molecule-core``workspace-runtime-readme.md` (sync-published from there) |
## What if you really need to edit this repo
You don't. Even hot-fixes go through the monorepo. If the monorepo path
is broken for some reason and you genuinely cannot wait, ping `#platform`
and an admin can override the mirror-guard CI.

View File

@ -1,10 +1,18 @@
# molecule-ai-workspace-runtime
> **⚠️ This repo is a publish artifact, not the source of truth.**
>
> Runtime code lives in **[`Molecule-AI/molecule-core` → `workspace/`](https://github.com/Molecule-AI/molecule-core/tree/main/workspace)**. This repo is regenerated and republished from there by the [`publish-runtime`](https://github.com/Molecule-AI/molecule-core/blob/main/.github/workflows/publish-runtime.yml) workflow on every `runtime-v*` tag.
>
> **Don't edit files here directly.** PRs against this repo will not be merged. Open them against `molecule-core` instead.
---
Shared Python runtime infrastructure for all Molecule AI agent adapters.
This package provides the core machinery that every Molecule AI workspace container needs:
This package provides the core machinery every Molecule AI workspace container needs:
- **A2A server** — Registers with the platform, heartbeats, serves A2A JSON-RPC
- **A2A server**registers with the platform, heartbeats, serves A2A JSON-RPC
- **Adapter interface**`BaseAdapter` / `AdapterConfig` / `SetupResult`
- **Built-in tools** — delegation, memory, approvals, sandbox, telemetry
- **Skill loader** — loads and hot-reloads skill modules from `/configs/skills/`
@ -17,49 +25,38 @@ This package provides the core machinery that every Molecule AI workspace contai
pip install molecule-ai-workspace-runtime
```
## Adapter Discovery
## Adapter discovery
The runtime discovers adapters in two ways:
1. **`ADAPTER_MODULE` env var** (standalone adapter repos):
```bash
ADAPTER_MODULE=my_adapter molecule-runtime
ADAPTER_MODULE=adapter molecule-runtime
```
The module must export an `Adapter` class extending `BaseAdapter`.
The runtime imports `adapter` and calls `adapter.Adapter`.
2. **Built-in subdirectory scan** (monorepo local dev):
Scans `molecule_runtime/adapters/` subdirectories for `Adapter` classes.
2. **Subdirectory scan** (monorepo local dev): falls back to scanning
`molecule_runtime/adapters/<runtime>/` and importing the matching
subdir's `Adapter` class.
## Writing an Adapter
## Contributing
```python
from molecule_runtime.adapters.base import BaseAdapter, AdapterConfig
from a2a.server.agent_execution import AgentExecutor
**Don't open PRs here.** Send your change to
[`Molecule-AI/molecule-core`](https://github.com/Molecule-AI/molecule-core)
under the `workspace/` directory. After your PR merges to main and a
`runtime-v*` tag is pushed, the [`publish-runtime`](https://github.com/Molecule-AI/molecule-core/blob/main/.github/workflows/publish-runtime.yml)
workflow rebuilds this mirror + uploads the new wheel to PyPI.
class Adapter(BaseAdapter):
@staticmethod
def name() -> str:
return "my-runtime"
See [`docs/workspace-runtime-package.md`](https://github.com/Molecule-AI/molecule-core/blob/main/docs/workspace-runtime-package.md)
for the full publishing flow.
@staticmethod
def display_name() -> str:
return "My Runtime"
## Why this split
@staticmethod
def description() -> str:
return "My custom agent runtime"
The runtime needs to ship as a PyPI artifact (so the 8 workspace template
images can `pip install` it), but it also needs to evolve in lock-step
with the platform's wire protocol (queue shape, A2A metadata, event
payloads). A monorepo edit + auto-publish pipeline gives both: atomic
cross-cutting changes, plus a clean PyPI release on every tag.
async def setup(self, config: AdapterConfig) -> None:
result = await self._common_setup(config)
# Store result attributes for create_executor
async def create_executor(self, config: AdapterConfig) -> AgentExecutor:
# Return an AgentExecutor instance
...
```
Set `ADAPTER_MODULE=my_package.adapter` and run `molecule-runtime`.
## License
BSL-1.1 — see LICENSE for details.
For the back-history of why this repo previously was the source of truth
and the drift that caused: see issue [`Molecule-AI/molecule-core#2103`](https://github.com/Molecule-AI/molecule-core/pull/2103).