Repository navigation
feat(registry)!: release CLI, idempotent publish, promote, quarantine review, signed downloads and GitHub OIDC trusted publishing - #99
Merged
Conversation
…d downloads and GitHub OIDC trusted publishing Adds migration 0018 (additive tables plus release_channel), the Idempotency-Key replay store, audited channel promotion, admin approve/reject of quarantined releases, short-lived signed artifact URLs for private releases, and GitHub Actions OIDC exchange for scoped publish tokens with a trusted-publisher config API. All routes stay behind SKILLX_RELEASES_API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dir> and promote publish owner/repo keeps working as a deprecated alias of import with a stderr warning; release pack/publish remain aliases. Release commands share the JSON envelope and standard exit codes, and publish supports --idempotency-key and --oidc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se resolution policy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion Kit directories ak-<x> map to @bestagentkits/<x> because @agentkit/* and ak- slugs stay reserved by the skillx.release/v1 contract. The workflow refuses a version that differs from kits/<kit>/kit.yaml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
This PR overlaps with #100 ( #100 makes these changes:
Whichever lands second should keep both sets of changes when resolving the conflict. After #100 lands, foundations can publish as Generated by Claude Code |
This was referenced Oct 19, 2026
feat: collections API and verified CLI installer with lockfile, sync and local MCP (#35 phase 3)
#98
Merged
…h paths - Approving a quarantined release restores the state it had before the quarantine (from release_state_audit): a yanked release stays yanked unless the admin sends restoreTo "published". The approval is pinned to the quarantine it read by quarantined_at. - Idempotency-Key stores only answers that are a function of the request bytes (2xx, 400, 415, 409 release.version_exists) and releases the claim for everything else; the body is read and the caller authorized once. - The OIDC exchange allows only push, workflow_dispatch and release events, keeps verifying with recently fetched JWKS while GitHub is unreachable, and expired exchange and idempotency rows are deleted in small batches. - The current channel header is sent only on the short-lived version route, never on immutable digest routes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CLI is installed and built in a job without id-token or the skillx-publish environment, and the publish job installs the packed tarball with lifecycle scripts disabled. The API key fallback also covers a registry answering oidc.unavailable, and server text is stripped of line breaks and '::' before it reaches a workflow command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rules The registry publishing routes are live on deploy (SKILLX_RELEASES_API has been on since #70), so migration 0018 must be applied first, and SKILLX_TOKEN_SECRET gates only OIDC and signed downloads. Documents restoreTo on quarantine approval, the unsigned listing channel, the OIDC event allowlist, the JWKS grace period, retention, the idempotency replay rules and the skillx-publish environment protection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BREAKING CHANGE: `skillx release publish --json` (now an alias of
`skillx publish <dir>`) prints the `{"ok": true, "data": {...}}` /
`{"ok": false, "error": {...}}` envelope instead of the bare release
object, and failures use the documented exit codes instead of always 1.
Read the release from `.data`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… token gaps - Approval reads the last two applied audit edges by rowid and treats only the composite yanked -> published -> quarantined batch as a yank, so a yank and a quarantine in the same millisecond no longer re-publish. - Token segments must be canonical base64url, so a MAC has one spelling; the tamper test changes a middle character. - While the JWKS endpoint fails, the refetch is retried at most once a minute instead of on every request. - Indexes for expired-row cleanup on release_publish_idempotency.expires_at and oidc_token_exchanges.created_at, folded into migration 0018 (not applied anywhere yet). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ented Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2 of the registry (#34, #54): the release CLI surface, publish idempotency, channel promotion, quarantine review, signed downloads for private releases, and GitHub OIDC trusted publishing, plus a manual-dispatch workflow that publishes an AgentKit foundation skill. The new routes sit under the release API, and production has had
SKILLX_RELEASES_APIset to"1"since #70, so they are live as soon as the worker deploys. Migration 0018 is additive only, but it must be applied to remote D1 before the deploy.SKILLX_TOKEN_SECRETgates only OIDC trusted publishing and signed downloads.Breaking (CLI):
skillx release publish --jsonnow prints the{ok, data}/{ok:false, error}envelope instead of the bare release object (commitfeat(cli)!with aBREAKING CHANGEfooter).#34 checklist → evidence
skillx packreuses the packerpublish→importpackages/cli/src/commands/import.ts;publish owner/repo/ GitHub URL still works with a stderr deprecation warning (publish.tspublishMode, tests inrelease-commands.test.ts,built-cli.test.ts)skillx validatecommands/validate.ts: packs, requires SKILL.md frontmatter with name/description, checks name/semver/kind/channel, exit 2 withdetails.problemsskillx packcommands/pack.ts; built CLI packs the same commit twice → identicalreleaseDigestand identical tar bytes (built-cli.test.ts, also checked by hand:sha256:b224…3cc0twice)skillx publish <dir>commands/release-publish.ts;release pack/publishkept as aliases; JSON envelope + standard exit codes (0/1/2/3/5/6)skillx promotecommands/promote.ts→POST …/releases/:version/promote(release-promote.ts): audited compare-and-set of the channel (subject_kindrelease_channel); scoped keys / OIDC tokens may only promote, demotion needs owner or admin (release-promote.test.ts)release-idempotency.ts+release_publish_idempotencytable (24 h): same key + same request replays the stored answer; same key + different request →422 idempotency.key_reused; in-flight duplicate →409(release-idempotency.test.ts)sxo_scopedreleases:publish:@handle, 15 mingithub-oidc.ts(RS256 only, JWKS fromtoken.actions.githubusercontent.com, iss/aud/exp/iat/nbf, claim checks),oidc-exchange.ts,trusted-publishers.tsadmin API;oidc-trusted-publishing.test.ts(15 tests with a generated RSA key + JWKS)GET /api/v1/admin/quarantine,POST …/releases/:version/reviewapprove/reject; approve restores the pre-quarantine state (a yanked release stays yanked unlessrestoreTo: "published"), audited inrelease_state_audit+release_reviews(release-review.test.ts)docs/release-resolution-policy.md(states, channel rules, operator review)POST /api/v1/releases/:digest/artifact/url→ 5 min HMAC token,GET /api/v1/downloads/:token; public releases keep the plain artifact route (release-download-url.test.ts)docs/deployment-guide.md.github/workflows/publish-agentkit-foundation.yml(dispatch only,expected-digestinput fails the job before upload if the CI pack differs from the manual one)Acceptance criteria:
skillx packand compares withexpected-digest; deterministic-pack test above.sxo_tokens and scoped keys are verified only inrelease-auth.ts(publish, promote). Legacy routes look keys up by plain hash and never match them (scoped-key-isolation.test.ts); the state API refuses scoped keys; review and trusted-publisher APIs are admin-secret only.ak: no installer code touched.OIDC: what is safe and what was left out
Shipped: verification of GitHub's RS256 JWT against its JWKS (cached 10 min, refetch on unknown
kidat most once a minute, keys up to 70 min old still verify while GitHub's JWKS endpoint fails), audience pinned (SKILLX_OIDC_AUDIENCE, defaultskillx), a max token lifetime of 1 h with 60 s skew, matching on the numericrepository_id(resists repo rename/resurrection) plus repository, workflow path, and optional ref and environment; onlypush,workflow_dispatchandreleaseevents may publish (allowlist);jtiis stored for 7 days, so a replayed JWT gets409. The exchanged token is HMAC-signed, scoped to one publisher, and lasts 15 minutes.Not shipped: OIDC for anything other than publish and promote, and wildcard workflow or ref patterns. Both widen trust in ways that need a product decision.
AgentKit foundation naming (
@bestagentkits/<x>)The workflow publishes the kit skill directory
kits/<kit>/skills/ak-<x>as@bestagentkits/<x>. Theak-prefix is dropped, and the version must equal theversioninkits/<kit>/kit.yaml(the job fails otherwise). Slugs starting withak-orak:are refused by the job and by the registry. The packed file set is the skill directory at the pinned commit, which is the tree AgentKit hashes, so thereleaseDigestequals AgentKit'sagentkit.skill-tree/v1digest. AgentKit references these releases asskillx:bestagentkits/<x>.This PR adds no
@agentkit/*exception. Reservation, contract and vectors are unchanged. Three things block it:skillx.release/v1contract always validates identities as providerskillx, so@agentkit/*is invalid. The golden vectors also require an@agentkitmanifest to be rejected.sourceProvenanceis a closed{repo, commit, path}. Addingproviderwould break every v1 verifier, including the npm CLI 0.5.0 and the AgentKit Go client.@agentkit.The real fix is a
skillx.release/v2with kit provenance, tracked in #100.SKILLX_AGENTKIT_PUBLISH_KEYkeeps its name; its value must be a key scoped toreleases:publish:@bestagentkits.Migrations
0018_registry_publishing.sql(strictly additive:release_publish_idempotency,release_reviews,trusted_publishers,oidc_token_exchanges, their indexes (including cleanup indexes onrelease_publish_idempotency.expires_atandoidc_token_exchanges.created_at) and two triggers; no existing table changes; rollback SQL in the header). It is numbered 0018 because the journal already has 18 entries (feat(registry)!: release CLI, idempotent publish, promote, quarantine review, signed downloads and GitHub OIDC trusted publishing #99 merges before feat: collections API and verified CLI installer with lockfile, sync and local MCP (#35 phase 3) #98, which renumbers to 0019). Apply it to remote D1 before deploying: the release API is already on in production, and publish withIdempotency-Key, the OIDC exchange, trusted publishers and review use these tables. Applied--localonly.Env / secrets
SKILLX_TOKEN_SECRET(new Worker secret, ≥ 32 chars). It gates only OIDC trusted publishing (503 oidc.unavailablewithout it) and signed download URLs (503); key-based publish, idempotency, promote and review work without it.SKILLX_OIDC_AUDIENCE(optional var, defaultskillx).skillx-publish: restrict deployment branches tomainand require reviewers. Secrets:SKILLX_AGENTKIT_PUBLISH_KEY(fallback key scoped toreleases:publish:@bestagentkits) andAGENTKIT_READ_TOKEN(only while agentkit is private).docs/deployment-guide.md→ "Registry publishing additions".Verification
pnpm typecheck✅ ·pnpm test✅ (103 files, 2127 passed, 2 skipped) ·apps/web/tests/releases8 consecutive runs, 246/246 each ·release-review+release-download-url25 consecutive runs, 0 failures ·pnpm --filter web build4 consecutive runs, all exit 0 ·drizzle-kit generate: no schema driftpackages/cli/src/lib/git-tree-tar.test.ts(94-file git pack under full-suite load on Windows; untouched here; passes alone).npm install --ignore-scripts --omit=devfrom thepnpm packtarball runs--versionandpack(the workflow's new install path).validateexit 0;pack×2 → same digest, same tar hash.wrangler d1 migrations apply DB --local✅ (no remote run).Review fixes (head
cd10964→5829807)release_state_audit(walks the same-timestamp edge chain, sinceyanked → quarantinedruns throughpublished); a yanked release staysyanked,{"restoreTo":"published"}publishes it; the approval is pinned to the quarantine it read viaquarantined_at. Test: yank → quarantine → approve, plus validation ofrestoreTo. Documented indocs/release-resolution-policy.mdand the API reference.2xx,400,415and409 release.version_exists(isReplayable); everything else releases the claim (test: a413subrequest-budget refusal, then a successful retry with the same key). An oversize upload is413before any claim.PreparedUpload): one buffer, one auth.build-clijob (contents: read, no id-token, no environment) runspnpm install/build/pnpm pack;publishinstalls the tarball with--ignore-scripts. Runbook:skillx-publishlimited tomainwith required reviewers.push,workflow_dispatch,release(tests for 6 refused and 2 more accepted events).feat(cli)!commit with theBREAKING CHANGEfooter for the--jsonenvelope; the CLI README documents it and theimportrename.X-SkillX-Release-Channelonly on the short-lived version route, never on immutable digest routes; policy states the channel is an unsigned listing field (signed statement: skillx.release/v2: kit provenance (provider/origin) to allow @agentkit foundation releases #100).oidc.unavailable(read fromerror.details.code). n2::runs and line breaks stripped from server text before::error::. n3 stale JWKS grace (60 min past the TTL). n4 expiredrelease_publish_idempotencyrows andoidc_token_exchangesolder than 7 days deleted in batches of 50 on the write path. n5registry-schema.tsaudit comments updated.Re-review fixes (head
5829807→d486263)stateBeforeQuarantinereads the last two applied audit edges byrowid(true write order; the table is append-only) and treats only the exact composite pairyanked → published+published → quarantinedfrom one batch (samecreated_at) as a yank; a directyanked → quarantinededge also counts. Anything else ispublished. Tests: yank and quarantine in the same millisecond with frozenDate(fails on the previous code), and an approval racing a re-quarantine (quarantined_atguard →409, no review row).fromBase64urlinrelease-tokens.tsaccepts only canonical encodings (re-encoding must equal the input), so a MAC has one spelling. The tamper test changes a middle MAC character and also checks a padding-bit respelling of the last one →404.JWKS_MIN_REFRESH_MS(lastFailureAt); the test counts fetch attempts.oidc-exchange.tsindentation and JSDoc placement fixed; cleanup indexes folded into migration 0018 (schema, SQL, snapshot, rollback lines); PR retitledfeat(registry)!:.Notes for review
sx-phase1-discovery(not merged) addslib/output.ts+lib/run-command.tshelpers. This branch keeps its own smalllib/release-cli.tswith the same envelope and exit codes; once phase 1 lands, the helpers can be consolidated. A dry merge with that branch conflicts only inpackages/cli/src/built-cli.test.ts. A dry merge withsx-bugs-47-49-61-79is clean.release-read.ts,release-state-http.tsandingest-release.tswere already near or over the 200-line guideline on main; this PR grows them slightly..github/workflows/publish-release.ymlnow reads the envelope (j.data ?? j).Refs #34 #54 #88 #100
🤖 Generated with Claude Code