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.

What it provides

skills is the skill-provider seam: it browses, imports, and refreshes external SKILL.md text (skills.sh, GitHub, plain https) into storage-backed, versioned, grantable INSTRUCTION-ONLY skills for a swarm. A code skill (defineSkill) lives in your source; an imported skill is third-party methodology (copywriting, SEO, AEO) that can teach an agent but must never carry executable grants — this package enforces that boundary end to end. importSkill/refreshSkill gate on a host authorize hook before any network fetch and persist each import as an append-only, provenance-tracked version; materializeSkillStore(storage.skills) builds the SwarmConfig.dynamicSkills resolver that wraps every imported block in an <imported-skill untrusted> fence, cached and per-skill error-isolated.

When to use it

  • You want to give an agent third-party methodology guidance — a copywriting, SEO, or AEO skill from skills.sh or a GitHub SKILL.md — without hand-copying prompt text.
  • You need imports to be governed: a host authorize hook and provider/author allowlists that run BEFORE any fetch, append-only versioned storage with provenance, and idempotency by upstream version.
  • You want a UI over multiple skill sources — one search across providers behind a policy floor, capability discovery by presence, and a drift badge that reports updates WITHOUT installing them.
  • You want imported instructions treated as untrusted: fenced third-party reference text that can never grant a tool or override policy, with a code-defined skill always winning a name clash.

When not to

  • The skill needs to carry executable tools — that is a CODE skill (defineSkill({ name, instructions, tools }) in @nightowlsdev/core); an imported skill is structurally tool-less by design.
  • You want retrieved factual documents an agent searches at runtime — that is the @nightowlsdev/knowledge plane, not instruction-only methodology text.
  • You have no storage adapter with a skills repo — importSkill/materializeSkillStore persist and resolve through storage.skills / storage.skillsWritable, so this needs a backing store (e.g. storage-supabase).

Alternatives

  • A code skill (defineSkill in @nightowlsdev/core)The skill is yours and may carry tools. Code skills live in source, get code review, and can grant executable tools — imported skills can only ever contribute prompt guidance.
  • @nightowlsdev/knowledgeYou want retrievable facts/documents (chunked, embedded, searched at runtime) rather than a block of methodology instructions injected into the system prompt.

Strengths

  • Enforces the instruction-only boundary end to end: parse-time sanitization (YAML-only frontmatter, slugged names, control chars stripped, size cap), an import policy gate, append-only provenance, and a spoof-proof untrusted fence at resolution.
  • The provider registry is honest: capabilities are reported by presence (canList == typeof provider.list === 'function'), search returns a { listings, warnings } envelope so one provider being down is a partial result not a silent empty one, and duplicate provider ids are rejected at construction.
  • policy is a floor, not a default: a per-call policy can only TIGHTEN the registry's — allowlists intersect, byte caps take the minimum, both authorize hooks run — the only merge rule that makes a deployment-wide policy worth setting.
  • checkSkillUpdates reports drift without publishing (a badge must not install), through a bounded worker pool so a few hundred skills don't burst skills.sh's 600 req/min limit into 429s.
  • Engine-wall clean: depends only on @nightowlsdev/core types + yaml, with all network I/O injected — providers are hermetically testable.

Limits & trade-offs

  • Imported skills can never grant a tool — correct and deliberate, but a limit if you expected an installable skill to bring executable capabilities; use a code skill for that.
  • It needs a storage adapter with the skills repo (storage.skills / skillsWritable), plus migrations for versioning and org-scope (storage-supabase 0026 append-only, 0030/0032 for the FR-055 scope axis).
  • httpSkillProvider is SSRF-hardened (https-only, redirects rejected, private/loopback/link-local IP literals blocked) but a public hostname that DNS-resolves to a private IP (rebinding) is the residual gap — Node fetch has no pre-connect hook — so pin allowHosts for untrusted-adjacent deployments.
  • You still own the authorize hook and allowlists — the repo actor-bar only excludes agent principals; it is NOT authorization, so a missing authorize hook is an open import surface.

How it works

A provider (skillsShProvider, githubSkillProvider, httpSkillProvider, or a custom SkillProvider implementing { id, list?, fetch }) locates and fetches raw SKILL.md. importSkill runs the host authorize hook and provider/author allowlists BEFORE any network call, parses and sanitizes the text, rejects reserved names and silent source swaps, and is idempotent by the upstream sourceVersion — persisting each import as an append-only version in storage.skills with provenance. A stored skill can belong to a sub-org scope (FR-055): reads resolve most-specific-wins through the exact head a caller can see (their scope and its ancestors, never siblings or descendants), and writes land at exactly ctx.orgScope. At runtime materializeSkillStore(storage.skills) is the dynamicSkills resolver: it wraps every imported instruction block in an <imported-skill untrusted="true"> fence with embedded delimiters neutralized, TTL-cached and keyed on (tenantId, orgScope), with per-skill error isolation. createSkillProviderRegistry bundles the sources behind a policy floor for a UI, and checkSkillUpdates reports drift (a discriminated union per skill) without ever writing a version.

Examples

Import a skill, resolve it at runtime, keep it current

The authorize hook + allowlists run before any fetch; imports are append-only and idempotent by upstream version; resolution is instruction-only, fenced, cached.

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

const skillsSh = skillsShProvider();

// Browse, then import into the tenant's store (append-only, versioned, provenance-tracked).
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 }, // runs BEFORE the fetch
});

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

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

A registry + a drift badge that never installs

policy is a floor a per-call gate can only tighten; checkSkillUpdates reports updates without publishing.

skills-example-2.ts
import { createSkillProviderRegistry, githubSkillProvider, httpSkillProvider, skillsShProvider, checkSkillUpdates } from "@nightowlsdev/skills";

const registry = createSkillProviderRegistry({
  providers: [skillsShProvider(), githubSkillProvider(), httpSkillProvider()],
  policy: { allowAuthors: ["acme"], maxInstructionBytes: 40_000, authorize: assertIsAdmin }, // the FLOOR
});

registry.describe();                       // the source picker: capability by presence
const { listings, warnings } = await registry.search({ q: "seo" }, gate); // partial-result envelope

// A badge: report drift WITHOUT publishing (a discriminated union per skill).
const statuses = await checkSkillUpdates({ registry, storage, tenantId, actor, persist: true });

Doing the parts it doesn't support

  • An imported skill that grants a toolNot possible by design — ImportedSkill/InstructionSkill have no tools field and core's tool/gate paths never consult dynamic skills. If a skill must carry executable capability, author it as a code skill (defineSkill with tools) in your source, where it gets code review and always wins a name clash.
  • Cloning a git repo for its skillNo git subprocess ships — githubSkillProvider reads a SKILL.md straight off raw.githubusercontent. For a full clone, implement the SkillProvider interface ({ id, list?, fetch }) as a host-side adapter; a git-clone provider is a natural one, and all network I/O is injected.
  • Deleting an imported skill's historyStorage is append-only — storage-supabase's 0026 triggers reject a cascading skill_versions delete. Offboarding archives instead: SkillWritableRepo.archiveScope flips a scope's heads to archived (every resolution path excludes archived heads), and unarchiveSkill is the way back. Content redaction is deferred on that same precedent.

Related

  • coredefineSkill (code skills) and the SwarmConfig.dynamicSkills resolver materializeSkillStore produces.
  • skill-storeThe guide: browse providers behind a policy floor and import through a server-issued preview token.
  • storage-supabaseThe backing store: append-only versioned skills (0026) plus the org-scope columns (0030/0032).
  • reactSkillStudio — the library-and-store UI that drives import/refresh over these providers.
  • org-scopeThe sub-org scope axis that partitions a stored skill's overlays by department inside one org.