diff --git a/.github/workflows/auto-promote-staging.yml b/.github/workflows/auto-promote-staging.yml deleted file mode 100644 index 646e861..0000000 --- a/.github/workflows/auto-promote-staging.yml +++ /dev/null @@ -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)" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4cf559..69d5aaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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." diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 0bdeae0..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -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/* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d8d7d00 --- /dev/null +++ b/CONTRIBUTING.md @@ -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-` (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. diff --git a/README.md b/README.md index 5a9e468..14c5cc1 100644 --- a/README.md +++ b/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//` 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).