Skip to content
Night Owls.dev
Jump to a page

@nightowlsdev/skills

Adapter/Tooling

Browse, import, and refresh external skills (skills.sh, GitHub, https) into storage-backed, versioned, grantable instruction-only skills, third-party guidance, safely fenced.

What it does

@nightowlsdev/skills is the skill-provider seam. A CODE skill (`defineSkill({ name, instructions, tools })`) lives in your source; an IMPORTED skill is third-party SKILL.md text pulled from a registry, it can teach an agent methodology (copywriting, SEO, AEO) but must never carry executable grants. This package enforces that boundary end to end. `skillsShProvider()` searches and downloads from Vercel's skills.sh directory (the same unauthenticated endpoints the `npx skills` CLI uses, plus its partner security audits); `githubSkillProvider()` reads a SKILL.md straight off raw.githubusercontent (no git subprocess); `httpSkillProvider()` fetches any https URL and is SSRF-hardened (https-only, redirects rejected, private/loopback/link-local IP literals blocked, optional host allowlist). `importSkill`/`refreshSkill` gate on a host authorize hook BEFORE any network fetch, enforce provider/author allowlists, reject reserved names and silent source swaps, and are idempotent by the upstream version, persisting each import as an append-only, provenance-tracked version in the adapter (`storage.skills`). `materializeSkillStore(storage.skills)` builds the `SwarmConfig.dynamicSkills` resolver: it wraps every imported instruction block in an `<imported-skill untrusted="true">` fence (labeled third-party reference guidance that cannot grant tools or override policy) with a TTL-bounded cache and per-skill error isolation. Structurally tool-less, an imported skill can only ever contribute prompt guidance, never a callable tool, and a code-defined skill always wins a name clash. Engine-wall clean: depends only on `@nightowlsdev/core` types + `yaml`, all network I/O injected. FR-039 adds `createSkillProviderRegistry`, the set of sources a deployment offers, with a governance FLOOR (a per-call policy can only ever TIGHTEN it: allowlists intersect, byte caps take the minimum, both authorize hooks run) and a `{ listings, warnings }` search envelope, so one provider being down is a partial result rather than a silent empty one. `checkSkillUpdates` reports drift WITHOUT publishing, because a badge saying "3 skills have updates" must not install them; it returns a discriminated union per skill and fans out through a bounded pool, since skills.sh publishes 600 req/min and an unbounded burst turns every badge into a 429.

Install

pnpm add @nightowlsdev/skills

Key exports

  • skillsShProvider / githubSkillProvider / httpSkillProvider
  • SkillProvider (seam) / SkillProviderError / isSkillProviderError / isBlockedHost
  • parseSkillMd / fenceImportedSkill
  • importSkill / refreshSkill / refreshAllSkills
  • createSkillProviderRegistry (FR-039: describe / search / policy FLOOR)
  • checkSkillUpdates (FR-039: reports drift WITHOUT publishing)
  • materializeSkillStore (builds SwarmConfig.dynamicSkills)

Usage

skills.ts
import { skillsShProvider, importSkill, refreshAllSkills, materializeSkillStore } from "@nightowlsdev/skills";
import { defineSwarm } from "@nightowlsdev/core";

const skillsSh = skillsShProvider();

// 1. Browse the directory, then import a skill (append-only, versioned, provenance-tracked).
//    The host authorize hook + allowlists run BEFORE any network fetch.
const hits = await skillsSh.list({ q: "seo", limit: 10 });
await importSkill({
  provider: skillsSh,
  ref: "coreyhaines31/marketingskills@seo-writing",
  storage,                     // createSupabaseStorage(...), has storage.skills
  tenantId,
  actor: { type: "human", userId, tenantId },
  policy: { allowAuthors: ["coreyhaines31"], authorize: assertIsAdmin },
});

// 2. Grant it to an agent by adding its name to the agent's skillNames (publishAgentVersion / bundle apply).

// 3. Resolve at runtime, instruction-only, fenced untrusted, cached. Code skills win a name clash.
const swarm = defineSwarm({ agents, dynamicSkills: materializeSkillStore(storage.skills) });

// 4. Keep imports current (re-pull → diff by upstream version → re-version on change).
await refreshAllSkills({ providers: { "skills.sh": skillsSh }, storage, tenantId, actor });

// Also: githubSkillProvider() (raw fetch, no clone) and httpSkillProvider() (SSRF-hardened) for other sources.