Independent post-merge review of #19 surfaced two more findings. Both shipped here. Q3 — abstract intermediates + multiple-concrete-classes. The class-discovery filter from O1 (#19) only excluded BaseAdapter itself. Two failure modes slipped through: (a) A locally-defined abstract intermediate `class FrameworkAdapter(BaseAdapter): @abstractmethod ...` passed the filter, falsely satisfying "at least one concrete subclass" while still being non-instantiable at workspace boot. (b) A template defining BOTH `class FrameworkAdapter(BaseAdapter)` AND `class ConcreteAdapter(FrameworkAdapter)` had both pass the filter, producing a silent ambiguity where the runtime's class-discovery picks one per its resolution rules — wrong class loaded after a future runtime refactor. Fixes: - Add `not inspect.isabstract(obj)` to the discovery filter so abstract intermediates are excluded. - Hard-error if `len(adapter_classes) > 1` listing both names so the contributor knows exactly which classes are competing. Three new tests pin the behaviors: - test_abstract_intermediate_alone_does_not_count - test_abstract_plus_concrete_passes_with_concrete_only - test_multiple_concrete_baseadapter_subclasses_errors Identity-based deduplication. Caught against the real langgraph template during smoke-testing the Q3 fix: production adapters often do `Adapter = ConcreteAdapter` as a module-level alias for the runtime's discovery convention. `vars(mod)` returns BOTH bindings pointing at the same class object, so the new multiple-concrete-classes error fired falsely on every aliased template. Fix: deduplicate by `id(obj)` BEFORE counting, so the same class object under multiple bindings counts once. New regression test test_aliased_concrete_class_is_deduplicated pins this against any future filter regression. Existing tests updated to use fully-concrete BaseAdapter subclasses (matching production templates) since the new abstract-filter correctly rejects partial stubs that don't override every abstract method BaseAdapter declares (5 methods: name, display_name, description, setup, create_executor). Q5 — GITHUB_TOKEN scope lockdown. validate-workspace-template.yml runs untrusted-by-design code from the calling template repo: pip post-install hooks, adapter.py imports, Dockerfile RUN steps. Each of those primitives executes with GITHUB_TOKEN in env. The workflow had no `permissions:` block, defaulting to whatever the calling repo grants — often contents: write. Add `permissions: contents: read` at the workflow level. Worst- case-with-token now drops to "read public repo state" — no write to issues, no push to branches, no comment-spam, no workflow re-trigger. Partial mitigation; the deeper `pull_request_target` discipline is bigger scope (tracked separately). Verification: - 47/47 tests pass (was 43; +3 abstract/multi-concrete + +1 alias) - All 8 production templates pass the full updated validator end-to-end with 0 warnings / 0 errors Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .github/workflows | ||
| .molecule-ci/scripts | ||
| docs | ||
| scripts | ||
| .gitignore | ||
| README.md | ||
molecule-ci
Shared CI workflows for the Molecule AI ecosystem. Every plugin, workspace template, and org template repo calls these reusable workflows to enforce a standard validation gate.
Usage
Plugin repos (molecule-ai-plugin-*)
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
validate:
uses: Molecule-AI/molecule-ci/.github/workflows/validate-plugin.yml@main
Workspace template repos (molecule-ai-workspace-template-*)
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
validate:
uses: Molecule-AI/molecule-ci/.github/workflows/validate-workspace-template.yml@main
Org template repos (molecule-ai-org-template-*)
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
validate:
uses: Molecule-AI/molecule-ci/.github/workflows/validate-org-template.yml@main
Any repo with auto-merge enabled
PR-time guards (currently: disable auto-merge on follow-up push). Consume from a thin caller:
# .github/workflows/pr-guards.yml
name: pr-guards
on:
pull_request:
types: [synchronize]
permissions:
pull-requests: write
jobs:
disable-auto-merge-on-push:
uses: Molecule-AI/molecule-ci/.github/workflows/disable-auto-merge-on-push.yml@main
When the team lands more PR-time guards in this repo, add them as additional jobs in the same caller — keeps each consuming repo's footprint to one file.
What each workflow validates
validate-plugin
| Check | Severity | What it catches |
|---|---|---|
plugin.yaml exists |
Error | Missing manifest |
| Required fields (name, version, description) | Error | Incomplete plugin |
| Has content (SKILL.md, hooks/, skills/, or rules/) | Error | Empty plugin |
| SKILL.md starts with heading | Warning | Bad formatting |
| No committed secrets | Error | Leaked API keys |
| No build artifacts | Error | node_modules, pycache |
validate-workspace-template
| Check | Severity | What it catches |
|---|---|---|
config.yaml exists |
Error | Missing config |
| Required fields (name, runtime) | Error | Incomplete template |
template_schema_version: 1 |
Error | Missing version contract |
| Known runtime check | Warning | Typo in runtime name |
adapter.py imports molecule_runtime |
Warning | Legacy imports |
| Dockerfile builds | Error | Broken image |
| molecule-ai-workspace-runtime dependency | Warning | Missing base package |
| No committed secrets | Error | Leaked API keys |
validate-org-template
| Check | Severity | What it catches |
|---|---|---|
org.yaml exists |
Error | Missing org definition |
| Required fields (name) | Error | Incomplete template |
| Workspace structure valid | Error | Malformed hierarchy |
files_dir references exist |
Warning | Broken system-prompt paths |
template_schema_version present |
Warning | Missing version contract |
| No committed secrets | Error | Leaked API keys |
disable-auto-merge-on-push
PR-time safety guard. When pull_request:synchronize fires (= a new commit pushed to an open PR) and auto-merge is already enabled, this workflow disables auto-merge and posts a comment requiring the operator to re-engage explicitly.
Why it exists: on 2026-04-27, molecule-core PR #2174 auto-merged with only its first commit because the second commit was pushed AFTER the merge queue had locked the PR's SHA. The second commit ended up orphaned on a merged-and-deleted branch.
Pairs with the org-wide repo setting "Automatically delete head branches" (already enabled on all 10 Molecule-AI repos). Defense in depth:
- Repo setting blocks pushes to a merged-and-deleted branch (catches the post-merge orphan case).
- This workflow catches the in-queue race (push during queue processing) by force-disabling auto-merge.
Together they cover the full lifecycle of "auto-merge enabled → new commits arrive" without operator discipline.
False-positive note: if a CI bot pushes (dependency update, secret rotation), this also disables auto-merge. That's intentional — the operator who originally enabled auto-merge gets notified and re-engages, which is exactly the verify-after-machine-edits behavior we want.
License
Business Source License 1.1 — © Molecule AI.