Repository navigation
feat(contracts): signed skillx.release/v1 manifest, release status and migration 0011 - #58
Merged
Merged
Conversation
…and identity rules Add @skillx/skill-tree, a TypeScript implementation of the AgentKit skill tree contract shared with the AgentKit CLI: - canonical serialization and sha256 tree digest - path, NFC and case-fold collision rules on Unicode 15.0.0 tables generated from pinned inputs, never runtime Intl data - streaming tar/tar.gz and zip readers with exact entry, file and byte limits enforced while content streams (pull-driven inflate) - package identity, alias and directory alias rules, provenance trust and frontmatter exposure The AgentKit conformance vectors are vendored unedited with a provenance note and sha256 drift tests; every tree and identity vector passes. Negative tests cover links, special entries, sparse files, unsafe modes and names, case collisions, damaged archives and decompression bombs. Refs #54
…uests The repository has no pull request CI yet, so run the package typecheck and its conformance suite when the package, the lockfile or the test config change. Refs #54
- NFC/NFD follow x/text stream-safe processing: a run of more than 30 non-starters gets a CGJ, so such a path is non-NFC; composition ports reorderBuffer.compose; counts and differential vectors come from Go - header modes outside a safe, non-negative 7-digit octal value are rejected before the type bits are cleared - zip entries with header faults or non-regular types are unsupported without inflating their data or counting toward file and byte limits - tar reads stay within a total byte budget covering non-regular data and PAX/GNU extension chains - GNU sparse files apply GNU.sparse.name, size and realsize and validate their maps as Go does; malformed maps fail with tree.read_failed - numeric and PAX parsing match Go: NUL-cut octal, negative base-256, signed and zero-padded PAX lengths, PAX times, all header numerics - an empty GNU long name clears an earlier one; PAX records live in a prototype-free object; non-safe sizes fail the limit step - collision keys fold each path once; vector harness asserts reader coverage
… guard Rewrite type NUL from the merged name before sparse handling, as Go Reader.Next does, and use that type for the data length, the sparse header-only check and the entry kind. Check the raw header size for negativity before a PAX size overrides it. The tar byte budget now counts only bytes that are not hashed file content, at maxFiles x 64 KiB, and its detail names the ingest overhead guard. Document the guard and the nodejs_compat requirement.
Pin that a type NUL entry whose trailing slash comes from a PAX path or a GNU long name reads no data, and keep the vector helper from writing data for a type NUL directory.
Export the path, collision and identity rules from @skillx/skill-tree/rules so Worker code can validate manifests without pulling node:crypto or the archive readers into its program.
Define the release manifest that pins a package version to its agentkit.skill-tree/v1 tree digest, a strict validator, byte-exact canonical JSON for the signing payload, and WebCrypto Ed25519 signing and fail-closed verification compatible with the AgentKit registry style. Ship cross-language vectors signed with a key derived from a public seed text, plus a skill-tree test that recomputes each vector's tree digest.
Add release_digest, file_count, tree_size, signed_manifest_key and quarantined_at to package_releases, packages.visibility and publishers.state with safe defaults, and a release digest index. The generator re-emitted the registry tables missing from the 0010 snapshot; the SQL keeps only the additive delta and records rollback SQL.
Keep the skillx.release/v1 manifest immutable per release digest and move the mutable state into skillx.release-status/v1: release digest, state, optional reason, issuedAt, a strictly increasing sequence and the signing key, under the same canonical bytes and Ed25519 rules. Verification fails closed on an unknown state, a status for another release, a bad signature, a lower sequence (rollback) and a different status with the accepted sequence (conflict). Ship cross-language status vectors, regenerate the manifest vectors without state, and record the last status sequence and issue time in migration 0011. Document that a release without source provenance must be labeled provenance unknown.
The same tree can be released under several identities or versions, so a status keyed only by releaseDigest would apply to every sibling release. Add a required manifestDigest: the sha256 of the manifest signing payload, the exact bytes the manifest signature covers. Verification now rejects a status for another manifest with status.manifest_mismatch, and rollback and conflict checks track the sequence per manifest digest. Vectors list the signed manifests with their digests and cover one tree under two versions and a missing or wrong manifest digest. Document that the registry stores the full signed status document, so reason needs no database column.
Move the agentkit.skill-tree/v1 serialization (header, per-file lines, UTF-8 path ordering) into a module without Node.js imports, and export serializeTree, compareUtf8 and sortFiles from @skillx/skill-tree/rules. Worker code can now render the exact digest bytes and hash them with WebCrypto. The root entry keeps treeDigest over node:crypto.
Manifest: - sign and verify require releaseDigest to be the tree digest of files (release.tree_digest_mismatch), hashed with WebCrypto - verifiers take the received bytes and require them to equal the canonical JSON (release.non_canonical), rejecting duplicate members, number and escape variants, whitespace, BOM and invalid UTF-8 - release versions reject build metadata and leading zeros (max 128) - signatures must use the single canonical base64 text - validators reject non-plain objects and sparse arrays without throwing Status: - required expiresAt, at most 7 days after issuedAt; verify against a caller-supplied now (status.expired) and the verified manifest - issuedAt not before the manifest createdAt, later than the previous accepted status, and transitions enforced (status.invalid_transition) - reason bounded to 512 UTF-8 bytes without control, format, bidi or separator characters; non-canonical bytes rejected Signed-release fixtures leave the package root export. Vectors become received documents with one expected code each, add expiry, lifetime, transition, non-canonical and UTF-8 path ordering cases, and the docs describe the byte rules, expiry policy and registry-signed trust mapping.
Record the d1_migrations row removal in the 0011 rollback SQL so a later apply runs the migration again, and select migrations by name in its test so a future migration does not shift the 0010 baseline.
…lasses Reject a signed release status whose issuedAt is more than 300 seconds ahead of the consumer clock with status.not_yet_valid, checked after the signature like expiry, with vectors at and beyond the skew edge. Classify reason format characters with the pinned Unicode 15.0 tables that the tree path rules use, so every runtime agrees on the verdict. Add manifest and status vectors whose signature has non-zero unused base64 bits, rejected by the signature pattern although lenient decoders return the original bytes. Document that consumers must not map these releases to publisher trust, the 24 hour re-sign cadence under the 7 day cap, the issuer obligation to advance issuedAt, and the two-step quarantine lift.
mrgoonie
marked this pull request as ready for review
October 13, 2026 05:42
…ntract # Conflicts: # packages/skill-tree/README.md # packages/skill-tree/package.json # packages/skill-tree/src/serialize.ts
1 of 7 tasks
Closed
6 tasks
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
This is slice SX-2 of #54: the signed
skillx.release/v1contract, a separate signedskillx.release-status/v1document, and an additive D1 migration 0011. It is stacked on #57 (skill tree digest) and should be reviewed after it.skillx.release/v1manifest: immutable, one per release. It is signed with Ed25519 over canonical JSON (the payload excludessignature) and carriestreeDigest, identity, publisher (verifiedBy: none|github|admin), limits andsourceProvenance(nullable; consumers must label a null value "provenance unknown"). It has nostate, so it never changes after signing.skillx.release-status/v1status:manifestDigest, the sha256 of the manifest signing payload.status.rollback).status.conflict).status.manifest_mismatch).packages/contracts/src/fixtures/. A Go verifier (AgentKit side) reproduced every valid and invalid result.status_sequenceandstatus_issued_at, with rollback SQL in the header. A test applies 0000..0011 on SQLite and checks that a database already at 0010 keeps its rows.@skillx/skill-tree/rules: a new entry point with no Node imports, so the web typecheck can use the identity rules.Not in this PR (owner only)
Notes
drizzle-kit generatewanted to re-create seven tables because the 0010 snapshot is missing them; the generated 0011 snapshot is kept, and the header explains this.reasoncolumn.packages/cli/src/version.test.tsneeds a barepnpmon PATH.apps/web/tests/tsconfig.jsonalready reports type errors.Validation (offline)
packages/contracts: 203 passed, 2 skipped (the vector writers).packages/skill-tree: 583 passed.apps/web: 99 passed.Refs #54, bestagentkits/agentkit#2277
Review rounds
expiresAt(7 day cap,status.expiredchecked after the signature), exact canonical bytes, signed state transition graph, re-appliable 0011 rollback, strict release semver, boundedreason.status.not_yet_validforissuedAtmore than 300 s ahead, pinned Unicode 15.0 classes forreason, base64 padding-bit vectors, docs for the 24 h re-sign cadence,issuedAtmonotonicity and the two-step quarantine lift.Follow-up
trustOfin skill-treeidentity.tsstill returnspublisher-signedfor a verified skillx identity because it mirrors the vendored AgentKit identity vectors. It changes together with those vectors when bestagentkits/agentkit#2277 addsregistry-signed.