Skip to content
scalefocusPublic

About

Enterprise-grade, self-hosted registry for governing SKILL.md agent skills, with identity anchored in Microsoft Entra ID

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

skilly

skilly — a self-hosted registry that turns your organization's AI agent skills into a governed, versioned company asset. Among the first platforms of its kind — and the most complete free and open-source offering for enterprise skill governance.

  • Own your real IP. Anyone can rent the same model by Tuesday — the moat is the know-how that makes it do your work to your standard. Today it lives in chat threads and one analyst's head, and walks out the door with them. skilly captures it once, for good.
  • One preferred way, improved by everyone. Each skill is your org's single best-known way to do a task — a living standard, not a rotting wiki page. Anyone can propose an improvement, reviewers gate what lands, and every accepted contribution ships as the new version for everyone. Your standards evolve like open source, governed like production.
  • A trusted source, not a public free-for-all. Every skill is vetted twice before anyone can install it — automatically (malware, secrets, risky-pattern scans) and by your own admins. External skills are mirrored at a pinned, immutable ref and re-checked for upstream drift, so a future version can never silently swap in something you didn't approve. No random skills slipping in — what your agents run is exactly what your reviewers signed off.
  • Full control over what runs. Nothing publishes without clearing your review gates and security scans (ClamAV, secret detection, static heuristics). You decide what ships, to whom — and what gets pulled.
  • Control every install — by version, user, and time. A first among skill registries: every installation is an attributed, skill-scoped credential — pinned to a version or tracking latest, tied to a specific person, expiring when you say. Revoke it and access stops. You always know who runs what, which version, and until when.
  • The whole user lifecycle, not just the skill lifecycle. Users are provisioned automatically through SCIM 2.0 and roles follow your Entra ID groups — joiners get access on day one, movers' permissions shift with them, and the moment someone is decommissioned every install token and URL they hold stops serving. No orphaned credentials, no loose ends.
  • Immutable versioning. Every release is a semver-tagged, immutable version: pin it, promote beta → stable, roll back, or yank it and it vanishes from every install path instantly.
  • Tamper-evident audit trail. Every governance action lands in a hash-chained, append-only log — audits stop being archaeology.
  • End the reinvention. One searchable catalog, org-wide or team-scoped, so twelve teams stop rebuilding the same skill and start compounding on each other's work.
  • Zero new tooling. No custom CLI — each skill is served as an authenticated git repo, installed with the standard npx skills add.
  • Yours, entirely. Self-hosted via docker compose or Helm, Apache 2.0, nothing leaves your network.

Enterprise-grade, open-source, self-hosted registry for governing Anthropic-style SKILL.md agent skills across an organization and its business units, with identity and access anchored in Microsoft Entra ID.

Monorepo layout

packages/
  web/        Next.js app — themeable UI + REST API + OIDC (Auth.js/Entra)
  worker/     SCIM 2.0 + Entra reconciliation + git smart server + publish/scan sweeps (singleton, leader-locked)
  shared/     domain types, RBAC, semver, validation, scanners, external-tool adapter
db/migrations/  plain SQL migrations, applied in order by the migrate service
db/seed.dev.sql dev-only seed data (NOT for production)
deploy/         docker-compose, .env.example, sample reverse proxy (Caddy)
docs/           operator + developer docs

Quickstart: install a skill (consumers)

Just want to use a skill? skilly ships no CLI — install with the external vercel-labs/skills client. Copy the ready-made command from a skill's page in the catalog, or build it yourself:

npx skills add https://x-access-token:<token>@skilly.example.com/team-a/foo.git#v1.2.0

skilly serves each skill as a git repo over an authenticated git smart server — one skill = one repo, each version = an immutable tag. The <token> is a skill-scoped, reusable install token minted per user (revoked by uninstall; SKILLY_SPEC.md §23), embedded as git basic-auth in the URL.

Running your own registry instead? Everything below — prerequisites, testing, local dev, and deployment — is for operators self-hosting skilly. Jump straight to Deployment.


Prerequisites

  • Node.js ≥ 20 (22/25 fine) and pnpm 9 (corepack enable pnpm).
  • git on PATH — the worker shells out to it for repo synthesis and pointer mirroring; tests need it too.
  • Docker — for the full stack and the live-DB integration tests.
  • Windows users: see the Defender exclusion note at the bottom.
pnpm install

Testing

Hermetic suite (no Docker, runs offline)

pnpm --filter @skilly/shared build   # build shared first — web/worker typecheck needs its emitted types
pnpm -r typecheck     # type-check every package
pnpm -r test          # unit + integration tests (shared + worker), uses fakes
pnpm --filter @skilly/web build   # production build of the UI

pnpm -r test is fully hermetic — the DB-backed tests below are gated by SKILLY_DB_E2E and skip unless that env var is set.

Live-DB integration tests (need Docker)

Spin up a throwaway Postgres, apply all migrations, then run the gated tests:

# 1) start Postgres and apply migrations
docker run -d --name skilly-test \
  -e POSTGRES_PASSWORD=test -e POSTGRES_USER=skilly -e POSTGRES_DB=skilly \
  -p 55433:5432 postgres:16-alpine
for f in db/migrations/*.sql; do
  docker exec -i skilly-test psql -v ON_ERROR_STOP=1 -U skilly -d skilly < "$f"
done

export SKILLY_DB_E2E=1
export DATABASE_URL=postgres://skilly:test@127.0.0.1:55433/skilly

# 2) publish chain (hosted + pointer + pending-mirror) → synthesize → real `git clone`
pnpm --filter @skilly/worker build
node --test packages/worker/dist/integration/publishFlow.test.js

# 3) admin flows (namespaces + role mappings + audit), run via tsx
pnpm --filter @skilly/web test:db

# cleanup
docker rm -f skilly-test

UI end-to-end (Playwright, opt-in)

Browser smoke tests live in packages/web/e2e/ and run against a running web server (they need browsers + a live stack, so they are not part of pnpm -r test):

npx playwright install            # one-time: fetch browsers
pnpm --filter @skilly/web dev     # terminal 1 (http://localhost:3000)
pnpm --filter @skilly/web e2e     # terminal 2 (override target with PLAYWRIGHT_BASE_URL)

Running locally

Option A — full stack in Docker (closest to production)

cp deploy/.env.example deploy/.env      # then edit deploy/.env (see Deployment below)
docker compose -f deploy/docker-compose.yml up --build

Brings up postgres · migrate · minio · clamav · worker · web · proxy. Migrations run automatically. The dev reverse proxy (Caddy) listens on http://localhost:8080 and routes /scim/* and *.git smart-HTTP to the worker, everything else to the web app.

Two one-time setup steps:

  • The artifact bucket and the app's scoped MinIO account are created for you by the one-shot minio-init compose service from S3_ACCESS_KEY / S3_SECRET_KEY (bucket skilly-artifacts, or whatever you set S3_BUCKET to). The MinIO console binds to 127.0.0.1:9001 only (login = MINIO_ROOT_USER / MINIO_ROOT_PASSWORD).
  • Sign-in needs a real Entra app (see Deployment) or dev auth (below).

Option B — fast dev loop (apps via pnpm, infra in Docker)

Best for UI/iteration. Run Postgres + MinIO yourself, seed demo data, use dev auth (no Entra needed), and run the apps with hot reload.

# infra
docker run -d --name skilly-pg -e POSTGRES_PASSWORD=test -e POSTGRES_USER=skilly \
  -e POSTGRES_DB=skilly -p 5432:5432 postgres:16-alpine
docker run -d --name skilly-minio -e MINIO_ROOT_USER=skilly -e MINIO_ROOT_PASSWORD=skillyminio \
  -p 9000:9000 -p 9001:9001 minio/minio server /data --console-address ":9001"
# create the bucket once (console http://localhost:9001) → "skilly-artifacts"

# schema + demo data
for f in db/migrations/*.sql; do
  docker exec -i skilly-pg psql -v ON_ERROR_STOP=1 -U skilly -d skilly < "$f"; done
docker exec -i skilly-pg psql -v ON_ERROR_STOP=1 -U skilly -d skilly < db/seed.dev.sql

Create packages/web/.env.local (gitignored):

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=dev-only-secret
SKILLY_REGISTRY_URL=http://localhost:3000
DATABASE_URL=postgres://skilly:test@127.0.0.1:5432/skilly
# dev sign-in (no Entra): seeded user "dev-admin-oid" is a platform admin
SKILLY_DEV_AUTH=1
SKILLY_DEV_OID=dev-admin-oid
# object storage
S3_ENDPOINT=http://127.0.0.1:9000
S3_ACCESS_KEY=skilly
S3_SECRET_KEY=skillyminio
S3_BUCKET=skilly-artifacts

Run the apps (separate terminals):

pnpm --filter @skilly/web dev       # http://localhost:3000
GIT_REPO_ROOT=./data/git DATABASE_URL=postgres://skilly:test@127.0.0.1:5432/skilly \
  S3_ENDPOINT=http://127.0.0.1:9000 S3_ACCESS_KEY=skilly S3_SECRET_KEY=skillyminio \
  S3_BUCKET=skilly-artifacts pnpm --filter @skilly/worker dev

Then open http://localhost:3000, go to /api/auth/signin, and choose “Dev sign-in.” You’ll be the seeded platform admin (catalog, review queue, and admin screens populated).


Deployment (self-hosted, production)

skilly is container-native and runs entirely inside your environment. The reference deployment is the deploy/docker-compose.yml stack behind your org’s TLS-terminating reverse proxy.

1. Configure deploy/.env (copy from .env.example)

cp deploy/.env.example deploy/.env

Replace every placeholder (change-me…, generate-…, all-zero GUIDs) — none of them are safe defaults. Never commit deploy/.env.

Required — the stack won't start (or nobody can sign in) without these. Variables marked ⛔ are enforced by docker-compose.yml (:?set in .env), so docker compose up refuses to run while they're empty.

Var Purpose / how to generate
PUBLIC_BASE_URL External HTTPS URL of skilly, no trailing slash (e.g. https://skilly.example.com). Used for OIDC redirects, install-command / clone URLs, links in notifications, and the MCP endpoint. Becomes NEXTAUTH_URL + SKILLY_REGISTRY_URL inside the web container.
POSTGRES_PASSWORD ⛔ Postgres superuser password (used by postgres + migrate only).
POSTGRES_USER, POSTGRES_DB Superuser name + database name. Default skilly / skilly.
SKILLY_APP_PASSWORD ⛔ Password for the least-privilege skilly_app role that web + worker connect as (created by the migrate service).
NEXTAUTH_SECRET ⛔ Session/JWT signing secret — openssl rand -base64 32. Rotating it signs everyone out.
ENTRA_TENANT_ID Directory (tenant) id of your Entra tenant.
ENTRA_CLIENT_ID / ENTRA_CLIENT_SECRET The Entra app registration used for OIDC sign-in and (worker) Graph group reconciliation. See step 2.
SKILLY_BOOTSTRAP_ADMIN_GROUP Entra group object id whose members are Platform Admins from first boot (before any role mapping exists).
SCIM_BEARER_TOKEN ⛔ Long random secret Entra presents to /scim/v2 — e.g. openssl rand -hex 32. Paste the same value into the Enterprise App's provisioning Secret Token.
MINIO_ROOT_PASSWORD ⛔ Bundled MinIO root password — used only by minio-init to create the bucket and the app's scoped account; web + worker never hold it.
MINIO_ROOT_USER Bundled MinIO root user. Default skilly.
S3_ACCESS_KEY / S3_SECRET_KEY ⛔ The bucket-scoped account web + worker authenticate with (created by minio-init). Distinct from the root pair.

Optional — leave unset to keep the default / disable the feature.

Var Default Purpose
S3_BUCKET skilly-artifacts Artifact bucket. Shared by web (writes) and worker (reads) — create it once in MinIO/S3 (step 3).
CSP_MODE enforce Content-Security-Policy posture: enforce (strict nonce CSP, recommended) · report-only (blocks nothing, reports to /api/csp-report) · off (legacy escape hatch).
METRICS_TOKEN (unset) If set, GET /metrics on web + worker requires Authorization: Bearer <token>. Unset = unauthenticated on the internal network.
POINTER_REFRESH_INTERVAL_MS 86400000 (24h) How often the worker re-clones + re-scans mirrored pointer skills and checks for upstream drift.
AI_TOKEN_ENC_KEY (unset) Encrypts the AI provider token (Open WebUI / Anthropic API) a Platform Admin saves on the Administration page. openssl rand -base64 32 (must decode to exactly 32 bytes). Unset = the AI integration card is disabled. Shared by web + worker.
EMAIL_TOKEN_ENC_KEY (unset) Encrypts the Microsoft 365 service-mailbox tokens for Graph email (the preferred email transport). openssl rand -base64 32 (exactly 32 bytes). Shared by web + worker.
ENTRA_EMAIL_CLIENT_ID / ENTRA_EMAIL_CLIENT_SECRET (unset) A separate Entra app registration for Graph email (not the OIDC app) — see step 2. All three EMAIL/ENTRA_EMAIL vars are needed for Graph email.
SMTP_HOST (unset) Fallback (plain-text) email transport, used when no Graph mailbox is operational. Unset = SMTP disabled.
SMTP_PORT 587 SMTP port (465 implies implicit TLS).
SMTP_USER / SMTP_PASSWORD (unset) SMTP auth; omit for an unauthenticated relay.
SMTP_FROM skilly@example.com Sender address for SMTP mail.
NOTIFY_WEBHOOK_URL (unset) POSTs one JSON body per notification (e.g. a Teams/Slack incoming webhook).
ONE_TIME_TOKEN_TTL_SECONDS 300 Legacy/no-op — install tokens don't use it (kept for compatibility).

With no email or webhook configured, notifications still land in the in-app inbox.

.env.example also documents TRUST_PROXY (the number of reverse proxies in front of skilly, e.g. 1 — passed to both web and worker so rate limits and /installed see the real client address instead of the proxy's), CLAMAV_HOST / CLAMAV_PORT (set on both tiers so hosted uploads are anti-virus scanned at upload), AI_ALLOW_PRIVATE_BASE_URL=1 (only for an AI provider on your private network) and SMTP_SECURE (1 for implicit TLS).

Advanced: tuning knobs read by the apps (not in .env.example)

These have sensible defaults and are rarely changed. To use one with docker compose, add it to the relevant service's environment: block.

  • Database: PG_POOL_MAX (10), PG_STATEMENT_TIMEOUT_MS (30000).
  • Object storage: S3_REGION (us-east-1), S3_ENDPOINT / S3_ACCESS_KEY / S3_SECRET_KEY (compose derives these from MinIO — override to point at real S3).
  • Worker: WORKER_PORT (4000), GIT_REPO_ROOT (/data/git in compose), CLAMAV_HOST / CLAMAV_PORT (clamav / 3310), LEADER_POLL_MS, RECONCILE_INTERVAL_MS, and the sweep cadences *_SWEEP_INTERVAL_MS / *_INTERVAL_MS.
  • Caches: RBAC_ROLE_CACHE_TTL_MS, RBAC_ACCESS_CACHE_TTL_MS, FACETS_CACHE_TTL_MS, and the other *_CACHE_TTL_MS values.
  • Pointer skills: MIRROR_MAX_ATTEMPTS, MIRROR_CLONE_TIMEOUT_MS, POINTER_FETCH_TIMEOUT_MS, POINTER_FETCH_MAX_BYTES, POINTER_REFS_TIMEOUT_MS.

Grep process.env. under packages/*/src for the full list and defaults. Dev/test only — never set in production: SKILLY_DEV_AUTH, SKILLY_DEV_OID, SKILLY_MIRROR_ALLOW_INSECURE, SKILLY_DB_E2E.

2. Microsoft Entra ID setup

  • OIDC SSO — register an app; add redirect URI https://<host>/api/auth/callback/azure-ad; put tenant/client id/secret into ENTRA_*.
  • SCIM provisioning — in the Enterprise App → Provisioning, set the tenant URL to https://<host>/scim/v2 and the secret token to SCIM_BEARER_TOKEN. Assign the groups you want synced. (The worker hosts /scim/v2; the proxy routes it there.)
  • Reconciliation (optional) — grant the app the Graph application permission GroupMember.Read.All (or Directory.Read.All) with admin consent; the worker then periodically reconciles membership for role-mapped groups (RECONCILE_INTERVAL_MS).
  • Bootstrap — set SKILLY_BOOTSTRAP_ADMIN_GROUP; its members can sign in and create namespaces + role mappings (Administration screen) before any mapping exists.
  • Graph email (optional) — register a second app (not the OIDC one) with delegated Mail.Send + offline_access (admin-consented), set Assignment required = Yes and assign only the service mailbox user, and add redirect URI https://<host>/api/admin/email/callback. Put its id/secret into ENTRA_EMAIL_CLIENT_ID/ENTRA_EMAIL_CLIENT_SECRET, set EMAIL_TOKEN_ENC_KEY, then connect the mailbox from the Administration page.

3. Bring it up

docker compose -f deploy/docker-compose.yml up -d --build
  • The migrate service applies db/migrations/*.sql in order on every start.
  • Create the S3_BUCKET in MinIO once (or pre-create it in your real S3).
  • Terminate TLS at your reverse proxy and route to the stack (see deploy/Caddyfile for the routing shape: /scim/* and *.git/... → worker:4000, else → web:3000).

4. Persistence & operations

  • Volumes to persist/back up: Postgres data, MinIO data, and the git repo volume (deploy/data/git, the worker’s GIT_REPO_ROOT). Back up all three (e.g. pg_dump + object-store snapshot + tar of the git volume).
  • Health: GET /healthz (liveness) and GET /readyz (DB check) on both web and worker.
  • Metrics: GET /metrics (Prometheus text exposition) on both web and worker — scrape each instance. Set METRICS_TOKEN to require an Authorization: Bearer <token> on the endpoint; leave it unset to expose metrics unauthenticated on the internal network. Counters cover proposals/actions, tokens, installs, searches, git clones, publishes/mirrors, notification delivery, and pointer drift.
  • Rate limiting: per-instance, in-memory limits on propose / token-mint / install-mint / search (429 + Retry-After). Horizontal scaling makes these per-replica; a shared store is a future upgrade.
  • Pointer refresh: the leader re-clones + re-scans mirrored pointer refs on a schedule (POINTER_REFRESH_INTERVAL_MS, default 24h), refreshing scan findings and flagging upstream drift on pinned refs into the audit log.
  • Scaling: the web app is stateless (scale horizontally behind the proxy); the worker is a singleton (Postgres advisory-lock leader election — only the leader runs SCIM reconciliation, publish/mirror, scan, and token sweeps).
  • Outbound network is required at runtime for pointer mirroring and ClamAV signature updates. UI fonts are vendored (self-hosted) — no CDN dependency. Fully air-gapped deployments must mirror pointer sources internally and disable ClamAV updates.

Kubernetes (Helm)

A chart lives at deploy/helm/skilly: stateless web (Deployment + Service + HPA), leader-locked worker (Deployment + git PVC), a migrations Job (pre-install/upgrade hook), an Ingress that routes /scim and *.git to the worker and everything else to web, plus bundled Postgres/MinIO/ClamAV you can disable to use managed services.

# migrations are applied by a Job from a ConfigMap built off the repo:
kubectl -n skilly create configmap skilly-migrations --from-file=db/migrations
helm install skilly deploy/helm/skilly -n skilly \
  --set publicBaseUrl=https://skilly.example.com \
  --set ingress.host=skilly.example.com
# re-create the ConfigMap + `helm upgrade` whenever you add a migration.
  • HA: web scales horizontally (HPA, stateless JWT sessions); the worker is leader-locked (advisory lock) so extra replicas are safe — scaling it past 1 needs ReadWriteMany git storage. Per-instance rate limiting becomes per-replica (a shared store is the next upgrade).
  • The Ingress git route uses a regex (ingress-nginx annotations by default); on other controllers ensure *.git/... reaches the worker Service or npx skills add clones fail.
  • See the chart's NOTES.txt for the bucket-creation + Entra wiring steps.

Windows: Microsoft Defender exclusion (important)

Real-time Defender scanning intermittently quarantines files inside node_modules (notably Next.js’ bundled jest-worker/processChild.js), breaking next build/next dev and package resolution (MODULE_NOT_FOUND, missing next/dist/bin/next).

Fix — add a Defender folder exclusion (PowerShell, as Administrator):

Add-MpPreference -ExclusionPath "<path-to-your-clone>\node_modules"
Add-MpPreference -ExclusionPath (pnpm store path)   # the global pnpm content store

Recovery if it already happened: pnpm install --force.


Status

Backend subsystems are implemented and tested (identity: OIDC + SCIM + reconciliation + RBAC; catalog + authenticated git smart server; proposals / validation / scanning / audit; hybrid hosted + pointer; yank/archive, promotion-to-global, direct publish), with a themeable React UI. Tiers 2–4 complete: CI (GitHub Actions), notification delivery (in-app center + SMTP + webhook), scoped audit-log viewer, catalog facets + rendered SKILL.md, PAT management, Prometheus /metrics + rate limiting, install-count analytics, pointer refresh/drift-check, a Playwright smoke harness, a Helm chart (web HPA + leader-locked worker + Ingress + bundled or external deps), tamper-evident audit hash-chaining, and watch/follow with new-version notifications. Explicitly deferred (see SKILLY_SPEC.md §16): per-version visibility (conflicts with the per-skill invariant), SAML, OpenTelemetry, i18n. 78+ tests pass; two gated suites verify the live-DB + real-git paths (incl. .zip bundles, pointer refresh, audit-chain tamper detection, and watch→notify); helm lint runs in CI.

About

Enterprise-grade, self-hosted registry for governing SKILL.md agent skills, with identity anchored in Microsoft Entra ID

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages