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:
parent
7fc3537b2f
commit
96864263bb
119
.github/workflows/auto-promote-staging.yml
vendored
119
.github/workflows/auto-promote-staging.yml
vendored
@ -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)"
|
||||
61
.github/workflows/ci.yml
vendored
61
.github/workflows/ci.yml
vendored
@ -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."
|
||||
|
||||
29
.github/workflows/publish.yml
vendored
29
.github/workflows/publish.yml
vendored
@ -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
22
CONTRIBUTING.md
Normal 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.
|
||||
67
README.md
67
README.md
@ -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).
|
||||
|
||||
Loading…
Reference in New Issue
Block a user