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
255 changes: 255 additions & 0 deletions .github/workflows/release-direct.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
name: Release verified packages via OIDC

on:
workflow_dispatch:
inputs:
dist_tag:
description: npm distribution tag for the verified release
required: true
default: next
type: choice
options:
- next
- latest
dry_run:
description: prepare and verify immutable tarballs without publishing them
required: false
default: true
type: boolean
scope:
description: Publication cohort
required: true
default: all
type: choice
options:
- public-consumer
- all

permissions:
contents: read

concurrency:
group: release-packages
cancel-in-progress: false

jobs:
prepare:
if: >-
github.ref == 'refs/heads/main' &&
github.actor_id == '207043696' &&
github.actor == 'douglas-grishen' &&
github.triggering_actor == 'douglas-grishen' &&
(inputs.dry_run || vars.AGENTPLAT_NPM_DIRECT_RELEASE_ENABLED == 'true')
env:
NPM_PACKAGE_SCOPE: ${{ inputs.scope }}
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
contents: read
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: agentplat_release
POSTGRES_USER: agentplat_release
POSTGRES_PASSWORD: agentplat_release
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U agentplat_release -d agentplat_release"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.20.0
package-manager-cache: false
- run: corepack enable
- name: Install dependencies without registry credentials
run: pnpm install --frozen-lockfile
env:
NPM_CONFIG_USERCONFIG: /dev/null
- name: Prepare required public terminology denylist
shell: bash
env:
AGENTPLAT_PUBLIC_DENYLIST: ${{ secrets.AGENTPLAT_PUBLIC_DENYLIST }}
run: |
if [ -z "${AGENTPLAT_PUBLIC_DENYLIST:-}" ]; then
echo "The release terminology denylist secret is required." >&2
exit 1
fi
umask 077
denylist_path="${RUNNER_TEMP}/agentplat-public-terminology.txt"
printf '%s\n' "${AGENTPLAT_PUBLIC_DENYLIST}" > "${denylist_path}"
echo "AGENTPLAT_PUBLIC_DENYLIST_FILE=${denylist_path}" >> "${GITHUB_ENV}"
- name: Run release audit and full verification
run: |
pnpm run audit:public:release
pnpm run audit:dependencies:production
if [ "${NPM_PACKAGE_SCOPE}" = "public-consumer" ]; then
pnpm --filter @agentplat/collective-runtime... --filter @agentplat/audit... build
pnpm --filter @agentplat/collective-runtime --filter @agentplat/audit type-check
else
pnpm run check
pnpm run verify:mesh-postgres-faults
pnpm run verify:mesh-soak -- --messages 9 --repetitions 2
pnpm run benchmark:mesh-adapters
fi
env:
AGENTPLAT_POSTGRES_TEST: "1"
NPM_CONFIG_USERCONFIG: /dev/null
PGHOST: 127.0.0.1
PGPORT: "5432"
PGDATABASE: agentplat_release
PGUSER: agentplat_release
PGPASSWORD: agentplat_release
- name: Prepare immutable npm release artifacts
run: node scripts/prepare-npm-release-artifacts.mjs
env:
AGENTPLAT_RELEASE_ARTIFACT_DIRECTORY: release-artifacts
NPM_CONFIG_USERCONFIG: /dev/null
NPM_DIST_TAG: ${{ inputs.dist_tag }}
NPM_PACKAGE_SCOPE: ${{ inputs.scope }}
- name: Verify the exact release artifacts
run: |
if [ "${NPM_PACKAGE_SCOPE}" = "public-consumer" ]; then
pnpm run verify:public-consumer
else
pnpm run verify:pack
fi
env:
AGENTPLAT_PREPACKED_TARBALL_DIRECTORY: ${{ github.workspace }}/release-artifacts
NPM_CONFIG_USERCONFIG: /dev/null
- name: Upload immutable npm release artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: npm-release-${{ github.sha }}-${{ inputs.scope }}-${{ inputs.dist_tag }}
path: release-artifacts/
if-no-files-found: error
compression-level: 0
# Human staged approval and post-publication verification can span days.
retention-days: 30
include-hidden-files: false

publish:
if: ${{ !inputs.dry_run && vars.AGENTPLAT_NPM_DIRECT_RELEASE_ENABLED == 'true' }}
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 30
environment: npm-release
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 1
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.20.0
package-manager-cache: false
- name: Download the same verified cohort
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: npm-release-${{ github.sha }}-${{ inputs.scope }}-${{ inputs.dist_tag }}
path: release-artifacts
- name: Publish exact tarballs with scoped OIDC
run: node scripts/publish-direct-npm-release.mjs
env:
AGENTPLAT_NPM_DIRECT_RELEASE_ENABLED: ${{ vars.AGENTPLAT_NPM_DIRECT_RELEASE_ENABLED }}
AGENTPLAT_NPM_DIRECT_PUBLISH_CONFIRMED: ${{ vars.AGENTPLAT_NPM_DIRECT_PUBLISH_CONFIRMED }}
AGENTPLAT_RELEASE_ARTIFACT_DIRECTORY: release-artifacts
NPM_CONFIG_USERCONFIG: /dev/null
NPM_DIST_TAG: ${{ inputs.dist_tag }}
- name: Retain publication progress even after partial failure
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: npm-direct-report-${{ github.sha }}-${{ inputs.scope }}-${{ inputs.dist_tag }}
path: direct-release-report.json
if-no-files-found: ignore
retention-days: 30

verify:
needs: publish
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: agentplat_release_verify
POSTGRES_USER: agentplat_release_verify
POSTGRES_PASSWORD: agentplat_release_verify
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U agentplat_release_verify -d agentplat_release_verify"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
AGENTPLAT_RELEASE_ARTIFACT_DIRECTORY: release-artifacts
AGENTPLAT_SOURCE_COMMIT: ${{ github.sha }}
AGENTPLAT_RELEASE_WORKFLOW_PATH: .github/workflows/release-direct.yml
NPM_DIST_TAG: ${{ inputs.dist_tag }}
NPM_CONFIG_USERCONFIG: /dev/null
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.20.0
package-manager-cache: false
- run: corepack enable
- run: pnpm install --frozen-lockfile
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: npm-release-${{ github.sha }}-${{ inputs.scope }}-${{ inputs.dist_tag }}
path: release-artifacts
- name: Verify registry integrity, signatures, tag and source provenance
run: node scripts/verify-npm-release-provenance.mjs
- name: Verify the public-consumer registry cohort
if: ${{ inputs.scope == 'public-consumer' }}
run: pnpm run verify:public-consumer
env:
AGENTPLAT_PUBLIC_CONSUMER_SOURCE: registry
- name: Verify complete coordinated registry distribution
if: ${{ inputs.scope == 'all' }}
run: node scripts/npm-distribution-readiness.mjs --require-complete --tag "$NPM_DIST_TAG"
- name: Verify portable registry consumer
if: ${{ inputs.scope == 'all' }}
run: pnpm run verify:registry-consumer
env:
AGENTPLAT_REGISTRY_CONSUMER_PM: pnpm
AGENTPLAT_REGISTRY_CONSUMER_PROFILE: portable
- name: Verify durable registry consumer
if: ${{ inputs.scope == 'all' }}
run: pnpm run verify:registry-consumer
env:
AGENTPLAT_REGISTRY_CONSUMER_PM: pnpm
AGENTPLAT_REGISTRY_CONSUMER_PROFILE: postgres
PGHOST: 127.0.0.1
PGPORT: "5432"
PGDATABASE: agentplat_release_verify
PGUSER: agentplat_release_verify
PGPASSWORD: agentplat_release_verify
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
if: ${{ inputs.scope == 'all' }}
with:
node-version: 22.22.0
package-manager-cache: false
- name: Verify npm registry consumer on Node 22
if: ${{ inputs.scope == 'all' }}
run: node scripts/verify-registry-consumer.mjs
env:
AGENTPLAT_REGISTRY_CONSUMER_PM: npm
AGENTPLAT_REGISTRY_CONSUMER_PROFILE: portable
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,10 @@ with `node scripts/approve-owner-npm-release.mjs RUN_ID`; do not ask him to name
another reviewer. Other initiators still require separate owner review. Preserve
required CI, protected main, stage-only publishing, staged-byte review and npm
2FA. See `docs/security/npm-release-security.md` for the exact boundary.


The release-level OIDC alternative in `docs/security/npm-direct-release.md` is
prepared but NOT activated. Do not enable its flags, create its trusted publisher
relationships or publish a new cohort without the owner's final activation
approval. Once explicitly enabled, its own single-release approval and exact
artifact checks replace per-package staged approval for that profile only.
6 changes: 6 additions & 0 deletions docs/release-channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,9 @@ version's `latest` tag. This caveat applies to the first publication of
`@agentplat/trust`, as it did when the four Agent Mesh packages were introduced
in Alpha 1. Consumers should still install the coordinated channel explicitly
while the framework is in preview.


A disabled-by-default [release-level OIDC profile](security/npm-direct-release.md)
prepares one verified cohort for one protected deployment approval. It requires
explicit activation of separate npm publisher permissions; the existing staged
release workflow remains the active default until that activation.
122 changes: 122 additions & 0 deletions docs/security/npm-direct-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Release-level OIDC publication (prepared, not activated)

The owner requested preparation of this alternative after interactive staged
approval failed to reuse a passkey across the coordinated package cohort.
This document is the reviewable activation proposal. No npm publisher permissions,
GitHub environments or enablement variables are changed by merging the code.

## Change in authority

The existing `release.yml` / `npm-production` relationship remains stage-only.
A separate `release-direct.yml` / `npm-release` relationship would allow public
publication from CI after one protected deployment approval for the exact release.
The package registry would no longer require a separate passkey approval for
each package through this relationship. Account 2FA stays enabled; no long-lived
npm token is created. This deliberately changes the current security boundary:
trusted CI becomes able to publish publicly after release-level authorization.
A compromise of that authorized workflow/environment is therefore a publication
risk; it must not be described as equivalent to per-package human approval.

## Execution and checks

1. Manual dispatch is restricted to the owner's immutable GitHub identity, both
initial actor and rerun actor, on `main`. Dry-run is the default. Non-dry runs
also require the repository enablement flag.
2. An unprivileged job installs the frozen lockfile, performs the public and
dependency audits, builds, tests and checks the selected release scope. Full
scope retains the existing complete checks, PostgreSQL faults, soak and adapter
benchmark. The exact packed tarballs are independently consumed before upload.
3. `npm-release` gates publication once for the whole prepared artifact. The owner
may approve his own requested release, following the standing owner policy;
no second person is required. Other accounts cannot initiate this profile.
4. The OIDC job installs no project dependencies and executes no build. It verifies
the source commit, version, cohort, sizes, SHA-512, package identity and absence
of lifecycle hooks for EVERY archive before publishing any. It refuses symlinks
and extra or missing files. npm runs from an isolated directory with user/global
configuration disabled, explicit public registry, `--ignore-scripts` and
`--provenance`. Only the previously verified tarballs are passed to npm.
5. Existing versions are skipped only when their actual downloaded bytes, registry
ECDSA signature, distribution tag, workflow provenance and source commit match
this exact release. Any disagreement or registry uncertainty stops before new
publication. Dependencies are published first, using the existing topological
package ordering. A command failure stops before dependent packages.
6. Read-only verification checks public registry bytes, ECDSA signatures, provenance
fields and tags, then exercises clean pnpm/npm consumers (including the durable
PostgreSQL profile and Node 22). These results are required before announcing
a completed release. Provenance fields come from npm's attestation endpoint;
this verifier is not an independent implementation of Sigstore certificate /
transparency-log verification.

npm has no atomic transaction spanning 65 packages. A network failure can leave
part of a cohort public. Rerun the SAME original workflow/commit/artifact; the
publisher verifies existing versions rather than rebuilding, republishing or
silently repairing tags. A mismatched existing version requires investigation.
The progress report is diagnostic; registry evidence determines resume behavior.

## Activation proposal — requires owner's final authorization

After the reviewed code is integrated and required CI passes:

- Create `npm-release` with administrator bypass disabled and exactly GitHub user
`douglas-grishen` (ID `207043696`) as reviewer; allow that owner to approve his own
release. Use **selected branches and tags**, with exactly one **branch** rule
named `main`. Do not use a wildcard or a tag rule. The workflow additionally
requires GitHub to report `main` as protected.
- For each of the 65 already-existing npm packages, add a trusted publisher tied
to organization `Agentplat`, repository `agentplat`, workflow filename
`release-direct.yml`, environment `npm-release`, with direct `npm publish`
permission. Retain the original stage-only publisher as a separate relationship.
Verify the actual package settings; do not assume that saving a name validates
the relationship. This initial setup may require npm account authentication.
- Only after those relationships are checked, set the environment variable
`AGENTPLAT_NPM_DIRECT_PUBLISH_CONFIRMED=true` in `npm-release`.
- Enable last: set repository variable `AGENTPLAT_NPM_DIRECT_RELEASE_ENABLED=true`.
Run `AGENTPLAT_NPM_RELEASE_MODE=direct node scripts/verify-npm-release-governance.mjs`
using the authenticated maintainer CLI. It checks the owner, exact main-only
deployment rule, enablement flags and existing main/Actions protections.
- Prepare a fresh coordinated version and run a dry-run first. Then dispatch the
same reviewed main source with `dry_run=false`, tag `next`, scope `all`.
After preparation passes, approve the one environment deployment, or use
`node scripts/approve-owner-npm-release.mjs RUN_ID --direct` on the owner's
behalf. It refuses foreign original/rerun actors, failed preparation, another
workflow, disabled flags or a different environment.

The flags are not proof of npm permissions: a maintainer must verify the package
publisher settings. Only npm's actual OIDC exchange can establish the final
provider configuration works. That live test has not been performed for this
prepared profile, and cannot be claimed from fixture tests.

## Existing beta.9 and migration

Beta.9 is already partially public, and other beta.9 versions remain staged.
Staged and public versions share npm's version uniqueness constraint. The new
publisher must not overwrite them, automatically reject staging, unpublish a
version or move old tags to disguise this partial release.

Use a fresh coordinated version, proposed `0.3.0-beta.10`, after checking current
registry availability. Bump all package manifests through the existing version
script, add the supported release-line entry, regenerate/verify the lockfile if
needed, and produce a NEW manifest from the approved source. No such version
bump or publication is included in this preparation change. Historical paper
and beta.9 artifacts keep their original source references. Handling leftover
private staging is a separate, explicitly authorized cleanup.

## Disable / rollback

Set `AGENTPLAT_NPM_DIRECT_RELEASE_ENABLED=false` to block new direct runs. Cancel
any still-running publication separately: changing a variable cannot undo a job
already underway. Revoke the `release-direct.yml` publisher relationships if
required. Already-public npm versions are immutable and are not rolled back.
The legacy staged workflow remains available for a separately prepared cohort.

## Validation and sources

Tests cover disabled flags, foreign actors/workflows/branches, incomplete archives,
changed bytes, lifecycle scripts, symlinks, dependency order/cycles, publication
failure, exact reruns and altered signatures/provenance/tags. Simulated publisher
calls never contact npm for writes. The cryptographic byte/signature reader was
also checked read-only against already-public beta.9 packages.

- [npm trusted publishing and allowed actions](https://docs.npmjs.com/trusted-publishers/)
- [npm staged publishing](https://docs.npmjs.com/staged-publishing/)
- [npm registry signature format](https://docs.npmjs.com/about-registry-signatures/)
Loading
Loading