capability

package
v0.11.13 Latest Latest
Warning

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

Go to latest
Published: May 13, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package capability declares per-provider hook-support metadata and a self-registering registry for it.

Why a separate package: provider sub-packages (provider/claude, provider/codex, …) need to declare their own Capability in init(). If Capability lived in the parent provider/ package, those sub-packages would have to import their parent — but provider/ would then need to know about its children to seed the registry, creating a cycle. Hosting the registry here breaks that loop: every package imports capability/, and capability/ imports nothing from agents/.

Lifecycle:

  1. At program init, each provider sub-package calls Register(name, cap) describing what its hook adapter can do statically (HookSupported, InterceptScope). Nothing runs the provider binary yet.
  2. Later, when the UI toggles gate ON for an instance, HookCapabilityCheck spawns the real binary with a sentinel and flips HookVerified. That runtime probe lives in a separate file (check.go, D5).

Index

Constants

View Source
const ProbeTimeout = 30 * time.Second

ProbeTimeout is the wall-clock budget for a single capability check. Generous because provider CLIs on Windows (.cmd shims wrapping node) can take 1-3s just to print --version, never mind doing a real prompt round-trip. If the probe doesn't finish in this window we treat it as "unverified, retry later" rather than a hard failure.

Variables

View Source
var ErrProbeInflight = errors.New("probe already in flight for this provider")

ErrProbeInflight is returned by HookCapabilityCheck when a probe is already running for the same provider name. UI surfaces this as "another probe in flight" rather than retrying.

View Source
var ErrUnsupported = errors.New("hook config not supported for this provider")

ErrUnsupported is returned by HookConfigWriter implementations whose provider does not yet have a verified hook integration. Gemini is the canonical case: an adapter is shipped in code (so Capability shows up in the registry) but the on-disk hook config format hasn't been validated against the real CLI, so the writer refuses to touch the filesystem rather than write something that might trip the user up.

Functions

func All

func All() map[string]Capability

All returns a snapshot of every registered Capability, keyed by name. Used by the Providers UI to render one row per known provider in a stable order regardless of registration timing.

func IsProbing

func IsProbing(providerName string) bool

IsProbing reports whether a HookCapabilityCheck is currently running for the named provider. Used by the HTTP layer / templ to disable the Test / Enable buttons while a probe is in flight.

func Register

func Register(name string, c Capability)

Register stores a Capability under name. Last write wins — duplicate registration silently overwrites, which is what tests want when they re-seed the registry. Init-time duplicates from real code would be a bug worth catching, but checking that here would force tests to deal with panics; we accept the trade-off and rely on code review instead.

func RegisterHookConfigWriter

func RegisterHookConfigWriter(name string, w HookConfigWriter)

RegisterHookConfigWriter pairs a HookConfigWriter with a provider name. Called from provider sub-package init() functions alongside Register so the gate-toggle path can look up "for this provider, which writer installs the hook?" without a central switch.

func RegisterProber

func RegisterProber(name string, p Prober)

RegisterProber pairs a Prober with a provider name. Same self- registration pattern as HookConfigWriter — called from provider sub-package init() so adding a new CLI doesn't require touching a central dispatch.

Types

type Capability

type Capability struct {
	HookSupported  bool
	HookVerified   bool
	HookProbedAt   time.Time
	HookError      string
	InterceptScope string
}

Capability is the per-provider hook support metadata.

HookSupported is the static declaration: "an adapter for this provider exists in the codebase and is wired into the gate binary." UI uses this to decide whether the gate toggle is even available.

HookVerified is the dynamic claim: "we spawned the real binary and confirmed the deny path is honored." Set by HookCapabilityCheck (D5); stays false until that probe runs successfully.

InterceptScope is a free-form short label describing which tool classes the hook covers ("bash+edit+mcp", "shell-only", "untested"). Surfaced in the UI badge so users see coverage at a glance.

func Lookup

func Lookup(name string) (Capability, bool)

Lookup returns the registered Capability for name. The second return is false when no provider has registered under name yet — callers should treat that as "hook not supported" rather than panicking.

type CheckInput

type CheckInput struct {
	// ProviderName is the registry key for the Capability/Writer/Prober
	// triple this check should exercise.
	ProviderName string

	// GateBinary is the absolute path to the <app>-gate executable the
	// provider will invoke via its hook. Empty means "skip probe; we
	// can't verify without a gate to point at".
	GateBinary string

	// WorkspaceRoot is where the probe's throwaway workspace will be
	// created. Empty → os.TempDir. The probe writes a fresh subdir
	// underneath and removes it on completion regardless of outcome.
	WorkspaceRoot string
}

CheckInput is the per-provider data HookCapabilityCheck needs to drive a probe. Kept minimal so test code can construct it without pulling the whole provider.Instance struct in.

type CheckResult

type CheckResult struct {
	Capability
}

CheckResult is what HookCapabilityCheck returns. It mirrors the Capability registry entry but with the runtime-probe fields filled in. Callers typically merge it back into the registry so subsequent Lookup() calls reflect the verified state.

func HookCapabilityCheck

func HookCapabilityCheck(ctx context.Context, in CheckInput) CheckResult

HookCapabilityCheck spawns the named provider with a probe-mode hook config that forces a deny, asks the provider to touch a sentinel file, and reports whether the deny was honored. The returned Capability has HookVerified=true exactly when the sentinel did NOT appear; HookError carries the reason on any failure path.

The function does NOT mutate the registry. Callers decide whether to merge the result back via Register — typically yes for a real probe, no for ad-hoc CLI runs (`wick agents capability`) that want to inspect a result without altering global state.

Probe sequence:

  1. Look up the static Capability. If HookSupported=false, return early — there's no adapter to verify.
  2. Look up the HookConfigWriter and Prober. Either missing → HookError="<role> not registered for <provider>".
  3. Create a temp workspace + sentinel path inside it.
  4. Writer.Write installs the hook config pointing at GateBinary with `--probe-deny --provider=<name>`.
  5. Prober.SendSentinel spawns the provider and asks it to touch the sentinel.
  6. Check the sentinel: absent = verified, present = unverified.
  7. Cleanup workspace.

type HookConfigWriter

type HookConfigWriter interface {
	Write(workspace, gateBin string) error
	Remove(workspace string) error
	DryRun(workspace, gateBin string) (path string, content []byte, err error)
}

HookConfigWriter is the per-provider strategy for installing / removing the hook command in the provider's project-scoped settings directory.

Lifecycle (per spawn):

  • GateEnabled=true → Write(workspace, gateBin)
  • GateEnabled=false → Remove(workspace) (clean up stale config from a previous run where gate was on)

Implementations must be idempotent: Write twice with the same args produces the same on-disk content; Remove on a non-existent file is a no-op. The point is that a partially-failed previous run leaves the world in a recoverable state.

DryRun(workspace, gateBin) returns the path the writer *would* touch without actually writing. The capability probe (D5) uses it to spawn the provider against a temp workspace without polluting any real settings file.

func LookupHookConfigWriter

func LookupHookConfigWriter(name string) (HookConfigWriter, bool)

LookupHookConfigWriter returns the writer registered for name. The second return is false when no provider has registered one — the spawn path treats that as "this provider can't be gated yet" and skips both Write and Remove rather than guessing a default.

type Prober

type Prober interface {
	SendSentinel(ctx context.Context, workspace, sentinelPath string) error
}

Prober is the per-provider strategy for verifying that a freshly- installed hook config actually intercepts shell commands. Called by HookCapabilityCheck (D5) once the writer (above) has put a probe config into a throwaway workspace. The implementation spawns the real provider binary, asks it to touch a sentinel file, waits for the spawn to exit, and reports nil if the sentinel did not appear (deny was honored). Any non-nil error — sentinel created, spawn failed, timeout — flips HookVerified to false on the registry entry.

Probers are provider-specific because each CLI has its own way of receiving a one-shot prompt: claude takes stream-json on stdin, codex has `codex exec`, gemini has `-p`. Encoding that variety here would couple capability/ to every CLI's flag surface; the Prober interface keeps it abstract.

func LookupProber

func LookupProber(name string) (Prober, bool)

LookupProber returns the prober registered for name. False means "this provider has no probe defined" — HookCapabilityCheck treats that as HookVerified=false with HookError="prober not registered".

Jump to

Keyboard shortcuts

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