feat(skills): catalog with signed manifest installs#1812
Conversation
Promote the MCP catalog browse / list / search / install / upgrade / info / status surface to skill packs. Source variants (github, git, npm, file, directory) resolve through the existing plugin_installer; catalog manifests carry an Ed25519 signature that the install verifies against the catalog's signer_pubkey. Every install / upgrade appends a skill.catalog.install event to the HMAC-chained audit log with (manifest_url, manifest_sha256, manifest_signer_pubkey, install_id, prev_chain_digest); replaying the chain pulls the identical sha and refuses installation if the upstream sha drifted. skills.lock is extended with [[catalog]] rows and [[lineage_receipt]] rows so two parallel worktrees launched from the same chain head observe identical skill versions, and an upgrade in one worktree produces a deterministic adopt/pin decision in the other. The existing lineage-v1 gate is extended with check_skill_lockfile to reject PRs whose lockfile references a manifest sha not present in the chain's known-good set. Catalog cache lives under .sdd/skills_catalog/ with revalidation honouring BERNSTEIN_SKILLS_CATALOG_TTL. Closes #1796
There was a problem hiding this comment.
Sorry @chernistry, you have reached your weekly rate limit of 2500000 diff characters.
Please try again later or upgrade to continue using Sourcery
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (16)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Sonar insights (advisory, no merge-block)Snapshot of
Run This comment is a soft signal. The Sonar scan runs on push to |
Review-bot acknowledgement summary
All must-address findings are resolved or acknowledged. |
|
bernstein doctor observe for PR #1812 ( sonar -- WARN (project bernstein)
code-scanning -- WARN (27 open alert(s))
Skipped backends (credentials not configured)
See docs/observability/unified-doctor.md for backend setup notes. |
Mutation gate (fixed critical paths)
Surviving mutations (top 20)
|
Contract drift detected - proposed patchInline autofix push failed ( Three contract tests act as drift detectors against the public CLI / API surface:
One or more failed on this PR. Files changed: How to applyEither run the regen script locally: uv run python scripts/regen_contract_drift.py --fixture all
git add -A && git commit -m "chore(ci): regenerate contract drift allow-lists"
git pushOr apply the patch directly: gh pr checkout 1812
git apply <<'PATCH'
diff --git a/tests/unit/test_readme_api_coverage.py b/tests/unit/test_readme_api_coverage.py
index fa6b08a6..90b3d71d 100644
--- a/tests/unit/test_readme_api_coverage.py
+++ b/tests/unit/test_readme_api_coverage.py
@@ -239,6 +239,8 @@ DOCUMENTED_COMMANDS: frozenset[str] = frozenset(
"desktop-register",
# Bot-added: drift autofix (regen_contract_drift.py)
"supervisor",
+ # Bot-added: drift autofix (regen_contract_drift.py)
+ "schedule",
}
)
PATCH
git add -A && git commit -m "chore(ci): regenerate contract drift allow-lists"
git pushFull diffdiff --git a/tests/unit/test_readme_api_coverage.py b/tests/unit/test_readme_api_coverage.py
index fa6b08a6..90b3d71d 100644
--- a/tests/unit/test_readme_api_coverage.py
+++ b/tests/unit/test_readme_api_coverage.py
@@ -239,6 +239,8 @@ DOCUMENTED_COMMANDS: frozenset[str] = frozenset(
"desktop-register",
# Bot-added: drift autofix (regen_contract_drift.py)
"supervisor",
+ # Bot-added: drift autofix (regen_contract_drift.py)
+ "schedule",
}
)Source CI run: https://github.com/sipyourdrink-ltd/bernstein/actions/runs/26251528553 Refs #1273. |
| from rich.table import Table | ||
|
|
||
| service = _build_service(scope) | ||
| rows = service.list_installed() |
| def info_cmd(entry_id: str, refresh: bool, scope: str) -> None: | ||
| """Show full info for a single catalog entry.""" | ||
| service = _build_service(scope) | ||
| entry = service.info(entry_id, force_refresh=refresh) |
| def sync_cmd(scope: str) -> None: | ||
| """Detect lockfile vs on-disk drift.""" | ||
| service = _build_service(scope) | ||
| drift = service.sync() |
| def status_cmd(scope: str) -> None: | ||
| """Show cache + lockfile state for ``skills catalog``.""" | ||
| service = _build_service(scope) | ||
| status = service.status() |
| return GateResult(ok=False, failures=["skill catalog lockfile module is missing"]) | ||
|
|
||
| state = read_state(lockfile_path) | ||
| failures: list[str] = [] |
| swaps the upstream while keeping the URL is caught. | ||
| """ | ||
| canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") | ||
| hasher = hashlib.sha256() |
| swaps the upstream while keeping the URL is caught. | ||
| """ | ||
| canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") | ||
| hasher = hashlib.sha256() |
| plugin_installer: InstallerCallable | None = None, | ||
| preloaded_catalog: SkillCatalog | None = None, | ||
| ) -> None: | ||
| if fetcher is None and preloaded_catalog is None: |
| ) -> UpgradeOutcome: | ||
| """Upgrade a single installed catalog entry.""" | ||
| state = read_state(self.lockfile_path) | ||
| prior = state.find_catalog(entry_id) |
| raise SkillCatalogError(f"{entry_id!r} is not installed") | ||
|
|
||
| catalog = self.browse(force_refresh=force_refresh) | ||
| upstream = catalog.find(entry_id) |
Summary
Promote the MCP catalog browse / list / search / install / upgrade / info / status surface to skill packs. Source variants (
github,git,npm,file,directory) resolve through the existingplugin_installer; manifests carry an Ed25519 signature that install refuses unless--allow-unverifiedis set. Each install appends askill.catalog.installaudit-chain event with(manifest_url, manifest_sha256, manifest_signer_pubkey, install_id, prev_chain_digest); replaying the chain pulls the identical sha and refuses if upstream drifted.skills.lockis extended with[[catalog]]rows and[[lineage_receipt]]rows so two parallel worktrees launched from the same chain head observe identical skill versions; an upgrade in one produces a deterministic adopt/pin decision in the other. The existing lineage-v1 gate is extended (not replaced) withcheck_skill_lockfileto reject PRs whose lockfile references a manifest sha not present in the chain's known-good set. Catalog cache lives under.sdd/skills_catalog/with revalidation honouringBERNSTEIN_SKILLS_CATALOG_TTL.Closes #1796
Files touched
src/bernstein/cli/commands/skills_catalog_cmd.py(new): CLI surfacesrc/bernstein/cli/main.py: wirebernstein skills catalogsubgroupsrc/bernstein/core/skills/catalog/__init__.py(new): public re-exportssrc/bernstein/core/skills/catalog/manifest.py(new): strict schema + validatorsrc/bernstein/core/skills/catalog/signature.py(new): Ed25519 sign/verifysrc/bernstein/core/skills/catalog/fetcher.py(new): HTTP fetcher + cachesrc/bernstein/core/skills/catalog/audit.py(new): audit chain integrationsrc/bernstein/core/skills/catalog/installer.py(new): wrapsplugin_installersrc/bernstein/core/skills/catalog/lockfile.py(new): atomic lockfile + receiptssrc/bernstein/core/skills/catalog/service.py(new): high-level servicesrc/bernstein/core/lineage/gate.py: additivecheck_skill_lockfiletests/unit/core/skills/test_catalog.py(new): 25 unit teststests/integration/test_skill_catalog_cross_worktree.py(new): 3 integration testsdocs/operations/skills-catalog.md(new): operator docsCHANGELOG.md: Unreleased entryAcceptance criteria
bernstein skills catalogexposes browse, list, search, install, upgrade<id>/--all, info, status modelled onbernstein mcp catalog(manifest_url, manifest_sha256, manifest_signer_pubkey, install_id, prev_chain_digest); replay refuses on upstream sha drift--allow-unverified.sdd/skills_catalog/withBERNSTEIN_SKILLS_CATALOG_TTLenv overrideplugin_installerand writes into.bernstein/skills/<name>/skills.lockupdates atomically;bernstein skills catalog syncdetects drift; two parallel worktrees from the same chain head observe identical skill versions; upgrade in wt-a produces a lineage receipt that wt-b uses to decide adopt or pinTest plan
uv run pytest tests/unit/core/skills/test_catalog.py tests/integration/test_skill_catalog_cross_worktree.py(28 tests pass)uv run pytest tests/unit/skills/(250 existing skill tests pass)uv run pytest tests/unit/lineage/test_gate.py(23 lineage-gate tests pass)uv run pytest tests/unit -k "skill or catalog"(603 tests pass)uv run ruff checkclean on all new/touched filesbernstein skills catalog --helpexposes all subcommands