Compare commits

...

10 Commits

Author SHA1 Message Date
6963378a89 Merge pull request 'fix(pypi): swap OIDC trusted-publisher for twine + PYPI_TOKEN; port .github -> .gitea' (#6) from fix/pypi-gitea-twine-no-oidc into main
Some checks are pending
CI / test (3.11) (push) Successful in 3m7s
CI / test (3.12) (push) Successful in 3m10s
Publish to PyPI / publish (push) Blocked by required conditions
Publish to PyPI / build (push) Successful in 3m14s
2026-05-16 00:17:42 +00:00
289c65603e fix(pypi): swap OIDC trusted-publisher for twine + PYPI_TOKEN; port .github -> .gitea
All checks were successful
CI / test (3.11) (pull_request) Successful in 2m55s
CI / test (3.12) (pull_request) Successful in 2m59s
Post-2026-05-06 PyPI Trusted-Publisher OIDC is dead for our repos — PyPI
only accepts GitHub/GitLab/Google/ActiveState issuers, not Gitea. This PR:

1. Renames .github/workflows/{ci,publish}.yml -> .gitea/workflows/. (Gitea
   Actions reads .gitea/ exclusively on this repo; the .github/ path was
   silently dead since the migration — saved memory
   reference_molecule_core_actions_gitea_only.)

2. Replaces `pypa/gh-action-pypi-publish` (which requires OIDC id-token
   exchange that PyPI rejects from Gitea) with `python -m twine upload
   --username __token__ --password "$PYPI_TOKEN"`. Mirrors the canonical
   pattern in molecule-core/.gitea/workflows/publish-runtime.yml that has
   been shipping successfully since 2026-05-11.

3. Drops `permissions: id-token: write` (no longer needed without OIDC).

4. Adds `twine check` to the build step (catches metadata regressions
   before upload).

5. Adds concurrency group to serialize tag-driven publishes.

6. Updates README "Releasing" section to describe the twine+SSOT model
   and link to the operator-config rotation runbook.

The PYPI_TOKEN secret is fanned out to this repo from the operator-host
SSOT by /opt/molecule-bootstrap/sync-pypi-token.sh — see operator-config
PR#48. It supersedes PR#4 (which only renamed .github -> .gitea without
fixing the OIDC issue) and unblocks the pushed v0.1.3 tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:59:26 -07:00
d57392df4d fix(deps): raise molecule-ai-workspace-runtime floor to >=0.1.129 (#5)
Some checks failed
CI / test (3.11) (push) Successful in 2m57s
CI / test (3.12) (push) Successful in 3m30s
Publish to PyPI / build (push) Successful in 3m43s
Publish to PyPI / publish (push) Failing after 1m1s
Closes molecule-ai/internal#424.
2026-05-15 22:54:24 +00:00
06739ba9e0 fix(deps): raise molecule-ai-workspace-runtime floor to >=0.1.129
All checks were successful
CI / test (3.11) (pull_request) Successful in 2m10s
CI / test (3.12) (pull_request) Successful in 2m10s
Pre-0.1.129 the runtime's inline A2A response sniffer in
``send_a2a_message`` checked only for ``result`` / ``error`` keys and
routed the poll-mode success envelope —
``{"status": "queued", "delivery_mode": "poll", "method": "..."}`` —
to the malformed branch:

    [A2A_ERROR] unexpected response shape (no result, no error): {...}

That string then propagated through ``tool_delegate_task`` into the
caller workspace's activity row, surfacing on canvas as the workspace's
task label and triggering a ~3s retry storm.

0.1.129 introduced ``a2a_response.py`` (SSOT typed parser with explicit
``Queued`` variant) + matching ``_A2A_QUEUED_PREFIX`` handling in
``a2a_tools_delegation.tool_delegate_task`` that falls back to the
durable ``/delegate`` + ``/delegations`` polling path — the correct
synchronous facade for poll-mode peers.

The existing ``>=0.1.110`` pin allowed a fresh ``pip install
codex-channel-molecule`` to resolve to a buggy wheel and reproduce the
incident. Raising the floor to 0.1.129 closes that window.

Adds a ``test_runtime_dependency_floor.py`` regression test that
parses ``pyproject.toml`` and asserts the lower bound stays at or
above 0.1.129 — bumps the version to 0.1.3 so a republish carries the
floor downstream.

Closes molecule-ai/internal#424.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:44:25 -07:00
b29ce85137 Merge pull request 'fix(post-suspension): migrate github.com/Molecule-AI refs to git.moleculesai.app (Class G #168)' (#3) from fix/post-suspension-github-urls into main
All checks were successful
CI / test (3.11) (push) Successful in 2m26s
CI / test (3.12) (push) Successful in 2m36s
2026-05-07 20:02:45 +00:00
4a8cd3648f fix(post-suspension): migrate github.com/Molecule-AI refs to git.moleculesai.app (Class G #168)
All checks were successful
CI / test (3.11) (pull_request) Successful in 2m43s
CI / test (3.12) (pull_request) Successful in 2m44s
The GitHub org Molecule-AI was suspended on 2026-05-06; canonical SCM
is now Gitea at https://git.moleculesai.app/molecule-ai/. Stale
github.com/Molecule-AI/... URLs return 404 and break tooling that
clones / pip-installs / curls them.

This bundles all non-Go-module URL fixes for this repo into a single PR.
Go module path references (in *.go, go.mod, go.sum) are out of scope
here -- tracked separately under Task #140.

Token-auth clone URLs also flip ${GITHUB_TOKEN} -> ${GITEA_TOKEN} since
the GitHub token does not auth against Gitea.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:02:40 -07:00
security-auditor
d59d1f15ac ci: re-trigger after runner-config v2 (CONFIG_FILE fix)
All checks were successful
CI / test (3.11) (push) Successful in 29s
CI / test (3.12) (push) Successful in 36s
Verify whether failure was setup-python toolcache class (now fixed via
orchestrator's runners-1-8 recreate) or real CODE class.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:57:23 -07:00
a4b3109e49 Merge pull request 'chore(ci): pin artifact actions to @v3 for Gitea act_runner compatibility' (#2) from chore/pin-artifact-actions-v3 into main
Some checks failed
CI / test (3.11) (push) Failing after 10s
CI / test (3.12) (push) Failing after 10s
2026-05-07 08:18:14 +00:00
cdf0892b2e chore(ci): pin artifact actions to @v3 for Gitea act_runner compatibility (internal#46)
Some checks failed
CI / test (3.11) (pull_request) Failing after 11s
CI / test (3.12) (pull_request) Failing after 11s
2 uses pinned in .github/workflows/publish.yml (1 upload at line 52, 1
download at line 64). v4 relies on a runtime API shape Gitea's act_runner
v0.6.x doesn't fully support; v3 works end-to-end. YAML parse green.

Sister PRs in molecule-controlplane (#18) and molecule-core (#18). Per
internal#46 Phase 2 audit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:11:34 -07:00
2f7f7a36c8 Merge pull request 'docs(install): migrate github.com refs to git.moleculesai.app (#37)' (#1) from fix/install-path-gitea into main
Some checks failed
CI / test (3.11) (push) Failing after 11s
CI / test (3.12) (push) Failing after 12s
2026-05-07 06:26:33 +00:00
6 changed files with 221 additions and 85 deletions

View File

@ -0,0 +1,95 @@
name: Publish to PyPI
# Triggered on tag push (vX.Y.Z). Tag-on-push instead of release-creation
# is the cheaper UX — `git tag v0.1.0 && git push origin v0.1.0` ships
# without leaving the terminal.
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
# Post-2026-05-06 (Molecule-AI GitHub org suspension): PyPI's Trusted
# Publisher OIDC flow only accepts GitHub/GitLab/Google/ActiveState
# issuers — not Gitea. This workflow uses a long-lived PyPI API token
# stored as the repo-level secret PYPI_TOKEN, fanned out from the
# operator-host SSOT (/etc/molecule-bootstrap/all-credentials.env) by
# /opt/molecule-bootstrap/sync-pypi-token.sh.
permissions:
contents: read
# Serialize tag-driven publishes so two concurrent tag pushes don't both
# try to upload the same version and race PyPI.
concurrency:
group: publish-pypi
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Verify tag matches pyproject version
run: |
tag="${GITHUB_REF#refs/tags/v}"
pkg=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
if [ "$tag" != "$pkg" ]; then
echo "::error::tag $tag does not match pyproject version $pkg — aborting publish to keep PyPI in sync with git tags"
exit 1
fi
- name: Build sdist + wheel
run: |
python -m pip install --upgrade pip build twine
python -m build
- name: Smoke-import the built wheel
run: |
python -m venv /tmp/install-test
/tmp/install-test/bin/pip install dist/*.whl
/tmp/install-test/bin/codex-channel-molecule --help
- name: Verify package metadata (twine check)
run: python -m twine check dist/*
- uses: actions/upload-artifact@v3 # pinned to v3 for Gitea act_runner v0.6 compatibility (internal#46)
with:
name: dist
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3 # pinned to v3 for Gitea act_runner v0.6 compatibility (internal#46)
with:
name: dist
path: dist/
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Publish to PyPI
# PYPI_TOKEN: repo-level Gitea Actions secret, written by
# /opt/molecule-bootstrap/sync-pypi-token.sh from the operator-host
# SSOT. Never set this by hand — rotate via the SSOT instead
# (ops/PYPI_TOKEN_ROTATION.md in operator-config).
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
if [ -z "$PYPI_TOKEN" ]; then
echo "::error::PYPI_TOKEN secret is not set. Run sync-pypi-token.sh on the operator host to fan it out from SSOT."
exit 1
fi
python -m pip install --upgrade twine
python -m twine upload \
--repository pypi \
--username __token__ \
--password "$PYPI_TOKEN" \
dist/*

View File

@ -1,69 +0,0 @@
name: Publish to PyPI
# Triggered on tag push (vX.Y.Z). Tag-on-push instead of release-creation
# is the cheaper UX — `git tag v0.1.0 && git push origin v0.1.0` ships
# without leaving the terminal.
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
permissions:
contents: read
# OIDC token for PyPI trusted-publisher auth — no secret token needed.
# PyPI side: register
# github.com/Molecule-AI/codex-channel-molecule
# workflow=publish.yml environment=pypi
# under "Trusted publisher management" on the codex-channel-molecule
# PyPI project page (see README "Releasing" section).
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Verify tag matches pyproject version
run: |
tag="${GITHUB_REF#refs/tags/v}"
pkg=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
if [ "$tag" != "$pkg" ]; then
echo "::error::tag $tag does not match pyproject version $pkg — aborting publish to keep PyPI in sync with git tags"
exit 1
fi
- name: Build sdist + wheel
run: |
python -m pip install --upgrade pip build
python -m build
- name: Smoke-import the built wheel
run: |
python -m venv /tmp/install-test
/tmp/install-test/bin/pip install dist/*.whl
/tmp/install-test/bin/codex-channel-molecule --help
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1

View File

@ -80,7 +80,7 @@ Tests are entirely real-subprocess (no mocking the spawn boundary) so the boot p
## Releasing ## Releasing
Tag-on-push triggers `publish.yml` which builds + publishes to PyPI via OIDC trusted publishing (no API token needed). Tag-on-push triggers `.gitea/workflows/publish.yml` which builds + publishes to PyPI via `twine upload` using the `PYPI_TOKEN` repo-level Gitea Actions secret.
```sh ```sh
# Bump pyproject.toml `version`, commit, then: # Bump pyproject.toml `version`, commit, then:
@ -89,19 +89,11 @@ git tag v0.1.1 && git push origin v0.1.1
The workflow refuses to publish if the tag doesn't match `pyproject.toml`'s `version` — keeps PyPI versions and git tags in lockstep. The workflow refuses to publish if the tag doesn't match `pyproject.toml`'s `version` — keeps PyPI versions and git tags in lockstep.
**One-time PyPI setup** (before the first release): ### Why twine, not Trusted Publisher OIDC
1. Create the project on PyPI by uploading the first wheel manually, OR Post-2026-05-06 (Molecule-AI GitHub-org suspension) the canonical SCM is Gitea. PyPI's Trusted-Publisher OIDC flow only recognises GitHub / GitLab / Google / ActiveState issuers — not Gitea — so this repo (and every other PyPI-publishing repo in `molecule-ai/*`) falls back to a long-lived API token.
2. Pre-register the project on PyPI under a "Pending publisher" config so the first tagged push creates it.
Either way, on the project's PyPI page → "Manage" → "Publishing" → "Add a new publisher", configure: The `PYPI_TOKEN` secret is **not set by hand**. It is fanned out from the operator-host SSOT (`/etc/molecule-bootstrap/all-credentials.env`) by `/opt/molecule-bootstrap/sync-pypi-token.sh` (see [operator-config/etc/pypi-publishers.yaml](https://git.moleculesai.app/molecule-ai/operator-config/src/branch/main/etc/pypi-publishers.yaml)). Rotation procedure: [PYPI_TOKEN_ROTATION.md](https://git.moleculesai.app/molecule-ai/operator-config/src/branch/main/ops/PYPI_TOKEN_ROTATION.md).
- Owner: `Molecule-AI`
- Repository: `codex-channel-molecule`
- Workflow filename: `publish.yml`
- Environment name: `pypi`
After this, every `git push origin v*.*.*` ships the wheel to PyPI without any further intervention.
## License ## License

View File

@ -4,14 +4,26 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "codex-channel-molecule" name = "codex-channel-molecule"
version = "0.1.2" version = "0.1.3"
description = "Bridge daemon for codex CLI ↔ Molecule platform — long-polls the platform inbox, runs `codex exec --resume <session>` per inbound message, replies via send_message_to_user MCP tool. Counterpart to hermes-channel-molecule." description = "Bridge daemon for codex CLI ↔ Molecule platform — long-polls the platform inbox, runs `codex exec --resume <session>` per inbound message, replies via send_message_to_user MCP tool. Counterpart to hermes-channel-molecule."
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
license = { text = "Apache-2.0" } license = { text = "Apache-2.0" }
authors = [{ name = "Molecule AI" }] authors = [{ name = "Molecule AI" }]
dependencies = [ dependencies = [
"molecule-ai-workspace-runtime>=0.1.110", # Floor raised from 0.1.110 → 0.1.129 to pull in the SSOT A2A response
# parser (a2a_response.py, introduced 0.1.129). Pre-0.1.129 the legacy
# inline sniffer in a2a_client.send_a2a_message treated the poll-mode
# ``{"status": "queued", "delivery_mode": "poll", "method": "..."}``
# envelope as malformed and surfaced ``[A2A_ERROR] unexpected response
# shape (no result, no error): ...`` on every reply attempt — that
# then propagated to canvas as the workspace's task label and triggered
# a ~3s retry storm. The 0.1.129+ runtime classifies the envelope as
# ``Queued`` and short-circuits to the durable /delegate-poll path
# (a2a_tools_delegation._delegate_sync_via_polling). See
# ``molecule-ai/internal#424`` and ``molecule-core#2967`` for the
# full incident + fix history.
"molecule-ai-workspace-runtime>=0.1.129",
] ]
[project.optional-dependencies] [project.optional-dependencies]
@ -24,8 +36,8 @@ test = [
codex-channel-molecule = "codex_channel_molecule.daemon:main" codex-channel-molecule = "codex_channel_molecule.daemon:main"
[project.urls] [project.urls]
Homepage = "https://github.com/Molecule-AI/codex-channel-molecule" Homepage = "https://git.moleculesai.app/molecule-ai/codex-channel-molecule"
Repository = "https://github.com/Molecule-AI/codex-channel-molecule" Repository = "https://git.moleculesai.app/molecule-ai/codex-channel-molecule"
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["codex_channel_molecule*"] include = ["codex_channel_molecule*"]

View File

@ -0,0 +1,106 @@
"""Pin the minimum molecule-ai-workspace-runtime version that ships
the SSOT A2A response parser.
Background see ``molecule-ai/internal#424``:
Pre-runtime-0.1.129 the inline sniffer in ``a2a_client.send_a2a_message``
checked for ``result`` or ``error`` keys and routed everything else to
``[A2A_ERROR] unexpected response shape (no result, no error): ...``.
That branch fired on every reply attempt to a poll-mode peer because the
platform proxy synthesizes the success envelope as
``{"status": "queued", "delivery_mode": "poll", "method": "..."}``
which has NEITHER ``result`` NOR ``error``. The retry loop hammered the
caller's canvas with the error string every ~3s.
Version 0.1.129 introduced ``a2a_response.py`` a typed parser with an
explicit ``Queued`` variant and the matching ``[A2A_QUEUED]`` sentinel
in ``a2a_client.py``. ``a2a_tools_delegation.tool_delegate_task`` then
falls back to the durable ``/delegate`` + ``/delegations`` polling path,
which IS the correct synchronous facade for poll-mode peers.
This test asserts the floor is held at ``>=0.1.129`` so a future
dependency-housekeeping pass cannot silently lower it back into the
broken range. If the floor is ever raised further (e.g. to require a
later SSOT parser feature), update the constant below the lower bound
must never go below 0.1.129.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import pytest
if sys.version_info >= (3, 11):
import tomllib
else: # pragma: no cover - Python 3.11+ required by pyproject anyway
import tomli as tomllib
# The earliest runtime version that ships the SSOT A2A response parser
# with the typed ``Queued`` variant. Bumping this floor MUST be paired
# with a comment update in pyproject.toml's dependencies block.
_MINIMUM_RUNTIME_VERSION = (0, 1, 129)
def _parse_version(spec: str) -> tuple[int, int, int]:
"""Extract the lower-bound version from a dependency specifier.
Supports the common shapes used in our pyproject files:
* ``pkg>=1.2.3`` (1, 2, 3)
* ``pkg>=1.2.3,<2`` (1, 2, 3)
* ``pkg~=1.2.3`` (1, 2, 3)
Raises ``ValueError`` on anything else so the test fails loud rather
than silently passing on a malformed specifier.
"""
m = re.search(r"(?:>=|~=)\s*(\d+)\.(\d+)\.(\d+)", spec)
if not m:
raise ValueError(f"no lower-bound version in specifier: {spec!r}")
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
@pytest.fixture(scope="module")
def runtime_specifier() -> str:
"""Return the raw dependency specifier for molecule-ai-workspace-runtime."""
root = Path(__file__).resolve().parent.parent
pyproject = root / "pyproject.toml"
with pyproject.open("rb") as f:
data = tomllib.load(f)
deps = data["project"]["dependencies"]
for dep in deps:
# match the package name allowing a hyphen-or-underscore in case
# the spec ever normalizes — PEP 503 treats them equivalently.
if re.match(r"^molecule[-_]ai[-_]workspace[-_]runtime\b", dep):
return dep
raise AssertionError(
"pyproject.toml is missing a molecule-ai-workspace-runtime dependency"
)
def test_runtime_floor_includes_a2a_response_parser(runtime_specifier: str) -> None:
"""The runtime floor must be at or above the SSOT parser release."""
bound = _parse_version(runtime_specifier)
assert bound >= _MINIMUM_RUNTIME_VERSION, (
f"runtime floor {bound} is below {_MINIMUM_RUNTIME_VERSION}"
f"pre-0.1.129 the A2A response parser misclassifies the poll-mode "
f"queued envelope as malformed and surfaces "
f"'[A2A_ERROR] unexpected response shape' on every poll-mode peer "
f"reply. See molecule-ai/internal#424."
)
def test_runtime_specifier_uses_a_lower_bound(runtime_specifier: str) -> None:
"""A bare ``pkg`` or upper-only spec would silently install ANY version
on a fresh ``pip install`` including the buggy pre-0.1.129 range.
Require an explicit lower bound (``>=`` or ``~=``).
"""
assert re.search(r">=|~=", runtime_specifier), (
f"runtime dependency {runtime_specifier!r} has no lower bound — "
f"a fresh install could resolve to a pre-0.1.129 wheel with the "
f"broken poll-mode parser"
)