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
130 changes: 1 addition & 129 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,6 @@ on:
push:
branches:
- main
workflow_dispatch:
inputs:
bootstrap:
description: Explicit one-time openapi-mcp 0.0.0 bootstrap exception
type: boolean
default: false
approved_owner:
description: Named approving owner (must equal authenticated actor and configured owner)
type: string
required: true
approval_evidence:
description: GitHub URL recording owner approval and bootstrap scope
type: string
required: true
permissions:
contents: write
issues: write
Expand Down Expand Up @@ -106,6 +92,7 @@ jobs:
consumer=$(mktemp -d "$RUNNER_TEMP/openapi-public-XXXXXX")
cd "$consumer"
npm init -y
node "$GITHUB_WORKSPACE/scripts/openapi-mcp-release.mjs" wait-for-version "$version"
npm install --ignore-scripts --save-exact --registry=https://registry.npmjs.org/ "@knitli/openapi-mcp@$version"
npm audit signatures --json --include-attestations > "$evidence/npm-audit-signatures.json"
node "$GITHUB_WORKSPACE/scripts/openapi-mcp-release.mjs" verify-audit "$evidence/npm-audit-signatures.json" "$tarball" "$version" > "$evidence/verified-provenance.json"
Expand All @@ -119,121 +106,6 @@ jobs:
path: ${{ runner.temp }}/openapi-mcp-release/
if-no-files-found: ignore

# This exception is disabled unless the named owner explicitly configures and
# dispatches it. Remove its token wiring/secret and revoke the token after use;
# the steady-state adapter refuses publication while that wiring remains.
bootstrap-openapi-mcp-pack:
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && inputs.bootstrap && vars.OPENAPI_MCP_BOOTSTRAP_ENABLED == 'true' && inputs.approved_owner == vars.OPENAPI_MCP_BOOTSTRAP_OWNER && github.actor == vars.OPENAPI_MCP_BOOTSTRAP_OWNER
runs-on: ubuntu-latest
environment: npmrelease
permissions:
contents: read
outputs:
sha256: ${{ steps.pack.outputs.sha256 }}
steps:
- uses: actions/checkout@v7
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.0
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
- name: Record named owner approval
env:
APPROVED_OWNER: ${{ inputs.approved_owner }}
APPROVAL_EVIDENCE: ${{ inputs.approval_evidence }}
run: |
test "$APPROVED_OWNER" = "$GITHUB_ACTOR"
case "$APPROVAL_EVIDENCE" in https://github.com/knitli/toolshed/*) ;; *) exit 1 ;; esac
mkdir -p "$RUNNER_TEMP/openapi-mcp-bootstrap"
node --input-type=module -e 'import {writeFileSync} from "node:fs"; writeFileSync(process.env.RUNNER_TEMP + "/openapi-mcp-bootstrap/approval.json", JSON.stringify({owner:process.env.APPROVED_OWNER,evidence:process.env.APPROVAL_EVIDENCE,sourceCommit:process.env.GITHUB_SHA,runId:process.env.GITHUB_RUN_ID},null,2));'
- run: bun i --frozen-lockfile
- name: Build and test bootstrap source
run: |
node -e 'const p=require("./packages/openapi-mcp/package.json"); if(p.name!=="@knitli/openapi-mcp" || p.version!=="0.0.0" || p.private!==false) process.exit(1);'
bun scripts/generate.mts --check
bun test scripts/openapi-mcp-release.test.ts
bun test packages/openapi-mcp
bun run --cwd packages/openapi-mcp test:node-transport
bun run --cwd packages/openapi-mcp test:node-stdio
bun run --cwd packages/openapi-mcp build
bun run validate
- name: Pack and exercise the exact bootstrap artifact
id: pack
shell: bash
run: |
tarball="$RUNNER_TEMP/openapi-mcp-bootstrap/knitli-openapi-mcp-0.0.0.tgz"
(cd packages/openapi-mcp && bun pm pack --filename "$tarball" --quiet)
OPENAPI_MCP_TARBALL="$tarball" bun test packages/openapi-mcp/tests/package-consumers.test.ts
cd "$RUNNER_TEMP/openapi-mcp-bootstrap"
sha256sum knitli-openapi-mcp-0.0.0.tgz > SHA256SUMS
printf 'sha256=%s\n' "$(cut -d ' ' -f1 SHA256SUMS)" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
name: openapi-mcp-bootstrap-tested-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/openapi-mcp-bootstrap/
if-no-files-found: error

bootstrap-openapi-mcp-publish:
needs: bootstrap-openapi-mcp-pack
if: github.event_name == 'workflow_dispatch' && inputs.bootstrap
runs-on: ubuntu-latest
environment: npmrelease
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v7
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.0
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
registry-url: https://registry.npmjs.org
- run: bun i --frozen-lockfile
- uses: actions/download-artifact@v4
with:
name: openapi-mcp-bootstrap-tested-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/openapi-mcp-bootstrap/
- name: Rehash downloaded tested artifact and publish only bootstrap
env:
EXPECTED_SHA256: ${{ needs.bootstrap-openapi-mcp-pack.outputs.sha256 }}
NODE_AUTH_TOKEN: ${{ secrets.OPENAPI_MCP_BOOTSTRAP_TOKEN }}
shell: bash
run: |
test -n "$NODE_AUTH_TOKEN"
cd "$RUNNER_TEMP/openapi-mcp-bootstrap"
sha256sum --check SHA256SUMS
actual=$(sha256sum knitli-openapi-mcp-0.0.0.tgz | cut -d ' ' -f1)
test "$actual" = "$EXPECTED_SHA256"
tarball="$RUNNER_TEMP/openapi-mcp-bootstrap/knitli-openapi-mcp-0.0.0.tgz"
publish_dir=$(mktemp -d "$RUNNER_TEMP/openapi-bootstrap-publish-XXXXXX")
cd "$publish_dir"
npm publish "$tarball" --access public --tag bootstrap --provenance --registry=https://registry.npmjs.org/
test "$(sha256sum "$tarball" | cut -d ' ' -f1)" = "$EXPECTED_SHA256"
- name: Verify bootstrap public install and cryptographic provenance
env:
NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/openapi-public-empty-user.npmrc
NPM_CONFIG_GLOBALCONFIG: ${{ runner.temp }}/openapi-public-empty-global.npmrc
shell: bash
run: |
evidence="$RUNNER_TEMP/openapi-mcp-bootstrap"
consumer=$(mktemp -d "$RUNNER_TEMP/openapi-bootstrap-public-XXXXXX")
cd "$consumer"
npm init -y
npm install --ignore-scripts --save-exact --registry=https://registry.npmjs.org/ @knitli/openapi-mcp@0.0.0
npm audit signatures --json --include-attestations > "$evidence/npm-audit-signatures.json"
node "$GITHUB_WORKSPACE/scripts/openapi-mcp-release.mjs" verify-audit "$evidence/npm-audit-signatures.json" "$evidence/knitli-openapi-mcp-0.0.0.tgz" 0.0.0 > "$evidence/verified-provenance.json"
cp "$GITHUB_WORKSPACE/packages/openapi-mcp/test-consumers/node.mts" "$consumer/node.mts"
node node.mts > "$evidence/public-consumer.json"
sha256sum "$evidence/npm-audit-signatures.json" > "$evidence/npm-audit-signatures.sha256"
- uses: actions/upload-artifact@v4
if: always()
with:
name: openapi-mcp-bootstrap-registry-evidence-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/openapi-mcp-bootstrap/
if-no-files-found: error
release-marketplace:
if: github.event_name == 'push'
needs:
Expand Down
75 changes: 75 additions & 0 deletions docs/superpowers/reviews/2026-09-05-openapi-mcp-bootstrap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# OpenAPI MCP bootstrap publication and OIDC transition

**Version 1.0.0** · Knitli · 2026-09-05

## AI reading instruction

Read `[SPEC]` and `[BUG]` blocks for verified facts and release requirements.
`[NOTE]` records context; `[?]` records settings reported by the owner but not
independently verified. This record does not authorize a new publication.

## 1. Verified bootstrap

**[SPEC]**

- Package: `@knitli/openapi-mcp@0.0.0`, public, dist-tag `bootstrap`.
- Source commit: `4f39c6c541568328b8566329c8eaa23fac1c52a0` on `main`.
- Workflow: `knitli/toolshed/.github/workflows/release.yml`; environment: `npmrelease`.
- Run: [33977232028, attempt 1](https://github.com/knitli/toolshed/actions/runs/33977232028).
- Named owner approval: [bootstrap scope recorded on PR #23](https://github.com/knitli/toolshed/pull/23#issuecomment-5552963096).
- Tested artifact: `openapi-mcp-bootstrap-tested-33977232028-1`, artifact ID `9972685133`.
- Tarball: `knitli-openapi-mcp-0.0.0.tgz`.
- Tarball SHA-256: `5a08830a006091bc0d4c371ab9530b91c0c506cf811a37a5b23f48cb6a93bca5`.
- Tarball SHA-512: `e346a9357baceccd9cd529af4c56f0554f129207c95a6ddc047792b5fba677fd753cf75932d0561f18fa2fe646420520376ee2c50699fb275f34e12993318337`.
- Publication completed at `2026-09-05T16:15:56Z`; [publish job](https://github.com/knitli/toolshed/actions/runs/33977232028/job/101336147561).
- No stable `latest` release, semantic-release Git tag, or OS deployment is established by this bootstrap.

## 2. Verification after registry propagation

**[SPEC]**

- A fresh public installation of exact version `0.0.0` passed without npm credentials and with lifecycle scripts disabled.
- `npm audit signatures --json --include-attestations` exited zero, with no invalid or missing evidence.
- `verify-audit` bound the verified provenance to the exact CI-tested tarball, package/version, source commit, repository, workflow path/ref, run ID, and attempt above.
- The installed Node consumer passed runtime, public MCP read, rejected-admission recovery, rollback, and CLI checks.
- Verification used Node `24.19.0`, matching the release workflow.
- Retained local evidence: `/private/tmp/openapi-bootstrap-verification-3l6HHy/`; raw audit: `npm-audit-signatures.json`.
- Raw audit SHA-256: `a6a76157126cd943ab2aea20b08cadfb32d0a7d9c02922d745af66e356b10f36`.
- These were subsequent local checks, not a green rerun of the failed CI job. The local evidence directory is not a durable hosted artifact.

**[BUG] Immediate registry lookup after publication**

- Symptom: the publish step succeeded, but the public install returned `E404` approximately 0.68 seconds later; the workflow remained failed.
- Cause: the package listing was not yet visible to that lookup. A later exact-version request succeeded while a package-listing request still returned `404`.
- Fix: poll the same public package listing used for installation with bounded requests before installing. Do not retry `npm publish`, waive cryptographic checks, or treat an uploaded artifact as proof that verification finished.

**[NOTE]**

The run also uploaded `openapi-mcp-bootstrap-registry-evidence-33977232028-1`
(artifact ID `9972692303`), but installation failed before its audit and consumer
commands ran. Its existence alone is not completed registry verification.

## 3. Transition status and remaining gates

**[SPEC]**

- GitHub's `npmrelease` environment secret listing was empty after the owner removed `OPENAPI_MCP_BOOTSTRAP_TOKEN` on 2026-09-05.
- This cleanup removes the bootstrap dispatch and jobs from the workflow while retaining the adapter's rejection of token wiring.
- `OPENAPI_MCP_OIDC_READY` was unset at inspection; this PR does not set it or start a release.
- The bootstrap owner/enablement repository variables were still present at inspection. They are unused once the bootstrap workflow path is removed and can be retired separately.
- Before enabling the first regular release, confirm the exact trusted publisher: owner `knitli`, repository `toolshed`, workflow filename `release.yml`, environment `npmrelease`, permission to publish.
- Confirm revocation of the bootstrap npm token, not merely removal of its GitHub secret, before enabling regular publication.
- GitHub must issue the standard issuer `https://token.actions.githubusercontent.com`; the earlier bootstrap attempt was rejected for the enterprise-specific `/knitli` issuer.
- After cleanup is merged and external setup confirmed, an operator may enable `OPENAPI_MCP_OIDC_READY`. A successful OIDC-authenticated publication and its public verification are still required to establish the first regular release.
- The native OS integration retains its separate stable-release and API approval gates.

**[?]**

- The owner reports configuring npm trusted publishing and disabling the token/publication route. Public `npm trust list` returned `E401`, so the exact publisher fields and npm token revocation were not independently inspected.

## 4. Changelog

**[NOTE]**

1.0.0 records the successful bootstrap, subsequent public verification, registry
propagation failure, and the remaining OIDC transition gates.
39 changes: 25 additions & 14 deletions packages/openapi-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ established; use Node for `serve`. `/runtime` and `/conformance` pass an isolate
WebWorker-only declaration check and browser bundle. This is not evidence of an
actual Cloudflare D1 deployment or D1 conformance.

The source version is `0.0.0`. Registry publication, trusted publishing, and
registry provenance verification remain release gates, not claims made by this
README. For a locally built and tested tarball, install its absolute path from a
clean consumer directory:
The source version is `0.0.0`. That version was published under the `bootstrap`
dist-tag on 2026-09-05; its public installation, signatures, provenance, and
installed consumer were verified. See the [bootstrap release record](../../docs/superpowers/reviews/2026-09-05-openapi-mcp-bootstrap.md)
for the exact artifact and workflow binding. This is not a stable `latest`
release or evidence of a successful trusted-publishing release.

For a locally built and tested tarball, install its absolute path from a clean
consumer directory:

```sh
npm install --ignore-scripts /absolute/path/to/openapi-mcp.tgz
Expand All @@ -28,8 +32,8 @@ node --version

After a real stable version is observed in npm, pin that exact verified version
with `npm install --save-exact @knitli/openapi-mcp@<verified-version>`. A bootstrap
`0.0.0` publication, if needed, uses the `bootstrap` dist-tag and is not a stable
release. The CLI examples below use the consumer's `node_modules/.bin` binary.
`0.0.0` publication uses the `bootstrap` dist-tag and is not a stable release.
The CLI examples below use the consumer's `node_modules/.bin` binary.

## Generate a signing key and compile a release

Expand Down Expand Up @@ -341,14 +345,21 @@ external setup and bootstrap-cleanup gates. Until then, build/test/validation
still run and publication is explicitly reported as pending. A real build,
publication, or audit failure continues to fail the job and its dependents.

If the package does not yet exist, bootstrap is a separate exception requiring
recorded named owner approval and protected-environment approval. It publishes
only `0.0.0`, with `--access public --tag bootstrap --provenance`; it does not run
semantic-release or create a Git tag/release. A short-lived granular token may be
injected only into that protected job, never into local files, logs, commits, or
artifacts. After bootstrap, configure the exact trusted publisher, remove token
variables and the GitHub secret, revoke the token, and commit the OIDC-only
workflow before allowing the first real `latest` release.
The one-time `0.0.0` bootstrap is complete. Its manual dispatch and token-backed
jobs have been removed; do not rerun publication of that version. The adapter
still rejects token environment mappings, including mappings in disabled jobs.
The [bootstrap release record](../../docs/superpowers/reviews/2026-09-05-openapi-mcp-bootstrap.md)
separates verified publication and secret removal from owner-reported npm setup
and the remaining first-`latest` gate.

After publication, the verification step waits for the exact version to appear
in the public package listing before attempting a fresh installation. Only a
missing package or missing version is retried: at most six requests, each with
a ten-second timeout, separated by ten-second waits. Invalid metadata and other
errors fail the check. This readiness probe cannot guarantee every registry
replica or tarball URL is ready. Installation, cryptographic verification, and
consumer checks remain mandatory; publication itself is never retried by this
probe.

For bootstrap and every stable release, retain a fresh public install's
`npm audit signatures --json --include-attestations` output in the protected job
Expand Down
70 changes: 69 additions & 1 deletion scripts/openapi-mcp-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,68 @@ const { parse: parseYaml } = createRequire(
)("yaml");
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");

/** Wait only for packument visibility; installation and verification can still fail. */
export async function waitForRegistryVersion(version, dependencies = {}) {
if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version))
throw new Error("Registry readiness requires an exact stable version");
const fetchRegistry = dependencies.fetch ?? fetch;
const sleep =
dependencies.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
for (let attempt = 0; attempt < 6; attempt++) {
const signal = AbortSignal.timeout(10000);
let onAbort;
let response;
let metadata;
try {
[response, metadata] = await Promise.race([
(async () => {
const result = await fetchRegistry(
`${registry}@knitli%2fopenapi-mcp`,
{
headers: { Accept: "application/vnd.npm.install-v1+json" },
signal,
},
);
return [
result,
result.status === 200 ? await result.json() : undefined,
];
})(),
new Promise((_, reject) => {
onAbort = () => reject(signal.reason);
signal.addEventListener("abort", onAbort, { once: true });
if (signal.aborted) onAbort();
}),
]);
} finally {
signal.removeEventListener("abort", onAbort);
}
if (response.status === 200) {
if (
!metadata ||
metadata.name !== packageName ||
!metadata.versions ||
typeof metadata.versions !== "object" ||
Array.isArray(metadata.versions)
)
throw new Error("Malformed or mismatched registry packument");
if (Object.hasOwn(metadata.versions, version)) {
const entry = metadata.versions[version];
if (!entry || entry.name !== packageName || entry.version !== version)
throw new Error(
"Registry entry does not match the exact package version",
);
return;
}
} else if (response.status !== 404)
throw new Error(`Registry request failed with HTTP ${response.status}`);
if (attempt < 5) await sleep(10000);
}
throw new Error(
`Registry version ${version} was not visible after 6 attempts`,
);
}

/** Bind npm's successfully verified audit output to this exact release.
* This is not a cryptographic verifier: the caller MUST first require npm audit
* signatures --json --include-attestations to exit zero on a fresh public install.
Expand Down Expand Up @@ -100,7 +162,7 @@ function releaseVersion(context) {

async function checkContext(context) {
const env = context.env;
if (env.NPM_TOKEN || env.NODE_AUTH_TOKEN)
if (Object.hasOwn(env, "NPM_TOKEN") || Object.hasOwn(env, "NODE_AUTH_TOKEN"))
throw new Error(
"Steady-state release must use OIDC without token environment variables",
);
Expand Down Expand Up @@ -333,6 +395,12 @@ if (
console.log(
JSON.stringify({ ...verified, auditSha256: hash(auditBytes) }, null, 2),
);
} else if (
process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url) &&
process.argv[2] === "wait-for-version"
) {
await waitForRegistryVersion(process.argv[3]);
} else if (
process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
Expand Down
Loading
Loading