tools

package
v1.4.0 Latest Latest
Warning

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

Go to latest
Published: Jun 16, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

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):

  1. Capability filter — a skill is visible only when its `RequiredTools / RequiredNS / RequiredTags` are subsets of the planner-supplied `CapabilityContext.AllowedTools / AllowedNamespaces / AllowedTags`.
  2. 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.
  3. 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

View Source
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

View Source
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):

  1. Full — every field intact.
  2. Drop optional — clear `Preconditions` and `FailureModes`.
  3. Cap steps — truncate `Steps` to 3 (and drop optional as in step 2).
  4. 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

type Deps struct {
	Bus events.EventBus
}

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.

Jump to

Keyboard shortcuts

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