Documentation
¶
Overview ¶
Package tools registers Harbor's planner-facing skill tools (`skill_search`, `skill_get`, `skill_list`) into the `tools.ToolCatalog`. Each handler wraps the `skills.SkillStore` with three injection-time concerns (RFC §6.7):
- Capability filter — a skill is visible only when its `RequiredTools / RequiredNS / RequiredTags` are subsets of the planner-supplied `CapabilityContext.AllowedTools / AllowedNamespaces / AllowedTags`.
- Redaction — disallowed tool names are scrubbed from skill text and replaced with `"a suitable tool (use tool_search)"` (when `tool_search` itself is allowed) or `"a suitable tool"` (otherwise). Optional PII redaction (`CapabilityContext.RedactPII`) additionally rewrites emails / phone / bearer-tokens / URL query strings across titles / triggers / descriptions / steps / preconditions / failure modes.
- Tiered budgeter — `skill_get` fits the returned skills inside a planner-supplied `MaxTokens` envelope by stepping a ladder: full → drop optional (preconditions, failure_modes) → cap steps to 3 → `ErrSkillTooLarge`. Fail-loud per CLAUDE.md §5; no silent degradation.
Identity-mandatory: every handler validates the `(tenant, user, session)` triple at the boundary via `identity.From(ctx)`. Missing identity returns the wrapped `skills.ErrIdentityRequired` AND emits `skill.identity_rejected` on the bus (via `skills.EmitIdentityRejected`). The design removes the predecessor's `require_explicit_key` knob — there is no opt-out.
Concurrent reuse: the catalog + the registered descriptors are stateless after `Register`; per-call state lives on `(ctx, args)`. One catalog + one store is safe to share across N concurrent goroutines.
Registration is a runtime call (the catalog is built at boot, not at package-init). Since the PRODUCTION registration path is the `internal/tools/builtin` carrier: the `skill_search` / `skill_get` / `skill_list` built-ins delegate to the exported handlers below (`SearchHandler` / `GetHandler` / `ListHandler`) with a runtime-computed capability envelope, so the capability filter + redaction + budgeter run on the production path. `Register` remains the direct Go-level registration entry for headless SDK consumers composing their own catalog (see docs/recipes/use-memory-and-skills-from-go.md). The package is blank-imported via `internal/drivers/prod`.
Index ¶
- Constants
- Variables
- func Filter(in []skills.Skill, cap CapabilityContext) []skills.Skill
- func Fit(in []skills.Skill, maxTokens int) (out []skills.Skill, summarized bool, droppedSteps bool, err error)
- func Redact(s skills.Skill, cap CapabilityContext) skills.Skill
- func Register(catalog tcat.ToolCatalog, store skills.SkillStore, deps Deps) error
- type CapabilityContext
- type Deps
- type GetArgs
- type GetResult
- type ListArgs
- type ListResult
- type SearchArgs
- type SearchResult
Constants ¶
const ( // ToolNameSkillSearch — `skill_search(query, limit, capability) // -> {skills, path}`. Returns ranked candidates from the // SkillStore's FTS5 → regex → exact ladder, filtered // by capability and redacted. ToolNameSkillSearch = "skill_search" // ToolNameSkillGet — `skill_get(names[], max_tokens, capability) // -> {skills, summarized, dropped_steps}`. Returns the full // content of the named skills, redacted and budget-fit through // the tiered ladder. ToolNameSkillGet = "skill_get" // ToolNameSkillList — `skill_list(scope, task_type, tags, limit, // offset, capability) -> {skills}`. Returns a paged enumeration // filtered by capability and redacted. ToolNameSkillList = "skill_list" )
Tool names registered into the catalog. These are load-bearing strings — the planner prompt references them by name and a silent rename would break every downstream model. The smoke script pins them with a string-grep.
Variables ¶
var ErrSkillTooLarge = errors.New("skills/tools: skill exceeds max_tokens after ladder")
ErrSkillTooLarge is returned by `skill_get` when the tiered budgeter cannot fit the requested skills within `max_tokens` after exhausting the ladder (full → drop optional → cap steps). Fail-loud per CLAUDE.md §5 — callers must not silently degrade.
Functions ¶
func Filter ¶
func Filter(in []skills.Skill, cap CapabilityContext) []skills.Skill
Filter applies the capability subset gate to `in`. A skill passes when, for each of `RequiredTools / RequiredNS / RequiredTags`, the required entries are a subset of the corresponding `Allowed*` set in `cap`. A skill with empty required lists for every axis is always allowed.
The order of `in` is preserved. The returned slice is a fresh allocation — callers may mutate it freely. Concurrent-safe by construction (pure function over value-type inputs).
The subset logic lives in capfilter — the single source shared with the virtual directory (internal/skills). By design: "the skill's `RequiredTools/Namespaces/Tags` must be subsets of the allowed sets."
func Fit ¶
func Fit(in []skills.Skill, maxTokens int) (out []skills.Skill, summarized bool, droppedSteps bool, err error)
Fit applies the tiered budgeter ladder to `in` and returns the first slice that fits within `maxTokens`. The ladder (a §4.5):
- Full — every field intact.
- Drop optional — clear `Preconditions` and `FailureModes`.
- Cap steps — truncate `Steps` to 3 (and drop optional as in step 2).
- Fail loud — return `ErrSkillTooLarge`.
Returns:
- `out` — the fit slice (may be empty if `in` was empty).
- `summarized` — true when step 1 (drop optional) fired.
- `droppedSteps` — true when step 2 (cap steps) fired.
- `err` — non-nil iff the budget was exhausted after step 3.
Token estimation is chars/4 (matches the §6.5 LLM safety net's estimator at V1). A tokenizer-backed estimator is a post-V1 swap-in via this single function.
Concurrent-safe by construction: pure function over value inputs. `in` is not mutated; the returned slice is a fresh allocation when the ladder steps fire.
func Redact ¶
func Redact(s skills.Skill, cap CapabilityContext) skills.Skill
Redact returns a copy of `s` with disallowed tool names scrubbed from every text field AND (when `cap.RedactPII=true`) PII patterns rewritten to `[REDACTED-PII]`.
Skill fields rewritten:
- Title
- Description
- Trigger
- Steps (every entry)
- Preconditions (every entry)
- FailureModes (every entry)
Slices on the returned skill are fresh allocations so the caller may not perturb the SkillStore's cached row.
Tool-name redaction matches tool names with word-boundary regex so a tool named `email` doesn't false-positive on `"emails"`. The regex set is built per-call from `cap.AllowedTools` — at the per-skill scale, the cost is dominated by the underlying string rewrite, not the regex compilation.
Concurrent-safe by construction: pure function over value inputs; no shared mutable state.
func Register ¶
func Register(catalog tcat.ToolCatalog, store skills.SkillStore, deps Deps) error
Register installs `skill_search`, `skill_get`, `skill_list` into `catalog`, wired against `store`. Returns wrapped errors on validation failure or catalog conflicts (e.g. a duplicate name — indicates a misconfigured boot path, not a runtime fault).
Concurrent reuse: the registered descriptors hold only an immutable closure over `store` + `deps`; the concurrent-reuse contract holds.
Types ¶
type CapabilityContext ¶
type CapabilityContext 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).
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"`
// RedactPII opts in to PII redaction across skill text
// (emails / phone / bearer tokens / URL query strings).
RedactPII bool `json:"redact_pii,omitempty"`
}
CapabilityContext is the planner-supplied envelope that gates and shapes a skill at injection time. Two purposes:
- Capability filter: a skill is visible only when its `RequiredTools / RequiredNS / RequiredTags` are subsets of the corresponding Allowed* set (see `filter.go`).
- Redaction: tool names not in `AllowedTools` are scrubbed from skill text; `RedactPII=true` additionally redacts PII patterns (see `redactor.go`).
Empty `AllowedTools` / `AllowedNamespaces` / `AllowedTags` means "the planner did not declare a capability set"; skills with empty required lists pass; skills with non-empty required lists are excluded (a "default-deny" stance).
Concurrent reuse: the struct is a value carried on the args; it is never mutated in-flight. Multiple goroutines may share a CapabilityContext value safely (slice headers are read-only).
type Deps ¶
Deps carries the runtime dependencies `Register` needs. `Bus` is mandatory so the identity-rejection emit path (`skills.EmitIdentityRejected`) lands on the audit pipeline.
type GetArgs ¶
type GetArgs struct {
// Names are the skill names to fetch. Each is looked up via
// `SkillStore.Get`; missing names are SKIPPED (not an error) so
// a partial response remains useful when one name is stale.
Names []string `json:"names"`
// MaxTokens caps the returned skills' combined estimated
// token count. 0 → `defaultMaxTokens` (1024).
MaxTokens int `json:"max_tokens,omitempty"`
// Capability gates + redacts the returned skills.
Capability CapabilityContext `json:"capability"`
}
GetArgs is the input shape for `skill_get`.
type GetResult ¶
type GetResult struct {
// Skills are the requested skills after capability filter +
// redaction + budgeter. The slice MAY be empty (every requested
// skill was filtered out or missing); it is never partially
// over-budget.
Skills []skills.Skill `json:"skills"`
// Summarized reports whether the budgeter dropped optional
// fields (preconditions / failure modes) — ladder step 1.
Summarized bool `json:"summarized"`
// DroppedSteps reports whether the budgeter capped procedural
// steps to 3 — ladder step 2.
DroppedSteps bool `json:"dropped_steps"`
}
GetResult is the output shape for `skill_get`.
func GetHandler ¶ added in v1.3.0
func GetHandler(ctx context.Context, store skills.SkillStore, bus events.EventBus, args GetArgs) (GetResult, error)
GetHandler is the `skill_get` planner-tool body. Identity → fetch each named skill → Filter → Redact → Budgeter ladder. Missing names are silently skipped (a partial response is more useful than a hard error for stale planner caches); the budgeter is the only hard error path.
Exported as the handler seam the `internal/tools/builtin` carrier delegates to.
type ListArgs ¶
type ListArgs struct {
Scope skills.Scope `json:"scope,omitempty"`
TaskType string `json:"task_type,omitempty"`
Tags []string `json:"tags,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
Capability CapabilityContext `json:"capability"`
}
ListArgs is the input shape for `skill_list`.
type ListResult ¶
type ListResult struct {
// Skills are the paged rows after capability filter + redaction.
// The list is NOT budget-trimmed — callers requesting a paged
// enumeration accept that the planner-facing text is
// summary-only (Title + Trigger + Tags), not full content.
Skills []skills.Skill `json:"skills"`
}
ListResult is the output shape for `skill_list`.
func ListHandler ¶ added in v1.3.0
func ListHandler(ctx context.Context, store skills.SkillStore, bus events.EventBus, args ListArgs) (ListResult, error)
ListHandler is the `skill_list` planner-tool body. Identity → SkillStore.List → Filter → Redact (summary fields only — full content is reserved for `skill_get`).
Exported as the handler seam the `internal/tools/builtin` carrier delegates to.
type SearchArgs ¶
type SearchArgs struct {
Query string `json:"query"`
Limit int `json:"limit,omitempty"`
Capability CapabilityContext `json:"capability"`
}
SearchArgs is the input shape for `skill_search`. Reflected into a JSON Schema by the inproc driver at registration time.
type SearchResult ¶
type SearchResult struct {
// Skills are the ranked candidates after capability filter +
// redaction. The slice never exceeds `Limit` (or the SkillStore
// default when zero).
Skills []skills.RankedSkill `json:"skills"`
// Path is the ranking ladder branch that produced the rows
// ("fts5" | "regex" | "exact"). Surfaced for observability;
// inert to the planner's reasoning.
Path string `json:"path"`
}
SearchResult is the output shape for `skill_search`.
func SearchHandler ¶ added in v1.3.0
func SearchHandler(ctx context.Context, store skills.SkillStore, bus events.EventBus, args SearchArgs) (SearchResult, error)
SearchHandler is the `skill_search` planner-tool body. Identity from ctx → SkillStore.Search → Filter → Redact (per-row). Path is surfaced for observability.
Exported as the handler seam the `internal/tools/builtin` carrier delegates to — ONE implementation home, two registration carriers collapse onto it. Callers supply the capability envelope on `args.Capability`; the builtin carrier computes it from the run's visible-tool set.