Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 87 additions & 32 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Release

# Builds the package, gates it on the license/boundary check, and — ONLY on
# a version tag or a published GitHub Release — publishes to PyPI and to the
# official MCP registry. A normal push or PR runs the validate job only
# a version tag push — publishes to PyPI and to the
# official MCP registry. A pull request or manual run starts validation only
# (dry run, no publish), so the workflow itself is testable without shipping.
#
# Auth is OIDC-first and secret-free for the io.github.OpenAdaptAI namespace:
Expand All @@ -15,8 +15,6 @@ name: Release
on:
push:
tags: ["v*"]
release:
types: [published]
workflow_dispatch:
pull_request:
paths:
Expand All @@ -27,9 +25,13 @@ on:
- "src/openadapt_agent/mcpb_entry.py"
- "scripts/check_mcpb.py"
- "scripts/check_release_artifacts.py"
- "scripts/validate_server_schema.py"
- "scripts/verify_release_registries.py"
- "schemas/production-lifecycle-admission-candidate.schema.json"
- "scripts/check_dist.py"
- "scripts/check_source_boundary.py"
- "source-policy.public.json"
- "tests/test_release_registries.py"
- ".github/workflows/release.yml"

concurrency:
Expand Down Expand Up @@ -57,7 +59,7 @@ jobs:
python -m pip install --upgrade build "twine>=6.1" "packaging>=24.2" jsonschema
python -m pip install -e ".[dev]"
- name: Version-consistency guard (registry / MCPB / package / runtime)
run: python -m pytest tests/test_distribution.py -q
run: python -m pytest tests/test_distribution.py tests/test_release_registries.py -q
- name: Build sdist + wheel
run: python -m build
- name: License and source-policy checks on built archives
Expand All @@ -77,22 +79,7 @@ jobs:
npx -y @anthropic-ai/mcpb@2.1.2 pack . "mcpb-dist/openadapt-agent-${version}.mcpb"
python scripts/check_mcpb.py mcpb-dist/*.mcpb
- name: Validate server.json against the MCP registry schema
run: |
python - <<'PY'
import json, sys, urllib.request
from jsonschema import Draft202012Validator
doc = json.load(open("server.json"))
url = doc["$schema"]
try:
schema = json.load(urllib.request.urlopen(url, timeout=30))
except Exception as exc: # network hiccup: don't fail the dry run
print(f"WARNING: could not fetch {url}: {exc}; skipping live schema check")
sys.exit(0)
errors = sorted(Draft202012Validator(schema).iter_errors(doc), key=lambda e: list(e.path))
for e in errors:
print("SCHEMA ERROR:", list(e.path), e.message)
sys.exit(1 if errors else 0)
PY
run: python scripts/validate_server_schema.py --server-json server.json
- name: Upload built distributions
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand All @@ -109,7 +96,7 @@ jobs:
pypi-publish:
name: Publish to PyPI
needs: validate
if: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || github.event_name == 'release' }}
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
runs-on: ubuntu-latest
environment: pypi
permissions:
Expand All @@ -122,11 +109,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "release" ]; then
tag="${{ github.event.release.tag_name }}"
else
tag="${GITHUB_REF#refs/tags/}"
fi
tag="${GITHUB_REF#refs/tags/}"
tag_version="${tag#v}"
pkg_version="$(grep -m1 -E '^version = ' pyproject.toml | sed -E 's/version = "(.*)"/\1/')"
echo "version=${pkg_version}" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -168,7 +151,7 @@ jobs:
mcp-registry-publish:
name: Publish server.json to the MCP registry
needs: [validate, pypi-publish]
if: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || github.event_name == 'release' }}
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
runs-on: ubuntu-latest
permissions:
id-token: write # OIDC for `mcp-publisher login github-oidc`
Expand All @@ -180,8 +163,15 @@ jobs:
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_REF#refs/tags/}"
pkg_version="$(grep -m1 -E '^version = ' pyproject.toml | sed -E 's/version = "(.*)"/\1/')"
if [ "${tag#v}" != "${pkg_version}" ]; then
echo "::error::Release tag '${tag}' does not match package version '${pkg_version}'." >&2
exit 1
fi
echo "version=${pkg_version}" >> "$GITHUB_OUTPUT"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "source_commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Wait for the PyPI package to be installable
shell: bash
run: |
Expand All @@ -205,10 +195,15 @@ jobs:
shell: bash
run: |
set -euo pipefail
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
arch="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')"
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_${os}_${arch}.tar.gz" \
| tar xz mcp-publisher
publisher_version="1.8.1"
archive="mcp-publisher_linux_amd64.tar.gz"
expected_sha256="a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc"
curl --fail --location --retry 3 --retry-delay 2 \
--output "${archive}" \
"https://github.com/modelcontextprotocol/registry/releases/download/v${publisher_version}/${archive}"
printf '%s %s\n' "${expected_sha256}" "${archive}" | sha256sum --check --strict
tar xzf "${archive}" mcp-publisher
./mcp-publisher --version
- name: Fail loud if token method selected without a token
if: ${{ vars.MCP_PUBLISH_METHOD == 'token' }}
env:
Expand All @@ -228,3 +223,63 @@ jobs:
run: ./mcp-publisher login github --token "${{ secrets.MCP_GITHUB_TOKEN }}"
- name: Publish server.json
run: ./mcp-publisher publish

registry-parity:
name: Verify registries + upload admission handoff
needs: [validate, mcp-registry-publish]
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Resolve the exact release identity
id: ver
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_REF#refs/tags/}"
pkg_version="$(grep -m1 -E '^version = ' pyproject.toml | sed -E 's/version = "(.*)"/\1/')"
if [ "${tag#v}" != "${pkg_version}" ]; then
echo "::error::Release tag '${tag}' does not match package version '${pkg_version}'." >&2
exit 1
fi
echo "version=${pkg_version}" >> "$GITHUB_OUTPUT"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "source_commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Download the exact published Python distributions
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist/
- name: Checkout the canonical lifecycle policy
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: OpenAdaptAI/.github
ref: main
path: .production-lifecycle
persist-credentials: false
- name: Verify PyPI and MCP registry parity; write an unadmitted candidate
shell: bash
run: |
set -euo pipefail
lifecycle_commit="$(git -C .production-lifecycle rev-parse HEAD)"
python scripts/verify_release_registries.py \
--dist dist \
--server-json server.json \
--version "${{ steps.ver.outputs.version }}" \
--tag "${{ steps.ver.outputs.tag }}" \
--source-commit "${{ steps.ver.outputs.source_commit }}" \
--lifecycle-policy .production-lifecycle/production-lifecycle-policy.json \
--lifecycle-source-commit "${lifecycle_commit}" \
--output release-metadata/production-admission-candidate.json \
--attempts 20 \
--retry-seconds 15
sha256sum release-metadata/production-admission-candidate.json
- name: Upload the bounded admission-candidate handoff
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: production-admission-candidate-${{ steps.ver.outputs.version }}
path: release-metadata/production-admission-candidate.json
if-no-files-found: error
retention-days: 30
1 change: 1 addition & 0 deletions .mcpbignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ build/
dist/
docs/
scripts/
schemas/
tests/
*.egg-info/
**/__pycache__/
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# OpenAdapt Agent

[![Lifecycle: Beta](https://img.shields.io/badge/lifecycle-Beta-2563eb)](https://github.com/OpenAdaptAI/openadapt-agent)
[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
[![Python 3.10–3.12](https://img.shields.io/badge/python-3.10%E2%80%933.12-blue)](https://www.python.org/downloads/)

Expand Down Expand Up @@ -276,8 +275,8 @@ Registry-launched installs start **read-only by default**; execution
tools are registered only when the operator adds `--allow-run`.

Publishing is automated: [`.github/workflows/release.yml`](.github/workflows/release.yml)
builds, runs the license/boundary gate, and — only on a `vX.Y.Z` tag or a
published Release — ships to PyPI (Trusted Publishing, OIDC) and the MCP
builds, runs the license/boundary gate, and — only on a pushed `vX.Y.Z` tag
ships to PyPI (Trusted Publishing, OIDC) and the MCP
registry (`mcp-publisher login github-oidc`), secret-free. It runs a dry
run (no publish) on PRs and manual dispatch. See
[`docs/DISTRIBUTION.md`](docs/DISTRIBUTION.md) for the one-time founder
Expand Down
2 changes: 0 additions & 2 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# OpenAdapt Agent design

**Lifecycle: Beta.**

`openadapt-agent` is the local agent-facing bridge for compiled
`openadapt-flow` workflows. It exposes two complementary interfaces:

Expand Down
52 changes: 38 additions & 14 deletions docs/DISTRIBUTION.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Distribution & discoverability — openadapt-agent

**Status: Beta (v2).** This document describes how the package is
This document describes how the package is
made installable and discoverable as an MCP server, the security-relevant
distinction between the *public capability* and a *user's private
bundle*, and the exact founder-owned steps to publish and list it. The
Expand Down Expand Up @@ -55,8 +55,8 @@ and [`../manifest.json`](../manifest.json).
- **Name (reverse-DNS, official registry):** `io.github.OpenAdaptAI/openadapt-agent`
- **Display name:** OpenAdapt Agent (openadapt-flow bridge)
- **PyPI package:** `openadapt-agent`
- **Version:** `2.0.2` (Beta)
- **Description:** Local Beta bridge for governed openadapt-flow workflows and attended actions.
- **Version:** `2.0.2`
- **Description:** Local bridge for governed openadapt-flow workflows and attended actions.
- **Homepage / docs:** https://docs.openadapt.ai
- **Repository:** https://github.com/OpenAdaptAI/openadapt-agent
- **License:** MIT
Expand All @@ -79,37 +79,61 @@ remains for the founder is a small set of **one-time identity/config
actions** (create accounts, enable Trusted Publishing, claim directory
listings) that mint public first-party identities and therefore stay
founder-authorized. Nothing publishes to a public index automatically
except on a deliberate version tag / GitHub Release.
except on a deliberate version-tag push.

### 3.0 What the workflow does (AUTOMATED)

| Trigger | Jobs that run | Publishes? |
| --- | --- | --- |
| Pull request touching release files, or `workflow_dispatch`, or any tag/release | `validate` (Python build, MCPB build, archive boundary checks, `twine check`, registry schema validation) | **No** — dry run |
| Push a `vX.Y.Z` tag, or publish a GitHub Release | `validate` -> `pypi-publish` -> `mcp-registry-publish` | **Yes** |
| Pull request touching release files, or `workflow_dispatch` | `validate` (Python build, MCPB build, archive boundary checks, `twine check`, registry schema validation) | **No** — dry run |
| Push a `vX.Y.Z` tag | `validate` -> `pypi-publish` -> `mcp-registry-publish` -> `registry-parity` | **Yes** |

- **`validate`** builds the sdist+wheel and local MCPB, runs
[`scripts/check_release_artifacts.py`](../scripts/check_release_artifacts.py)
(fails if a wheel/sdist carries a bundle, `.enc`, run outputs, keys, or
any non-code payload — the license/boundary gate), checks the MCPB for
workflow/evidence payloads, runs `twine check`, validates `server.json`
against its live schema, and runs the
against the exact MCP schema at the pinned `2025-12-11` URL and SHA-256
`3fba09590c99f61735d234822279f4223fab9e300c0a81e81c91ab62a4114de0`,
and runs the
version-consistency guard (`tests/test_distribution.py`). It runs on PRs
and manual dispatch so the pipeline is testable **without** publishing.
- **`pypi-publish`** (tag/release only) asserts the tag matches the
An unavailable schema or changed schema bytes fail validation before any
publisher can run.
- **`pypi-publish`** (tag push only) asserts the tag matches the
package version, then uploads via **PyPI Trusted Publishing (OIDC)** —
no long-lived token. Runs in the `pypi` GitHub environment (add required
reviewers there if you want a human approval gate on every publish).
- **`mcp-registry-publish`** (tag/release only) waits for the new version
- **`mcp-registry-publish`** (tag push only) waits for the new version
to be live on PyPI (the registry validates package existence), then
`mcp-publisher login github-oidc` + `mcp-publisher publish` — no token,
because the repo lives under the `OpenAdaptAI` org that owns the
`io.github.OpenAdaptAI` namespace.
`io.github.OpenAdaptAI` namespace. The workflow downloads a fixed
`mcp-publisher` version and verifies its SHA-256 before execution.
- **Registry parity and admission input** run after both publishes. The
workflow downloads each PyPI artifact and proves byte-for-byte equality
with the archives produced by the protected build. It also proves that the
exact and `latest` MCP registry records equal the reviewed `server.json`.
Only then does it upload a 30-day
`production-admission-candidate-<version>` handoff artifact. That record
binds the source commit, artifact hashes, both public registry observations,
and one exact commit of the canonical lifecycle policy. The Actions artifact
is a bounded transport copy. It is not the long-term evidence record. Before
activation, the central admission process must verify its exact digest and
copy it with the supporting release evidence into the immutable evidence
store. If that handoff expires before the copy, registry parity must run
again.

The candidate record is explicitly `not_admitted`. It has no admission ID,
release sequence, or Production channel selector. PyPI `latest` and MCP
`latest` are distribution checks only. They never grant Production status.
The active, signed ledger in `OpenAdaptAI/.github` is the sole Production
authority and requires its separate acceptance evidence and activation.

To cut a release: bump the synchronized version fields (see §3.1), merge, then
`git tag vX.Y.Z && git push origin vX.Y.Z` (or publish a Release with that
tag). The first tag you push IS the first real publish — do it
deliberately.
use the release App to create and push `vX.Y.Z`. The tag push is the one
authoritative publish event. Creating or publishing the GitHub Release does not
start another publisher run.

### 3.a Required repo configuration and secrets (FOUNDER, one-time)

Expand Down Expand Up @@ -150,7 +174,7 @@ both `version` fields in `server.json`, and `manifest.json` `version`. The

### 3.2 Publish to PyPI — AUTOMATED (`pypi-publish` job)

Fires automatically on a `vX.Y.Z` tag / Release once §3.a step 1-2 are
Fires automatically on a pushed `vX.Y.Z` tag once §3.a step 1-2 are
done. To reproduce locally (dry run or a manual emergency publish):

```bash
Expand Down
2 changes: 1 addition & 1 deletion llms.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# openadapt-agent

> The local agent bridge for [openadapt-flow](https://github.com/OpenAdaptAI/openadapt-flow). It exposes compiled workflows and PHI-safe Needs Attention items to MCP clients and emits Agent Skills without becoming a second automation runtime. Healthy execution uses Flow's governed `run` command; a halted or refused run is returned as that exact non-success outcome. Flow remains the authority for policy, identity, verification, durable pauses, attended decisions, repair, and audit. Status: Beta (v2).
> The local agent bridge for [openadapt-flow](https://github.com/OpenAdaptAI/openadapt-flow). It exposes compiled workflows and PHI-safe Needs Attention items to MCP clients and emits Agent Skills without becoming a second automation runtime. Healthy execution uses Flow's governed `run` command; a halted or refused run is returned as that exact non-success outcome. Flow remains the authority for policy, identity, verification, durable pauses, attended decisions, repair, and audit. Production status comes only from the active signed OpenAdapt release admission.

## What it provides

Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ maintainers = [
{name = "OpenAdaptAI", email = "contact@openadapt.ai"}
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
Expand Down
Loading