Skip to content
Merged
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
300 changes: 300 additions & 0 deletions .github/workflows/release-registry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
# Publish the skills registry: attest the index and the bundle archives.
#
# There is no signing key. The registry is attested keylessly, as the other
# releases are: actions/attest signs an in-toto statement naming each file's
# sha256 with a short-lived Sigstore certificate for this workflow's identity,
# and records it in Rekor. `openadapt-flow pull` checks that attestation
# against the pinned identity
# issuer https://token.actions.githubusercontent.com
# SAN https://github.com/OpenAdaptAI/OpenAdapt/.github/workflows/release-registry.yml@refs/tags/registry-*
# (openadapt_flow/registry_attest.py), offline, with the trusted root shipped
# in the package.
#
# - tag (manual dispatch from the tip of main) tags the commit registry-<suffix>
# with the openadapt-release App. The tag push starts this workflow again.
# - check (the tag push, no privileges) installs the engine and runs the
# registry's repository checks: no private-tier file and only allowlisted
# files in any bundle.
# - attest (the tag push, the protected release-identity environment, the only
# job with id-token: write) builds each bundle as a reproducible .tar.gz
# and an index stamped with issued_at, expires_at (30 days) and serial,
# attests the index and every archive, and checks its own attestation with
# the pull verifier. Its Python environment is only the hash-pinned wheels
# in packages/flow/registry/verify-requirements.txt; the engine is read from
# the checkout.
# - publish (release-identity) publishes a GitHub Release with the archives,
# the index and its attestation (index.sigstore.json), and opens a pull
# request that updates packages/flow/registry/ to them.
#
# The index expires 30 days after it is stamped, so run this at least monthly.
name: Release registry

on:
workflow_dispatch:
inputs:
suffix:
description: "Release tag suffix (the tag is registry-<suffix>), e.g. 2026-10-28"
required: true
type: string
push:
tags:
- "registry-*"

permissions:
contents: read

concurrency:
group: release-registry-${{ github.ref }}
cancel-in-progress: false

jobs:
tag:
name: tag registry-${{ inputs.suffix }}
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 10
environment: release-identity
permissions:
contents: read
steps:
- name: Require the tip of main and a plain suffix
env:
REF: ${{ github.ref }}
SUFFIX: ${{ inputs.suffix }}
run: |
set -euo pipefail
if [ "$REF" != "refs/heads/main" ]; then
echo "Releases run only from refs/heads/main, not $REF." >&2
exit 1
fi
if ! printf '%s' "$SUFFIX" | grep -Eq '^[0-9A-Za-z][0-9A-Za-z.-]{0,40}$'; then
echo "The suffix must be letters, digits, dots and dashes." >&2
exit 1
fi
- name: Create a repository-scoped release App token
id: release-app
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
app-id: ${{ vars.OPENADAPT_RELEASE_APP_ID }}
private-key: ${{ secrets.OPENADAPT_RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
permission-contents: write
permission-metadata: read
- name: Require the exact release App identity
env:
ACTUAL_APP_SLUG: ${{ steps.release-app.outputs.app-slug }}
run: |
if [ "$ACTUAL_APP_SLUG" != openadapt-release ]; then
echo "Tagging requires the openadapt-release App, not $ACTUAL_APP_SLUG." >&2
exit 1
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
token: ${{ steps.release-app.outputs.token }}
- name: Tag the commit
env:
TAG: registry-${{ inputs.suffix }}
run: |
set -euo pipefail
git -c user.name="openadapt-release[bot]" \
-c user.email="openadapt-release[bot]@users.noreply.github.com" \
tag -a "$TAG" "$GITHUB_SHA" -m "Skills registry $TAG"
git push origin "refs/tags/$TAG"

check:
name: check ${{ github.ref_name }}
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/registry-')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.ref }}
persist-credentials: false
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
- name: Install the engine
run: |
python scripts/ci/locked_install.py --lock 'packages/flow[dev]' \
--editable packages/types --editable packages/flow
- name: Check the bundles
run: |
set -euo pipefail
python packages/flow/scripts/build_registry_index.py --check
python -m pytest -q \
packages/flow/tests/test_skills_registry.py::test_no_registry_bundle_in_the_repo_holds_a_private_tier_file \
packages/flow/tests/test_skills_registry.py::test_every_file_of_a_registry_bundle_in_the_repo_is_allowlisted

attest:
name: build and attest ${{ github.ref_name }}
needs: check
runs-on: ubuntu-latest
timeout-minutes: 15
environment: release-identity
permissions:
contents: read
id-token: write
attestations: write
env:
PYTHONPATH: packages/flow:packages/types
TAG: ${{ github.ref_name }}
steps:
- name: Require a release App tag, attested on its first run
env:
EXPECTED_ACTOR: openadapt-release[bot]
TRIGGERING_ACTOR: ${{ github.triggering_actor }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
if [ "$GITHUB_REF_TYPE" != tag ] || [ "$GITHUB_ACTOR" != "$EXPECTED_ACTOR" ] \
|| [ "$TRIGGERING_ACTOR" != "$EXPECTED_ACTOR" ]; then
echo "Attesting requires a tag pushed by $EXPECTED_ACTOR." >&2
exit 1
fi
# A re-run of an old tag's run would stamp the old tree with a fresh
# serial, a rollback that beats every high-water mark. Tag again
# from main instead.
if [ "$RUN_ATTEMPT" != 1 ]; then
echo "Attesting runs only on a tag's first run, not attempt $RUN_ATTEMPT." >&2
exit 1
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.ref }}
fetch-depth: 0
persist-credentials: false
- name: Require the tag to be on protected main
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if ! git merge-base --is-ancestor HEAD refs/remotes/origin/main; then
echo "The registry tag is not contained in protected main." >&2
exit 1
fi
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: "3.12"
- name: Install only the hash-pinned verifier wheels
run: |
python -m pip install --require-hashes --no-deps --only-binary=:all: \
-r packages/flow/registry/verify-requirements.txt
- name: Build the archives and the stamped index
run: |
set -euo pipefail
python packages/flow/scripts/build_registry_index.py \
--archives dist-registry \
--asset-base "https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/" \
--out dist-registry/index.json \
--stamp
- name: Attest the index and every archive
id: attest
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: |
dist-registry/index.json
dist-registry/*.tar.gz
- name: Keep the attestation beside the index and check it as pull will
env:
BUNDLE: ${{ steps.attest.outputs.bundle-path }}
run: |
set -euo pipefail
if [ "$(grep -c . "$BUNDLE")" != 1 ]; then
echo "Expected one attestation bundle in $BUNDLE." >&2
exit 1
fi
head -n 1 "$BUNDLE" > dist-registry/index.sigstore.json
python - <<'PY'
import hashlib
import json
from pathlib import Path

from openadapt_flow import registry, registry_attest

dist = Path("dist-registry")
index = json.loads((dist / "index.json").read_text())
signer, subjects = registry_attest.verify_sigstore(
json.loads((dist / "index.sigstore.json").read_text())
)
index_digest = hashlib.sha256((dist / "index.json").read_bytes()).hexdigest()
if subjects.get("index.json") != index_digest:
raise SystemExit("the attestation does not name the index")
for archive in dist.glob("*.tar.gz"):
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
if subjects.get(archive.name) != digest:
raise SystemExit(f"the attestation does not name {archive.name}")
problem = registry.index_freshness_problem(index)
if problem:
raise SystemExit(problem)
print(f"attested by {signer}: {sorted(subjects)}")
PY
(cd dist-registry && sha256sum -- *.tar.gz index.json index.sigstore.json > SHA256SUMS)
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: registry-attested
path: dist-registry/
if-no-files-found: error

publish:
name: release ${{ github.ref_name }}
needs: attest
runs-on: ubuntu-latest
timeout-minutes: 15
environment: release-identity
permissions:
contents: read
steps:
- name: Create a repository-scoped release App token
id: release-app
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
app-id: ${{ vars.OPENADAPT_RELEASE_APP_ID }}
private-key: ${{ secrets.OPENADAPT_RELEASE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
permission-contents: write
permission-pull-requests: write
permission-metadata: read
- name: Require the exact release App identity
env:
ACTUAL_APP_SLUG: ${{ steps.release-app.outputs.app-slug }}
run: |
if [ "$ACTUAL_APP_SLUG" != openadapt-release ]; then
echo "Publishing requires the openadapt-release App, not $ACTUAL_APP_SLUG." >&2
exit 1
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
token: ${{ steps.release-app.outputs.token }}
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: registry-attested
path: dist-registry/
- name: Release, and propose the attested index
env:
GH_TOKEN: ${{ steps.release-app.outputs.token }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
(cd dist-registry && sha256sum --check --strict SHA256SUMS)
gh release create "$TAG" dist-registry/*.tar.gz dist-registry/index.json \
dist-registry/index.sigstore.json dist-registry/SHA256SUMS \
--verify-tag \
--title "Skills registry $TAG" \
--notes "Attested skills-registry index and bundles. See packages/flow/registry/README.md."
branch="registry/$TAG"
git switch -c "$branch"
cp dist-registry/index.json dist-registry/index.sigstore.json packages/flow/registry/
# The attestation is a new file the first time: add both by name.
git add packages/flow/registry/index.json packages/flow/registry/index.sigstore.json
git -c user.name="openadapt-release[bot]" \
-c user.email="openadapt-release[bot]@users.noreply.github.com" \
commit -m "registry: attested index for $TAG"
git push origin "refs/heads/$branch"
gh pr create --base main --head "$branch" \
--title "registry: attested index for $TAG" \
--body "The attested index the $TAG release published. Bundles are its release assets."
2 changes: 2 additions & 0 deletions CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ improvising or retrying when something is off.
| Compile an agent's first verified run from a prompt (`compile-agent`), and a code-as-action authoring agent | On main ([#1277](https://github.com/OpenAdaptAI/OpenAdapt/pull/1277), [#1305](https://github.com/OpenAdaptAI/OpenAdapt/pull/1305)); the agent harness behind it runs on every backend, on main ([#1283](https://github.com/OpenAdaptAI/OpenAdapt/pull/1283)) | Synthetic | `compile-agent` |
| Score harness optimizers (Harness², Meta-Harness) on GUI tasks with the record read-back as the reward, with full trace export | On main ([#1299](https://github.com/OpenAdaptAI/OpenAdapt/pull/1299), [#1300](https://github.com/OpenAdaptAI/OpenAdapt/pull/1300), [#1301](https://github.com/OpenAdaptAI/OpenAdapt/pull/1301)) | Synthetic (scripted proposer) | `integrations/harness2/` |
| One scoreboard comparing the agent alone with OpenAdapt across tasks (correctness by read-back, cost, time, items per hour) | On main ([#1303](https://github.com/OpenAdaptAI/OpenAdapt/pull/1303)) | Synthetic and local OpenEMR and openIMIS test copies | `benchmark/scoreboard/` |
| Pull a skill from the registry (`pull app/task@version`) and run it only after qualifying it on your own instance: `pull` marks it and expires any certification it carries, `skill bind` records your addresses and the names of the variables that hold credentials (never their values), `skill qualify` runs its bundled record check there, and `run`, `replay`, `emit-skill` and `emit-mcp` refuse it until then, and on any other instance. The index and its archives carry a keyless Sigstore attestation from the registry release workflow, checked offline against a pinned identity, and expire and can't be rolled back; a shipped record check runs no code and reads only your binding | On main | Unit tests | `openadapt-flow skill`, [SKILLS_REGISTRY.md](packages/flow/docs/design/SKILLS_REGISTRY.md) |

## Recording, privacy and proof

Expand Down Expand Up @@ -311,6 +312,7 @@ A test fails when a command has no file or this list is out of date.
- `record`
- `route`
- `sections`: Show, rename, merge or split the named sections a program map folds
- `skill`: Bind a pulled skill to your instance, qualify it there, and check whether it may run.
- `variants`
- `visualize`

Expand Down
2 changes: 2 additions & 0 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,7 @@ improvising or retrying when something is off.
| Compile an agent's first verified run from a prompt (`compile-agent`), and a code-as-action authoring agent | On main ([#1277](https://github.com/OpenAdaptAI/OpenAdapt/pull/1277), [#1305](https://github.com/OpenAdaptAI/OpenAdapt/pull/1305)); the agent harness behind it runs on every backend, on main ([#1283](https://github.com/OpenAdaptAI/OpenAdapt/pull/1283)) | Synthetic | `compile-agent` |
| Score harness optimizers (Harness², Meta-Harness) on GUI tasks with the record read-back as the reward, with full trace export | On main ([#1299](https://github.com/OpenAdaptAI/OpenAdapt/pull/1299), [#1300](https://github.com/OpenAdaptAI/OpenAdapt/pull/1300), [#1301](https://github.com/OpenAdaptAI/OpenAdapt/pull/1301)) | Synthetic (scripted proposer) | `integrations/harness2/` |
| One scoreboard comparing the agent alone with OpenAdapt across tasks (correctness by read-back, cost, time, items per hour) | On main ([#1303](https://github.com/OpenAdaptAI/OpenAdapt/pull/1303)) | Synthetic and local OpenEMR and openIMIS test copies | `benchmark/scoreboard/` |
| Pull a skill from the registry (`pull app/task@version`) and run it only after qualifying it on your own instance: `pull` marks it and expires any certification it carries, `skill bind` records your addresses and the names of the variables that hold credentials (never their values), `skill qualify` runs its bundled record check there, and `run`, `replay`, `emit-skill` and `emit-mcp` refuse it until then, and on any other instance. The index and its archives carry a keyless Sigstore attestation from the registry release workflow, checked offline against a pinned identity, and expire and can't be rolled back; a shipped record check runs no code and reads only your binding | On main | Unit tests | `openadapt-flow skill`, [SKILLS_REGISTRY.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/design/SKILLS_REGISTRY.md) |

## Recording, privacy and proof

Expand Down Expand Up @@ -708,6 +709,7 @@ A test fails when a command has no file or this list is out of date.
- `record`
- `route`
- `sections`: Show, rename, merge or split the named sections a program map folds
- `skill`: Bind a pulled skill to your instance, qualify it there, and check whether it may run.
- `variants`
- `visualize`

Expand Down
1 change: 1 addition & 0 deletions packages/flow/docs/commands/01-record-and-build/skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bind a pulled skill to your instance, qualify it there, and check whether it may run.
Loading
Loading