skills

package
v1.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 2, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package skills owns Harbor's token-savvy, DB-backed, identity- scoped skill subsystem (RFC §6.7). lands the leaf surface:

  • The mandatory `SkillStore` interface every backend implements.
  • The shared types — `Skill`, `Origin`, `Scope`, `ListFilter`, `RankedSkill`.
  • Sentinel errors compared via `errors.Is`.
  • The §4.4 extensibility-seam plumbing (registry + factory).

sibling layers (planner-facing tools, the virtual directory, the Skills.md importer, the in-runtime generator with persistence) all consume this surface.

Identity is mandatory at every method. The triple `(tenant, user, session)` MUST be fully populated; empty `RunID` is accepted (skills are session-scoped at the storage layer; the generator stamps `OriginRef` with the run id from `ctx`). Missing-triple operations fail closed with `ErrIdentityRequired` AND emit a `skill.identity_rejected` event on the bus — never silent (AGENTS.md §5 "Fail loudly").

Concurrent reuse: one `SkillStore` instance is safe to share across N concurrent goroutines. Drivers persist only internally-synchronized state on themselves; per-call state lives in `ctx` and the supplied `identity.Quadruple`.

Index

Constants

View Source
const (
	// DefaultMaxEntries is the per-View row cap when DirectoryConfig
	// leaves MaxEntries at zero.
	DefaultMaxEntries = 30
	// MinMaxEntries is the inclusive lower bound for MaxEntries.
	MinMaxEntries = 1
	// MaxMaxEntries is the inclusive upper bound for MaxEntries.
	MaxMaxEntries = 200
)

MaxEntries bounds — a binding contract.

View Source
const (
	// PathFTS5 — FTS5 virtual table produced the row.
	PathFTS5 = "fts5"
	// PathRegex — regex fallback produced the row.
	PathRegex = "regex"
	// PathExact — exact lowercase-equality fallback produced the row.
	PathExact = "exact"
	// PathSemantic — the opt-in semantic retrieval mode produced the
	// row: embedding-similarity ranking over the identity-scoped
	// catalog (RetrievalSemantic).
	PathSemantic = "semantic"
)

Search-result paths.

View Source
const DefaultDriver = "localdb"

DefaultDriver is the production driver name. later phases (Portico) registers additional names.

View Source
const EventTypeSkillDeleted events.EventType = "skill.deleted"

EventTypeSkillDeleted is emitted on a successful `Delete`. Payload carries the canonical identifiers. SafePayload by construction.

View Source
const EventTypeSkillIdentityRejected events.EventType = "skill.identity_rejected"

EventTypeSkillIdentityRejected is emitted when a `SkillStore` method is called with a missing identity triple (fail- closed contract). Mirrors `memory.identity_rejected` for the skills subsystem.

View Source
const EventTypeSkillPackOverwriteRefused events.EventType = "skill.pack_overwrite_refused"

EventTypeSkillPackOverwriteRefused is emitted when an `Upsert` would have overwritten an `Origin=pack` row with non-pack input. The store does NOT mutate the row; the emit makes the refusal observable from Console / audit. SafePayload by construction.

View Source
const EventTypeSkillProposed events.EventType = "skill.proposed"

EventTypeSkillProposed is the mandatory audit event emitted by the in-runtime skill generator (`skill_propose(persist=true)`) on every persist — whether the persist succeeded (`persisted`), was idempotent (`idempotent`), or was rejected by the conflict policy (`rejected`). Caller-controlled string fields (Title / Trigger excerpts) are run through `audit.Redactor.Redact` BEFORE the event is built (RFC §6.7). The payload itself is SafePayload so the bus does not re-run it through the redactor; the generator is the authoritative redaction point.

View Source
const EventTypeSkillSearchExecuted events.EventType = "skill.search_executed"

EventTypeSkillSearchExecuted is emitted on every `Search` call. Payload carries the query (HASHED — the search corpus is allowed to contain caller bytes per RFC §6.7, but the audit emit MUST NOT surface them), the path that produced the result, and the row count. SafePayload by construction.

View Source
const EventTypeSkillUpserted events.EventType = "skill.upserted"

EventTypeSkillUpserted is emitted on every successful `Upsert`. Payload carries the canonical skill identifiers + `Idempotent` flag so subscribers can distinguish a true write from a hash- matched no-op. SafePayload by construction — every field is a bounded enumerable string or a boolean; no caller-controlled bytes survive on the payload.

View Source
const ExtraPinnedKey = "pinned"

ExtraPinnedKey is the Skill.Extra map key the directory treats as a runtime-stamped pin marker. Value MUST be the bool true; any other shape is ignored (no string "true" / int 1 / etc. — fail-loud on shape drift rather than silently accept).

Variables

View Source
var (
	// ErrSkillNotFound — `Get` / `Delete` against a non-existent
	// row.
	ErrSkillNotFound = errors.New("skills: skill not found")
	// ErrPackOverwriteRefused — `Upsert` attempted to overwrite an
	// `Origin=pack` row with non-pack input.
	ErrPackOverwriteRefused = errors.New("skills: refuse to overwrite pack-origin skill")
	// ErrStoreClosed — store has been closed; further operations
	// are rejected.
	ErrStoreClosed = errors.New("skills: store is closed")
	// ErrInvalidSkill — supplied `Skill` failed validation.
	ErrInvalidSkill = errors.New("skills: invalid skill")
	// ErrUnknownDriver — `Open` was asked for a driver name no
	// factory has been registered under.
	ErrUnknownDriver = errors.New("skills: unknown driver")
	// ErrIdentityRequired — caller passed a `Quadruple` with at
	// least one empty `(tenant, user, session)` component. The
	// store also emits `skill.identity_rejected` on the bus.
	ErrIdentityRequired = errors.New("skills: identity triple incomplete")
)

Sentinel errors. Compare via `errors.Is`.

View Source
var ErrInvalidConfig = errors.New("skills: invalid directory config")

ErrInvalidConfig — NewDirectory rejected the DirectoryConfig. Returned wrapped via fmt.Errorf("%w: ...") so callers extract the cause via errors.Is. Fail-loud per CLAUDE.md §5 + §13.

Functions

func CanonicalContentHash

func CanonicalContentHash(s Skill) string

CanonicalContentHash returns the canonical sha256 hex of `s`'s content fields. The hash is the LWW gate and the idempotency key for `Upsert`:

  • Origin / OriginRef / Scope / ScopeTenantID / ScopeProjectID are EXCLUDED: the same skill imported via two paths (PackImport from a re-published pack vs. Generated from a planner re-derivation) hashes identically when the content is the same, so the conflict-policy gate fires on actual content drift rather than provenance noise.
  • Lifecycle timestamps (`CreatedAt`, `UpdatedAt`, `LastUsed`) and `UseCount` are EXCLUDED: they evolve over the row's life without semantic content change.
  • `Extra` IS INCLUDED via its canonical JSON-key-sorted text representation (the generator may stamp model metadata there that legitimately participates in identity).

The hash is computed over a fixed-key envelope, NOT over a struct rendering, so future field additions don't silently break stored hashes. The envelope format and the included-fields list ARE load-bearing — changes need a new content-hash version.

Slice-shaped fields (`Tags`, `RequiredTools`, `RequiredNS`, `RequiredTags`) are sorted before hashing so ordering noise from caller-side normalisation doesn't perturb the hash. Ordered slices (`Steps`, `Preconditions`, `FailureModes`) preserve their order because the planner's procedural rendering depends on it.

func EmitIdentityRejected

func EmitIdentityRejected(ctx context.Context, bus events.EventBus, q identity.Quadruple, operation string) error

EmitIdentityRejected publishes the `skill.identity_rejected` event on `bus` and returns the wrapped `ErrIdentityRequired`. Drivers call this from every method that detected an identity- rejection so the audit emit path is consistent across implementations.

The bus's `ValidateEvent` requires a fully-populated identity triple; the partial / empty triple the caller supplied is preserved where present and substituted with the `missingIdentitySentinel` string elsewhere, so the rejection event itself is bus-publishable. The audit-visible payload's `Reason` field names the component(s) that were actually missing.

`operation` is the rejected method name ("Upsert", "Get", "Search", "Delete", "List", "Close").

If Publish fails, the wrapped error names both the rejection cause and the bus failure — callers MUST NOT silently drop either.

func IdentityFromCtx

func IdentityFromCtx(ctx context.Context) (identity.Quadruple, error)

IdentityFromCtx reads the identity `Quadruple` (or bare `Identity`) from `ctx` and validates the triple. It returns the (possibly partial) Quadruple plus a wrapped `ErrIdentityRequired` on failure; the partial value is what callers pass to `EmitIdentityRejected`, which substitutes the missing-component sentinel for the bus's `ValidateEvent` triple check.

Single source for the planner tools (`internal/skills/tools`) and the virtual directory (`internal/skills`) — both consumed the same byte-for-byte logic before this was hoisted here.

func Register

func Register(name string, factory Factory)

Register installs a driver factory under `name`. Drivers self- register from their package `init()`; `cmd/harbor` blank-imports the production driver to trigger registration. Per AGENTS.md §4.4.

Re-registering the same name panics — the registration model is write-once-at-init and a duplicate signals a build mis-config.

func RegisteredDrivers

func RegisteredDrivers() []string

RegisteredDrivers returns a sorted list of driver names. Useful for boot-log emission and for surfacing in error messages.

func ValidateIdentity

func ValidateIdentity(q identity.Quadruple) error

ValidateIdentity returns wrapped `ErrIdentityRequired` when any of `(TenantID, UserID, SessionID)` on `q` is empty. `RunID` is allowed to be empty (skills are session-scoped at the storage layer). Mirrors `memory.ValidateIdentity` for the identity- mandatory contract.

Types

type ConfigSnapshot

type ConfigSnapshot struct {
	// Driver names the registered factory. Empty → DefaultDriver.
	Driver string
	// DSN is consumed by the `localdb` driver. Bare file path or
	// SQLite `file:` URI; the special `:memory:` sentinel is
	// honoured for tests. `secret:"true"` redaction lives at the
	// config-package boundary.
	DSN string
	// Retrieval opts in to a `Search` ranking mode. The zero value
	// keeps the FTS5 → regex → exact ladder; `RetrievalSemantic`
	// ranks by embedding similarity (requires `Deps.Embedder`).
	Retrieval RetrievalMode
}

ConfigSnapshot is the strict subset of `config.SkillsConfig` the skills package consumes. Keeping a snapshot decouples drivers from the config package's type evolution.

func SnapshotFromConfig added in v1.3.0

func SnapshotFromConfig(cfg config.SkillsConfig) ConfigSnapshot

SnapshotFromConfig projects the operator-facing `config.SkillsConfig` block onto the skills package's decoupled ConfigSnapshot. Every config field maps 1:1; the field-parity test in from_config_test.go fails the build when a new config field lands without a projection (or an explicit exclusion naming why). `Directory` is excluded — the store snapshot and the directory config feed two different constructors; `DirectoryFromConfig` is the directory's projection.

type Deps

type Deps struct {
	Bus      events.EventBus
	Embedder Embedder
}

Deps carries the runtime dependencies a skills driver needs.

`Bus` is mandatory so identity-rejection emits + audit events (`skill.upserted`, `skill.deleted`, `skill.pack_overwrite_refused`, `skill.search_executed`) land on the audit pipeline.

**Note** (analog): unlike memory drivers, skills drivers do NOT receive a `state.StateStore`. The `localdb` driver owns its own `skills` + `skills_fts` tables; persistent skill state lives in the driver's own DB, not piggybacked onto the StateStore. The Portico driver (post-V1) will fetch from a remote MCP surface and also has no StateStore need. `Embedder` is the injectable text→vector callable the semantic retrieval mode consumes. OPTIONAL — required only when `cfg.Retrieval == RetrievalSemantic`, ignored otherwise. A semantic config without an Embedder fails loudly at `Open` (mirroring the memory registry's Summarizer/Embedder rule) — never a stub fallback (AGENTS.md §13). Existing callers that construct `Deps{Bus}` keep compiling: the zero value is nil, valid for the default retrieval mode.

type Directory

type Directory struct {
	// contains filtered or unexported fields
}

Directory exposes the virtual-directory snapshot. Built once at boot (or per operator-config reload); safe to share across N concurrent goroutines. Per-call state lives on ctx + the args; no mutable field on Directory itself changes after construction.

func NewDirectory

func NewDirectory(store SkillStore, deps Deps, cfg DirectoryConfig) (*Directory, error)

NewDirectory validates cfg and returns a usable Directory.

Validation rules:

  • store == nil → wrapped ErrInvalidConfig
  • deps.Bus == nil → wrapped ErrInvalidConfig
  • cfg.MaxEntries == 0 → DefaultMaxEntries (30)
  • cfg.MaxEntries < 1 or > 200 → wrapped ErrInvalidConfig
  • cfg.Selection == "" → SelectionPinnedThenRecent
  • cfg.Selection not in the two canonical values → wrapped ErrInvalidConfig

Concurrent reuse: the returned *Directory holds no mutable state; safe to share across N goroutines.

func (*Directory) View

View returns the identity-scoped, capability-filtered, redacted, bounded snapshot of the skill catalogue.

Pipeline:

  1. Read identity from ctx (Quadruple or Identity); missing → wrapped ErrIdentityRequired + skill.identity_rejected emit.
  2. SkillStore.List for the identity (Limit=0 — driver default; the directory bounds locally to maxEntry afterwards).
  3. Capability filter: exclude skills whose RequiredTools / RequiredNS / RequiredTags are NOT subsets of cap.Allowed*.
  4. Partition by pinning (config-declared first, then Extra-pinned, then unpinned).
  5. Sort each partition by the Selection's secondary key (UpdatedAt DESC, Name ASC for pinned_then_recent; UseCount DESC, Name ASC for pinned_then_top). Config-declared pins preserve declaration order.
  6. Concatenate pinned ++ unpinned; truncate to maxEntry.
  7. Project to SkillView, redacting Title + Trigger.

Concurrent-safe: pure read pipeline over the immutable *Directory. Per-call state lives on ctx + cap; no shared mutable state.

type DirectoryCapability

type DirectoryCapability struct {
	// AllowedTools — the set of tool names visible to the run.
	// Used by the filter (subset check) AND the redactor (skills
	// referencing names not in this set get the disallowed-name
	// replacement in Title / Trigger).
	AllowedTools []string `json:"allowed_tools,omitempty"`
	// AllowedNamespaces — namespaces (Skill.RequiredNS) the run may
	// reference.
	AllowedNamespaces []string `json:"allowed_namespaces,omitempty"`
	// AllowedTags — tags (Skill.RequiredTags) the run may reference.
	AllowedTags []string `json:"allowed_tags,omitempty"`
}

DirectoryCapability mirrors the planner-supplied capability envelope the directory consults for filtering + redaction at View time. The shape is identical to internal/skills/tools.CapabilityContext; duplicating the type here avoids the import cycle (`internal/skills/ tools` imports `internal/skills` already). When a caller already holds a `tools.CapabilityContext`, the fields map one-to-one and the conversion is mechanical at the planner boundary.

Concurrent reuse: the struct is a value carried on the args; never mutated in-flight. Multiple goroutines may share a DirectoryCapability value safely.

type DirectoryConfig

type DirectoryConfig struct {
	// Pinned is the operator-declared list of skill names to anchor
	// at the top of every View. Order is preserved across calls.
	// Names that don't exist under the calling identity are dropped
	// silently (config + storage may legitimately disagree; storage
	// wins).
	Pinned []string
	// MaxEntries caps the View length. 0 → DefaultMaxEntries (30).
	// Outside [1, 200] → NewDirectory returns wrapped
	// ErrInvalidConfig.
	MaxEntries int
	// Selection picks the unpinned-partition ordering. Empty →
	// SelectionPinnedThenRecent.
	Selection Selection
}

DirectoryConfig configures one Directory instance.

func DirectoryFromConfig added in v1.3.0

func DirectoryFromConfig(cfg config.SkillsConfig, fallbackMaxEntries int) DirectoryConfig

DirectoryFromConfig projects the operator-facing `config.SkillsConfig.Directory` block onto a DirectoryConfig for `NewDirectory`.

`fallbackMaxEntries` is consulted when `skills.directory.max_entries` is unset (zero): the run-loop callers pass the resolved `planner.skills_context_max` value so the pre-111d injection-budget knob keeps capping the injected block's length. A zero fallback leaves MaxEntries at zero and `NewDirectory` applies its own package default (30). The Pinned slice is copied so the caller's config struct cannot mutate the directory after construction.

type Embedder added in v1.4.0

type Embedder interface {
	Embed(ctx context.Context, texts []string) ([][]float32, error)
}

Embedder is the injectable text→vector callable the semantic retrieval mode consumes — the consumer-side-interface shape the memory subsystem's `Summarizer` established. The canonical implementation is constructed via the embeddings factory (`internal/embeddings`); any `embeddings.Embedder` satisfies this interface.

Concurrent-reuse contract: one `Embedder` instance is safe to share across N concurrent goroutines. Implementers MUST honour `ctx.Done()`.

type Factory

type Factory func(cfg ConfigSnapshot, deps Deps) (SkillStore, error)

Factory builds a `SkillStore` from a `ConfigSnapshot` + `Deps`. Drivers expose one `Factory` each via `init()` → `Register`.

type ListFilter

type ListFilter struct {
	Scope    Scope
	TaskType string
	Tags     []string // any-of match against the skill's `Tags`
	Limit    int
	Offset   int
}

ListFilter narrows the rows `List` returns. Zero-value fields are matched as "any". Drivers cap `Limit` at 1000; `Limit == 0` falls back to the driver default (100).

type Origin

type Origin string

Origin is the provenance of a skill — the pack import path or the in-runtime generator path. The two flavors share storage but their conflict semantics differ: a `Generated` skill MAY NOT overwrite a `PackImport` skill of the same name.

const (
	// OriginPack — imported from a Skills.md pack (lands the
	// importer). Pack rows are immutable from the generator's
	// perspective: `Upsert` refuses to overwrite with
	// `ErrPackOverwriteRefused` when incoming Origin != OriginPack.
	OriginPack Origin = "pack"
	// OriginGenerated — produced by the in-runtime generator
	// (`skill_propose(persist=true)`). Generated→Generated
	// is last-write-wins gated by `ContentHash` change.
	OriginGenerated Origin = "generated"
)

Origin values.

type RankedSkill

type RankedSkill struct {
	Skill Skill
	Score float64
	Path  string
}

RankedSkill carries the search-time relevance score + the path that produced it. `Path` is one of `"fts5" | "regex" | "exact"`; callers (the planner tools) surface it for observability only — it is not part of the ranking math.

`Score` is the normalised 0.0–1.0 score:

  • FTS5 path: `bm25 → 1/(1+raw) → min-max normalised`.
  • Regex path: `name fullmatch=0.95 | name match=0.90 | name search=0.85 | body search=0.75`.
  • Exact path: 1.0 (lowercase equality on `name | title | trigger | tags`).

type RetrievalMode added in v1.4.0

type RetrievalMode string

RetrievalMode declares the opt-in `Search` ranking shape.

const (
	// RetrievalDefault — the zero value: the token-savvy FTS5 →
	// regex → exact ladder.
	RetrievalDefault RetrievalMode = ""
	// RetrievalSemantic — rank by embedding similarity over the
	// identity-scoped catalog (result path `PathSemantic`). Requires
	// `Deps.Embedder` — no stub fallback. Capability filtering,
	// redaction, and the budgeter apply unchanged downstream.
	RetrievalSemantic RetrievalMode = "semantic"
)

RetrievalMode values.

type Scope

type Scope string

Scope is the operator-declared visibility of a skill.

const (
	// ScopeSession — visible only inside the originating session.
	// The narrowest scope; cross-session visibility requires an
	// explicit promotion (RFC §6.7 "isolation
	// conformance"). The storage layer's identity filter already
	// pins rows to a `(tenant, user, session)` triple; ScopeSession
	// is the matching declared-scope marker for rows that should
	// stay session-local.
	ScopeSession Scope = "session"
	// ScopeProject — visible inside the operator-declared project
	// only. The generator default per RFC §6.7.
	ScopeProject Scope = "project"
	// ScopeTenant — visible to every project inside the same tenant.
	ScopeTenant Scope = "tenant"
	// ScopeGlobal — visible to every tenant. Reserved for operator-
	// managed shared skills.
	ScopeGlobal Scope = "global"
)

Scope values.

type Selection

type Selection string

Selection picks the unpinned-partition ordering. Pinned skills always come first; Selection orders only the unpinned remainder (and the secondary ordering inside the Extra-pinned subset).

const (
	// SelectionPinnedThenRecent — pinned first, then most-recently-
	// updated skills (UpdatedAt DESC, Name ASC).
	SelectionPinnedThenRecent Selection = "pinned_then_recent"
	// SelectionPinnedThenTop — pinned first, then most-used skills
	// (UseCount DESC, Name ASC). Library-level only at V1.1.x: no
	// production path increments UseCount, so the OPERATOR config
	// validator rejects `skills.directory.selection: pinned_then_top`
	// as not-yet-wired. An
	// embedder that maintains UseCount through Upsert itself may use
	// this Selection directly.
	SelectionPinnedThenTop Selection = "pinned_then_top"
)

Selection values.

type Skill

type Skill struct {
	Name           string
	Title          string
	Description    string
	Trigger        string
	TaskType       string
	Tags           []string
	Steps          []string
	Preconditions  []string
	FailureModes   []string
	RequiredTools  []string
	RequiredNS     []string
	RequiredTags   []string
	Origin         Origin
	OriginRef      string
	Scope          Scope
	ScopeTenantID  string
	ScopeProjectID string
	ContentHash    string
	CreatedAt      time.Time
	UpdatedAt      time.Time
	LastUsed       time.Time
	UseCount       int
	Extra          map[string]any
}

Skill is the canonical skill record. The struct is the storage envelope drivers persist; the planner-facing tools wrap it with capability filtering + redaction at injection time.

Validation rules at `validate`:

  • `Name` non-empty
  • `Trigger` non-empty (planner-visible match cue)
  • `Steps` non-empty (at least one step)
  • `Origin` ∈ {OriginPack, OriginGenerated}
  • `Scope` ∈ {ScopeSession, ScopeProject, ScopeTenant, ScopeGlobal}

func (Skill) Validate

func (s Skill) Validate() error

Validate returns `ErrInvalidSkill` when any mandatory field is missing or out-of-range. Drivers call this at the boundary so a caller's bad payload surfaces at `Upsert` rather than later via a corrupt row.

type SkillDeletedPayload

type SkillDeletedPayload struct {
	events.SafeSealed
	Name string
}

SkillDeletedPayload reports a successful delete. SafePayload by construction.

type SkillIdentityRejectedPayload

type SkillIdentityRejectedPayload struct {
	events.SafeSealed
	Operation string
	Reason    string
}

SkillIdentityRejectedPayload mirrors `memory.MemoryIdentityRejectedPayload`. Operation is the rejected method name ("Upsert", "Get", "Search", "Delete", "List"); Reason is a short static string indicating which component was missing.

type SkillPackOverwriteRefusedPayload

type SkillPackOverwriteRefusedPayload struct {
	events.SafeSealed
	Name           string
	ExistingOrigin Origin
	IncomingOrigin Origin
}

SkillPackOverwriteRefusedPayload reports a refused upsert against an existing `Origin=pack` row. `IncomingOrigin` names the offender; the store left the row untouched. SafePayload by construction.

type SkillSearchExecutedPayload

type SkillSearchExecutedPayload struct {
	events.SafeSealed
	QueryHash string // first 16 hex chars of sha256(lowercased query)
	Path      string // "fts5" | "regex" | "exact"
	Limit     int
	ResultN   int
}

SkillSearchExecutedPayload reports a `Search` execution. `QueryHash` is a sha256 hex prefix of the lowercased query so repeated identical searches are correlatable for tracing without leaking the raw text. SafePayload by construction.

type SkillStore

type SkillStore interface {
	// Upsert inserts or updates `skill` under the identity-scoped
	// `(tenant, user, session, scope, name)` key. Conflict policy
	// (RFC §6.7):
	//
	//   - existing.Origin == "pack" && skill.Origin != "pack" →
	//     `ErrPackOverwriteRefused` AND
	//     `skill.pack_overwrite_refused` emit. Row left untouched.
	//   - existing.Origin == "generated" && skill.Origin ==
	//     "generated" && existing.ContentHash == skill.ContentHash →
	//     idempotent no-op; emit a single `skill.upserted` for
	//     observability with `idempotent=true` payload field.
	//   - otherwise: last-write-wins; emit `skill.upserted` with
	//     `idempotent=false`.
	Upsert(ctx context.Context, id identity.Quadruple, skill Skill) error

	// Get returns the skill identified by `name` under the supplied
	// identity. Missing → `ErrSkillNotFound`.
	Get(ctx context.Context, id identity.Quadruple, name string) (Skill, error)

	// List returns the filtered, paged skills under the supplied
	// identity. Ordering is deterministic: `(UpdatedAt DESC,
	// Name ASC)`.
	List(ctx context.Context, id identity.Quadruple, filter ListFilter) ([]Skill, error)

	// Search returns up to `limit` skills ranked by the FTS5 →
	// regex → exact ladder. `limit == 0` falls back
	// to 20. The driver picks the first path that returns rows;
	// later paths run only when earlier ones produced nothing.
	// Emits `skill.search_executed` with the path that produced the
	// result.
	Search(ctx context.Context, id identity.Quadruple, query string, limit int) ([]RankedSkill, error)

	// Delete removes the named skill under the identity. Missing →
	// `ErrSkillNotFound`. Emits `skill.deleted` on success.
	Delete(ctx context.Context, id identity.Quadruple, name string) error

	// Close releases the driver's resources. Subsequent method
	// calls return `ErrStoreClosed`. Close is idempotent.
	Close(ctx context.Context) error
}

SkillStore is Harbor's mandatory skill-storage interface. A single surface; every V1 driver (`localdb` here; Portico post-V1) implements every method. No `Supports*` ceremony per AGENTS.md §4.4.

Identity-mandatory contract:

  • Every method validates the identity `Quadruple` at the boundary. Empty tenant / user / session returns wrapped `ErrIdentityRequired` AND emits one `skill.identity_rejected` event on the bus.

Concurrent-reuse contract:

  • One instance is safe to share across N concurrent goroutines. Mutable state is internally synchronised; per-call state lives in `ctx` and the supplied `Quadruple`, never on the driver.

func Open

func Open(_ context.Context, cfg ConfigSnapshot, deps Deps) (SkillStore, error)

Open returns the `SkillStore` built by the factory whose name matches `cfg.Driver` (defaults to `DefaultDriver` when empty).

Deps are validated: a missing EventBus returns a wrapped error before the factory runs — fail loudly, never silently degrade.

func OpenDriver

func OpenDriver(name string, cfg ConfigSnapshot, deps Deps) (SkillStore, error)

OpenDriver opens a specific driver by name; useful for tests that want to exercise the registry against a non-default driver.

type SkillUpsertedPayload

type SkillUpsertedPayload struct {
	events.SafeSealed
	Name        string
	Origin      Origin
	Scope       Scope
	ContentHash string
	Idempotent  bool // true → existing row's hash matched; no write occurred
}

SkillUpsertedPayload reports a successful upsert. SafePayload by construction — all fields are bounded enumerable strings / ints / booleans; no caller-controlled bytes leak through.

type SkillView

type SkillView struct {
	Name     string `json:"name"`
	Title    string `json:"title,omitempty"`
	Trigger  string `json:"trigger,omitempty"`
	TaskType string `json:"task_type,omitempty"`
	// Pinned is true when the skill was anchored by either
	// DirectoryConfig.Pinned (config-time) or Skill.Extra["pinned"]
	// (runtime-time). Surfaced for Console rendering; inert to the
	// planner's reasoning.
	Pinned bool `json:"pinned"`
}

SkillView is the planner-visible projection of a Skill returned by Directory.View. Four content fields plus a Pinned marker; the rest of the Skill is dropped at the boundary so the View is cheap to inject and Console-renderable without leaking storage-layer detail.

Directories

Path Synopsis
Package capfilter holds the primitive capability-filter and tool-name-scrub logic shared by the skills planner tools (internal/skills/tools) and the virtual directory (internal/skills).
Package capfilter holds the primitive capability-filter and tool-name-scrub logic shared by the skills planner tools (internal/skills/tools) and the virtual directory (internal/skills).
Package conformancetest is the shared `SkillStore` conformance harness.
Package conformancetest is the shared `SkillStore` conformance harness.
drivers
localdb
Package localdb is Harbor's SQLite-backed `skills.SkillStore` driver.
Package localdb is Harbor's SQLite-backed `skills.SkillStore` driver.
Package generator owns Harbor's in-runtime skill generator with persistence (RFC §6.7).
Package generator owns Harbor's in-runtime skill generator with persistence (RFC §6.7).
Package importer ships Harbor's Skills.md importer — the predecessor's gap-closer (RFC §6.7).
Package importer ships Harbor's Skills.md importer — the predecessor's gap-closer (RFC §6.7).
Package tools registers Harbor's planner-facing skill tools (`skill_search`, `skill_get`, `skill_list`) into the `tools.ToolCatalog`.
Package tools registers Harbor's planner-facing skill tools (`skill_search`, `skill_get`, `skill_list`) into the `tools.ToolCatalog`.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL