Repository navigation
feat: collections API and verified CLI installer with lockfile, sync and local MCP (#35 phase 3) - #98
Merged
Merged
Conversation
Contributor
Author
|
This affects the verified CLI installer here. #100 adds An installer built on it should do three things:
Both PRs touch Generated by Claude Code |
…d members Add the flag-gated /api/v1/collections API (SKILLX_COLLECTIONS_API) over the existing collections tables: create, metadata update, delete, clone with lineage, full-replace revisions and member add/remove as compare-and-set revisions. Revisions are digested and immutable (trigger), and content passes a shared sync-leak scanner that refuses secrets and absolute local paths. Contracts gain the skillx.lock/v1 lockfile schema used by the installer. Refs #35 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… MCP Adds skillx install, update, uninstall, list, sync --locked and mcp. Sources (kit revision, collection revision or packages) resolve to exact versions and digests in a skillx.lock/v1 lock; every manifest signature and file SHA-256 is verified before a staged, journaled rename into the Claude Code or Codex skills directory. Receipts prove ownership, so uninstall keeps edited and unknown files and shared skills are ref-counted. Interrupted runs recover on rerun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ons API Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The registry publishing migration takes 0018 and merges first; the collection revision lineage migration and its snapshot now follow it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d during install - uninstall deletes only regular files under a link-free path; a linked skill directory or subdirectory is left alone - re-check the target after staging: new or edited files are a conflict; files that appear after the check are saved to backups, not deleted - retry renames refused by Windows, then roll back and report a conflict - validate manifest and overlay paths and write only inside the target - a skill path that is a link or file is always unowned_directory - check skillx.json compatibility before fetching other files and refuse malformed declarations - collection locks must pin exactly the revision members - MCP apply requires planDigest and rejects force - install lock is created atomically and taken over only from a dead pid - lock file keeps one scope: package installs merge, update refreshes it, uninstall drops the package - aliases reject a trailing dot and Windows device names - API keys are sent only over https (http only to loopback) - recovery drops owners no source records and removes unowned skills Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- recovery parks the old tree in the backups when the skill path has been taken by something else, instead of letting the staging sweep delete it; an unjournaled old tree in staging is parked the same way - a symlink or junction inside a skill directory is a contains_link conflict, even with --force; copying refuses non-regular files - retiring the old tree removes a late link itself and records its target, never following it - explicit packages are owned one per package for multi-package installs and packages-lock syncs; uninstall and update accept several packages - an install lock released between attempts is retried, not reported busy - recovery moves into journal-recovery.ts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ewritten - parking an old tree renames it into the backups; across volumes, or when the rename stays refused, regular files are copied and links recorded as <path>.link, so a junction is never copied as a directory - a link inside a skill directory is a conflict only for an install or update that rewrites it; same-version reinstalls, sync and new owners keep working - a multi-package uninstall drops each package from the lock right after it is removed; repeated packages are uninstalled once - only a vanished install.lock is retried; an empty one keeps the "remove <path>" hint Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mrgoonie
force-pushed
the
sx-phase3-install
branch
from
October 19, 2026 14:26
19b3966 to
1ae4c82
Compare
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.
Refs #35
Phase 3 of #35. It adds a collections web API and a verified CLI installer: the resolver, the
skillx.lock.jsonlockfile, atomic apply, receipts,sync --lockedand a local stdio MCP. Personal kits (#55) are reused as the kit source. The kit lock endpoint, overlays andkitTreeDigestare consumed as they are, with no new kit endpoints. The design is inplans/261019-1914-issues-31-88-delivery/phase3-design.md.Checklist → evidence
apps/web/app/lib/collections/*,routes/api.v1.collections.tsapps/web/tests/collections/collections-api.test.tscollection_revisions_immutabletrigger;revisionDigest(contractscollection-revision.ts)POST /:id/members(add[],remove[], CASexpectedRevision)cli/src/lib/install/resolver.ts,release-pins.ts,fetch-verify.ts(skillx.jsoncompatible, kitruntimes). Releases declare no dependency graph yet, so the resolver detects alias collisions and shared versionsinstall-units.test.ts,install-e2e.test.tsinstall-lock.ts(skillx.lock/v1, validator,lockDigest);lock-file.tscontracts/src/install-lock.test.ts,install-units.test.tsplanner.ts,install-operations.ts(planDigest)install-units.test.ts, MCP testfetch-verify.ts: signed manifest against trusted keys, identity/version/digest, per-file size + SHA-256, overlay digests, composition digestadapters.ts, Claude Code ($CLAUDE_CONFIG_DIR/skillsor~/.claude/skills)$CODEX_HOME/skillsor~/.codex/skills)install-state.ts(skillx.install-receipt/v1)files[]+ owners record (skillx.install-owners/v1)updateInstalled,updateaction with backupremove-owned.tsdeletes only regular files under a link-free path whose hash matches the receiptinstall-safety.test.ts(junction/symlink)--force; force backs up edits and keeps unknown fileskeptModified,keptUnknown)requiredByskillx sync --lockedsync-locked.ts,commands/install.tssync-leaks.ts(lock write/read, collection content)sync-leaks.test.ts,install-units.test.ts, collections 422local-mcp.ts,skillx mcp;applyrequiresconfirm: trueandplanDigest; noforceover MCPinstall-recovery.test.ts"local MCP",install-hardening.test.tsapply-transaction.tsjournal and recovery;CrashPointhooksinstall-recovery.test.ts: 5 crash points × (update, first install), rerun convergesAll installer tests run against the in-memory server (real loaders,
FakeD1,FakeR2) with tempHOME/USERPROFILE/CODEX_HOME/SKILLX_HOME. None of them touches real user state.Review fixes (isolated review of this PR)
remove-owned.ts+safe-fs.ts: the skill root and every parent must be a real directory (lstat), files must be regular; links are never descended, a linked skill dir is left aloneinstall-safety.test.ts: junction subdir, junction skill root, file symlinkapply-transaction.ts: after staging, re-prove the target against the receipt, stage and backup (tree-proof.ts); drift →conflict(local_modified), nothing swapped. Files appearing after the check are copied to~/.skillx/backups/...and reported asdisplaced, neverrmSync'dinstall-hardening.test.ts"files changed between plan and swap"unknown: ['.']planner.ts: link/file skill path isunowned_directoryin every branch, even with--forceinstall-safety.test.tsfetch-verify.tscallsvalidateOverlayand checks every manifest path;apply-transaction.tswrites only viacontainedPathinstall-safety.test.ts(../../evil.txt, absolute, drive,\)renameWithRetrywith backoff; on failure roll back viarecoverJournals, reportconflictinstall-hardening.test.ts(busy twice → ok; always busy → old tree whole, no journal/stage)sync-locked.tsmemberDriftinstall-safety.test.tsapplyrequiresplanDigest;force→usage_errorinstall-hardening.test.tsinstall.lock(atomic); stale takeover via rename + re-checkinstall-hardening.test.tsusage_error;updaterefreshes,uninstalldrops (--lock/--no-lock)install-safety.test.tsisInstallAlias: no trailing., no Windows device namesinstall-safety.test.tsassertSecureRegistry: https, or http only to loopback/*.localhostinstall-safety.test.tsrecoverdropsrequiredByowners missing from the owners record, removes skills left with noneinstall-hardening.test.tsskillx.json→incompatible;skillx.jsonchecked before other files are fetched; migration renumbered to 0019 on top of #99's 0018 snapshotinstall-hardening.test.tsRe-review fixes (head 2572985)
journal-recovery.ts(recovery moved out ofapply-transaction.ts): when the stage exists and something else now sits at the target, the.oldtree is parked in~/.skillx/backups/<runtime>/<alias>/<stamp>-previous-treeand reported inparked(also in theconflictdetails from a refused rename); an unjournaled.oldin staging is parked, never sweptinstall-hardening.test.ts"parks the old tree in the backups when something else takes its place" (rename hook createsdemo/editor.tmp, then EPERM)contains_linkconflict, even with--force, when the directory would be rewritten (see round 3);copyIntorefuses non-regular files;retireOldremoves a late link itself (target saved as<path>.link), never follows itinstall-hardening.test.ts"links inside a skill directory" (forced install refused; late junction removed, outside file intact, no stuck journal)packages:<name>) for multi-package installs and packages-lock syncs;uninstall/updateaccept several packages; "one package at a time" error removedinstall-e2e.test.ts"gives each package of a multi-package install or a packages-lock sync its own owner"install.lockread after EEXIST retries instead of reporting busycli-reference.md: a missing owners file during recovery removes receipted skills (a corrupt one stops the run); uninstalling a kit/collection leaves its lock file as isRound-3 fixes (head 19b3966)
park()copied junctions as real directoriesparkrenames the old tree into the backups (links kept as they are); across volumes, or when the rename stays refused, it falls back toretireOld, which copies regular files and records links as<path>.linkwithout following or recreating them, so no symlink creation is needed and no journal is left stuckinstall-hardening.test.ts"parks across volumes without following a junction in the old tree" (EXDEV forced;savedToholdsrefs.link, not the junction target's files)contains_linkbeforeunchangedinstall/update); same-version reinstall,sync --lockedand a new owner keep workinginstall-hardening.test.ts"are kept while nothing is rewritten, and block an update even with --force"uninstallRef(..., onUninstalled)runs after each owner is removed; the command drops that package from the lock right theninstall-e2e.test.ts"keeps the packages lock in step when a multi-package uninstall stops part way"ownersOfRefsdedupesinstall-e2e.test.ts(uninstall@alpha/demo @alpha/tools @alpha/demo)install.lockinstall-hardening.test.tslock test (empty holder)Migrations and flags
0019_collection_revision_lineage.sqlis additive: two nullable columns plus the immutability trigger, with rollback SQL in its header. It was applied only to local D1.SKILLX_COLLECTIONS_APIis"0"inwrangler.jsonc, so the collections endpoints answer 404 until it is enabled.Notes for reviewers
0018_registry_publishing(idx 18) then0019_collection_revision_lineage(idx 19), and the 0019 snapshot'sprevIdis main's 0018 snapshot id. The 0019 snapshot was rebuilt from main's final 0018 snapshot plus the two lineage columns: the earlier copy lacked feat(registry)!: release CLI, idempotent publish, promote, quarantine review, signed downloads and GitHub OIDC trusted publishing #99's two late indexes (idx_oidc_token_exchanges_created,idx_release_publish_idempotency_expires).drizzle-kit generatenow reports no schema changes.EMBEDDED_KEYSand reads~/.skillx/trusted-keys.json/SKILLX_TRUSTED_KEYS; with no key it fails closed. To make the installer usable by default, the operator needs to publish the production key so it can be embedded.SKILLX_PERSONAL_KITS_API, which is off in production.integrity_error(exit 1) andconflict(exit 8). Exit 7 is left torate_limitedfrom feat: finish Phase 1 discovery (CLI envelope, get_kit, WebMCP mirror, contracts in CLI) #97.publish.ts,release-*.ts, release routes, remote MCP, webmcp,api.user-api-keysand release-scan.Verification
pnpm typecheck✓pnpm --filter web exec drizzle-kit check✓,drizzle-kit generate: no schema changes ✓pnpm test(on main + this branch): 111 files, 2210 passed, 2 skipped ✓ (installer integration tests get a 30s per-test timeout; one full run hit the 5s default under load)pnpm --filter web build✓pnpm --filter skillx-sh build✓--helplists install/update/uninstall/list/sync/mcp.list --json --runtime codexreturns an empty envelope.syncwithout--lockedand an invalid ref both exit 2.mcpanswerstools/list.🤖 Generated with Claude Code