SkillStore against a SQLite database. Copy this directory into your codebase, customize per your storage needs, register with skillscript-runtime’s Registry. This README is written for the agent implementing your adopter’s connector — including the human reviewing the PR.
What this demonstrates: the locked SkillStore contract surface (8 methods + staticCapabilities) wired through real SQL with two-table versioning, transactional status transitions, and JSON-extract tag filters.
The three-leg model
SkillStore is pluggable. Three legs ship out of the box, but the third is the open one:class FooSkillStore implements SkillStore { ... } and call registry.registerSkillStore("primary", new FooSkillStore(...)). The runtime is none the wiser.
This SqliteSkillStore is one such impl — useful as a copy-paste starting point, or directly usable if your needs match.
Quick start
skill_write MCP tool — same backend, same result.
When to use SqliteSkillStore (and when not)
Use it when:- You’re embedding skillscript-runtime as a library and want skills in a database rather than
.skill.mdfiles - Your deployment has no persistent filesystem (container-only) but does have SQLite
- You need richer query semantics than filesystem listing (tag filters, transactional status transitions)
- You’re using the bundled CLI (
skillfile compile,skillfile lint,skillfile list). The CLI is filesystem-first by design —vim foo.skill.md && skillfile lint foois the natural authoring loop. The CLI does NOT use SqliteSkillStore. Sqlite-backed skills are authored via dashboard orskill_writeMCP tool. - Your skills are authored as files-on-disk and committed to git as part of the source tree. FilesystemSkillStore is the right choice there.
Schema
Two tables:skills is the fast-path read for load() / metadata() / query(). skill_versions is the append-only history for versions() — and unlike FilesystemSkillStore, it preserves full body bytes per version, so load(name, version) can return historical content.
WAL is enabled at bootstrap so concurrent readers don’t block writers.
Footgun guard: delete() is hard-cascade
delete() removes both the skills row AND its skill_versions rows. If you need recovery, back up adopter-side BEFORE calling delete. Skill names can be reused after delete (no orphan history left behind).
This is the locked semantic. If your adopter substrate has compliance requirements (audit-grade retention) or you hit a “wait, I deleted that” moment, the upgrade path is soft-delete (tombstone status='Deleted' + filter from query results) — but that’s an adopter-side choice; the bundled SqliteSkillStore stays hard-cascade.
Feature flags
staticCapabilities() declares:
| Feature | Value | What it means |
|---|---|---|
supports_writes | ✓ | store() / update_status() / delete() mutate state |
supports_versioning | ✓ | versions() returns history; load(name, version) returns historical bytes |
supports_tag_filter | ✓ | query({ tag: "foo" }) works via json_extract(metadata_json, '$.tags') (O(n) scan) |
supports_audit_trail | ✓ | update_status() populates previous_status on every transition |
supports_atomic_status_transitions | ✓ | UPDATE skills + INSERT skill_versions wrapped in a transaction |
supports_atomic_status_transitions: false because filesystem writes can tear between body rewrite + sidecar append).
Authoring loop
SqliteSkillStore is the storage layer. Authoring happens above:- Dashboard: visit
http://localhost:7878, create/edit skills through the UI; dashboard writes viaskill_writeMCP tool → SqliteSkillStore - MCP tool: agents call
skill_writedirectly; same path as the dashboard - Programmatic: your code calls
store.store(name, source, metadata?)directly
substrate.skill_store: "sqlite" in connectors.json — both skillfile dashboard and the programmatic bootstrapFromEnv() honor it; no custom bootstrap needed.
Approval — the store doesn’t mint it
SqliteSkillStore.store() persists the body as handed to it; it does not stamp or verify approval. That’s the runtime’s concern, and it differs by mode:
- Unsecured mode — a bare
# Status: Approvedis sufficient and is stored verbatim, no token. - Secured mode — approval is a v3 Ed25519 signature applied by the approve flow (
skillfile approve/ the dashboard), never bystore(). The MCPskill_writehandler forces any unsignedApprovedwrite toDraftbefore it reaches the store, and the bundled stores apply the same force as defense-in-depth (in bothstore()andupdate_status()). Sostore()only ever persists a genuinely-signed Approved body or a Draft.
store() neither stamps nor verifies — persist the body, let the runtime own the gate. See Adopter Playbook §“Approval + secured mode”.
Forking checklist
When forking into your codebase:- Rename the class (e.g.,
PostgresSkillStore,AmpSkillStore) - Replace the SQL with your substrate’s API (HTTP, DataStore, vector DB, etc.)
- Update
staticCapabilities()to match what your substrate actually supports — dropsupports_versioningif you can’t track history, dropsupports_tag_filterif querying tags isn’t tractable - Update
manifest()to describe your substrate (kind: "amp"or whatever) - Optional but high-value for network-backed forks: implement
version()— a cheap store-wide change-token computed WITHOUT loading bodies (a list ETag, max-revision, or metadata digest), where any add/remove/edit/status-change moves it. It letsskill_listskip its N+1 catalog rebuild on unchanged polls (each entry otherwise costs aload()— one network round-trip per skill against a remote store).SqliteSkillStorehashes(name, status, current_version)in one body-free query. Skip it andskill_listjust always rebuilds. - Tests: copy
tests/SqliteSkillStore.test.tsas a starting point + run the conformance suite (SkillStoreConformance.buildTests()fromskillscript-runtime/testing)