Skip to content

Commit 2d5b3a3

Browse files
chernistryauto-heal-fixupgithub-actions[bot]
authored
feat(skills): catalog with signed manifest installs (#1812)
* feat(skills): catalog with signed manifest installs 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 * chore(auto): regenerate AGENTS.md mirrors + ruff format --------- Co-authored-by: auto-heal-fixup <auto-heal-fixup@bernstein.local> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 6160ea6 commit 2d5b3a3

17 files changed

Lines changed: 4458 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All notable project changes are tracked here (code + docs).
66

77
### Added
88

9+
- `bernstein skills catalog` command group promotes 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 under `.sdd/audit/` with `(manifest_url, manifest_sha256, manifest_signer_pubkey, install_id, prev_chain_digest)`; reverting and 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 (`bernstein.core.lineage.gate.check_skill_lockfile`) rejects 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` (#1796).
910
- `bernstein desktop-register --host <name>` covers the remaining priority hosts: Cursor, Continue, Cline, Zed, and Aider, alongside the existing Claude Desktop and Claude Code adapters. JSON hosts merge into their canonical `mcpServers` map (or `context_servers` for Zed); Aider records the entry in its YAML config under `mcp-servers` for community-wrapper consumption (#1676).
1011
- `bernstein doctor --substrate` reports which detected hosts have Bernstein registered, which do not, and which are stale (canonical command/args differ from the recorded entry) (#1676).
1112
- Operator docs at `docs/substrate/{cursor,continue,cline,zed,aider}.md` cover install, verification, and uninstall per host (#1676).

docs/operations/skills-catalog.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Skill catalog with signed manifest installs
2+
3+
The skill catalog promotes the same browse / list / search / install /
4+
upgrade / info / status surface that `bernstein mcp catalog` already
5+
ships - this time for skill packs. Catalog entries point at installable
6+
sources (github, git, npm, file, directory) and carry a content digest
7+
plus an optional Ed25519 signature so the operator (or an auditor) can
8+
prove what bytes landed in `.bernstein/skills/`.
9+
10+
## Commands
11+
12+
```
13+
bernstein skills catalog browse
14+
bernstein skills catalog list
15+
bernstein skills catalog search <query>
16+
bernstein skills catalog info <id>
17+
bernstein skills catalog install <id> [--allow-unverified] [--refresh] [--scope project|user]
18+
bernstein skills catalog upgrade <id> [--all] [--allow-unverified]
19+
bernstein skills catalog uninstall <id>
20+
bernstein skills catalog sync
21+
bernstein skills catalog status
22+
```
23+
24+
All commands honour `--scope project` (writes into
25+
`<cwd>/.bernstein/skills/`) or `--scope user`
26+
(`~/.bernstein/skills/`). The default is `project`.
27+
28+
## Manifest schema
29+
30+
A catalog entry is a strict JSON object. Unknown fields reject the
31+
fetch, identical to the MCP catalog schema:
32+
33+
```json
34+
{
35+
"id": "code-review",
36+
"name": "code-review",
37+
"version": "1.0.0",
38+
"description": "Review code diffs and surface risk hot-spots.",
39+
"source": {
40+
"kind": "github",
41+
"repo": "acme/code-review-skill",
42+
"tag": "v1.0.0"
43+
},
44+
"content_digest": "<64-char hex SHA-256>",
45+
"signature": "<base64url Ed25519>",
46+
"verified": true,
47+
"tags": ["review", "security"]
48+
}
49+
```
50+
51+
Supported `source.kind` values: `github`, `git`, `npm`, `file`,
52+
`directory`. Each variant maps onto the existing
53+
[`plugin_installer`](../../src/bernstein/core/plugins_core/plugin_installer.py)
54+
implementation; the catalog does not introduce new download or extract
55+
logic.
56+
57+
## Signature workflow
58+
59+
1. The publisher generates an Ed25519 keypair via
60+
`bernstein.core.skills.catalog.generate_signer_keypair()` (thin
61+
wrapper around the lineage layer's existing primitive).
62+
2. The publisher signs each entry with `sign_entry(entry, private_pem)`
63+
and attaches the base64url-encoded signature on the `signature`
64+
field.
65+
3. The catalog publishes the matching public key on the top-level
66+
`signer_pubkey` field.
67+
4. The install path runs `verify_entry(entry, signer_pubkey)`. An entry
68+
without a signature, or with a signature that does not verify, is
69+
refused unless the operator passes `--allow-unverified`. An
70+
unverified install still proceeds but the audit event records
71+
`manifest_signer_pubkey=null`.
72+
73+
The signed payload is the canonical JSON of the entry, deliberately
74+
excluding the `signature` and `verified` fields so the signature is
75+
neither self-referential nor sensitive to operator-side flags.
76+
77+
## Audit chain integration
78+
79+
Every install / upgrade / uninstall appends an HMAC-chained event under
80+
`.sdd/audit/`, reusing
81+
[`bernstein.core.security.audit.AuditLog`](../../src/bernstein/core/security/audit.py):
82+
83+
| Event type | Payload fields |
84+
|----------------------------|---------------------------------------------------------------------------------------------------------------|
85+
| `skill.catalog.fetch` | `source_url`, `from_cache`, `revalidated` |
86+
| `skill.catalog.install` | `manifest_url`, `manifest_sha256`, `manifest_signer_pubkey`, `install_id`, `prev_chain_digest` |
87+
| `skill.catalog.upgrade` | `from_version`, `to_version`, `manifest_url`, `manifest_sha256`, `install_id`, `prev_chain_digest` |
88+
| `skill.catalog.uninstall` | (none) |
89+
| `skill.catalog.sync` | `lockfile_digest`, `lineage_receipt` |
90+
91+
Reverting and re-running the chain pulls the identical manifest sha; the
92+
install refuses if the upstream sha drifted (a guard against silent
93+
upstream rewrites).
94+
95+
## Lockfile and lineage receipts
96+
97+
The lifecycle's `skills.lock` is extended with two additional TOML
98+
arrays:
99+
100+
```toml
101+
[[catalog]]
102+
id = "code-review"
103+
name = "code-review"
104+
version = "1.0.0"
105+
manifest_url = "github://acme/code-review-skill@v1.0.0"
106+
manifest_sha256 = "..."
107+
content_digest = "..."
108+
install_id = "..."
109+
chain_head = "..."
110+
installed_at = "2026-05-21T00:00:00+00:00"
111+
112+
[[lineage_receipt]]
113+
worktree_id = "..."
114+
action = "install" # one of: install, adopt, pin
115+
entry_id = "code-review"
116+
from_chain_head = "0000..."
117+
to_chain_head = "..."
118+
manifest_sha256 = "..."
119+
timestamp = "2026-05-21T00:00:00+00:00"
120+
```
121+
122+
Writes are atomic (`Path.replace` on a sibling `.tmp` file) so a
123+
concurrent reader either sees the old or the new state - never a partial
124+
write. Two parallel worktrees launched from the same chain head observe
125+
identical lockfile digests; an upgrade applied to one worktree produces
126+
a `RECEIPT_ADOPT` receipt that the sibling can consult to decide
127+
deterministically between `RECEIPT_ADOPT` (re-run the install) or
128+
`RECEIPT_PIN` (stay on the prior chain head).
129+
130+
## CI lineage gate
131+
132+
`bernstein.core.lineage.gate.check_skill_lockfile` extends the existing
133+
lineage-v1 gate (it does NOT add a new gate). The check passes when
134+
every `[[catalog]]` row's `manifest_sha256` is present in the audit
135+
chain's known-good set; a row whose sha is not anchored fails CI.
136+
137+
```python
138+
from bernstein.core.lineage.gate import check_skill_lockfile
139+
140+
result = check_skill_lockfile(
141+
Path("skills.lock"),
142+
frozenset(auditor.known_good_manifest_shas()),
143+
)
144+
if not result.ok:
145+
raise SystemExit("\n".join(result.failures))
146+
```
147+
148+
## Cache and TTL
149+
150+
The fetcher caches the upstream catalog under
151+
`.sdd/skills_catalog/catalog.json` (project-local). The cache TTL
152+
defaults to 6 hours; operators override it via
153+
`BERNSTEIN_SKILLS_CATALOG_TTL` (seconds). The cache and the audit log
154+
share a single source of truth: a stale fetch on a 5xx upstream serves
155+
the last validated copy instead of failing.
156+
157+
## Drift detection
158+
159+
`bernstein skills catalog sync` recomputes the on-disk content digest
160+
for every installed catalog skill and reports rows that do not match the
161+
lockfile. Drift indicates either a manual edit under
162+
`.bernstein/skills/<name>/` or an upstream rewrite; either is
163+
operator-actionable, never silently re-installed.

0 commit comments

Comments
 (0)