agent

package
v3.104.1 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: AGPL-3.0 Imports: 25 Imported by: 0

Documentation

Overview

Package agent implements the coding-agent surface: the hook engine that answers a tool call, and the installer that wires a host up to it.

The design constraint the whole package serves: a coding agent only learns what its hooks tell it. Silence is the correct output whenever the repository policy is satisfied, and everything else has to be worth an interruption.

Index

Constants

View Source
const CapabilitiesFile = ".vulnetix/capabilities.yaml"

CapabilitiesFile is where the detected surface is recorded.

The path, the schema and the key names are a contract with the plugin's skills, which read this file to scope themselves to what a machine actually has. It is reproduced here rather than redesigned: the point of moving detection into Go is that it becomes correct and cross-platform, not that it becomes different.

View Source
const PolicyFile = ".vulnetix/agent.yaml"

PolicyFile is where a repository states what its agents may do without being interrupted.

Variables

View Source
var Hosts = []Host{
	{
		ID:               "claude-code",
		Name:             "Claude Code",
		SkillDirs:        []string{"~/.claude/skills"},
		ProjectSkillDirs: []string{".claude/skills"},
		HookDialect:      DialectClaudeCode,
		HookConfig:       "~/.claude/settings.json",
		MCP:              true,
		Detect:           []string{"~/.claude"},
	},
	{
		ID:   "codex",
		Name: "OpenAI Codex",

		SkillDirs:        []string{"~/.agents/skills"},
		ProjectSkillDirs: []string{".agents/skills"},
		HookDialect:      DialectCodex,
		HookConfig:       "~/.codex/hooks.json",
		MCP:              true,
		Detect:           []string{"~/.codex"},
	},
	{
		ID:               "cursor",
		Name:             "Cursor",
		SkillDirs:        []string{"~/.agents/skills", "~/.cursor/skills"},
		ProjectSkillDirs: []string{".agents/skills", ".cursor/skills"},
		MCP:              true,
		Detect:           []string{"~/.cursor"},
	},
	{
		ID:               "gemini-cli",
		Name:             "Gemini CLI",
		SkillDirs:        []string{"~/.agents/skills", "~/.gemini/skills"},
		ProjectSkillDirs: []string{".agents/skills", ".gemini/skills"},
		MCP:              true,
		Detect:           []string{"~/.gemini"},
	},
	{
		ID:               "github-copilot",
		Name:             "GitHub Copilot",
		SkillDirs:        []string{"~/.copilot/skills"},
		ProjectSkillDirs: []string{".github/skills"},
		MCP:              true,
		Detect:           []string{"~/.copilot"},
	},
	{
		ID:               "opencode",
		Name:             "opencode",
		SkillDirs:        []string{"~/.config/opencode/skills"},
		ProjectSkillDirs: []string{".opencode/skills"},
		MCP:              true,
		Detect:           []string{"~/.config/opencode"},
	},
	{
		ID:               "pi",
		Name:             "Pi",
		SkillDirs:        []string{"~/.agents/skills", "~/.pi/agent/skills"},
		ProjectSkillDirs: []string{".agents/skills", ".pi/skills"},
		MCP:              true,
		Detect:           []string{"~/.pi"},
	},
	{
		ID:               "windsurf",
		Name:             "Windsurf",
		SkillDirs:        []string{"~/.codeium/windsurf/skills"},
		ProjectSkillDirs: []string{".windsurf/skills"},
		MCP:              true,
		Detect:           []string{"~/.codeium/windsurf"},
	},
	{
		ID:               "zed",
		Name:             "Zed",
		SkillDirs:        []string{"~/.agents/skills"},
		ProjectSkillDirs: []string{".agents/skills"},
		MCP:              true,
		Detect:           []string{"~/.config/zed"},
	},
	{
		ID:               "amp",
		Name:             "Amp",
		SkillDirs:        []string{"~/.config/agents/skills"},
		ProjectSkillDirs: []string{".agents/skills"},
		MCP:              true,
		Detect:           []string{"~/.config/amp"},
	},
}

Hosts is every agent this CLI knows how to wire.

Two rules govern entries here. A skills directory is only listed when it is the path the host actually reads, verified against that host's own documentation rather than inferred from its name. A hook dialect is only named when this CLI has been run against that host and its response accepted; everything else is skills-only, which is honest and still useful.

Functions

func CapabilitiesFresh added in v3.104.0

func CapabilitiesFresh(root string) bool

CapabilitiesFresh reports whether a recorded detection is still within its TTL.

A file that cannot be read or parsed is not fresh. Treating an unreadable file as current would pin a repository to a detection nobody can see.

func CapabilitySummary added in v3.104.0

func CapabilitySummary(c Capabilities) string

CapabilitySummary renders the one-line-per-section view the command prints.

func ExpandHome

func ExpandHome(p string) string

ExpandHome resolves a leading ~ against the user's home directory.

func HookCommand

func HookCommand() string

HookCommand is the command a host runs for every hook event.

The binary's absolute path rather than a bare name: a host does not necessarily inherit the shell PATH that installed the CLI, and a hook that cannot be found fails silently on some hosts.

func SortedHostIDs

func SortedHostIDs() []string

SortedHostIDs lists every known host identifier, for help text and docs.

func WriteCapabilities added in v3.104.0

func WriteCapabilities(root string, c Capabilities) (string, error)

WriteCapabilities records a detection, creating .vulnetix/ if it is missing.

Types

type Assessment

type Assessment struct {
	Candidate

	// Resolved is the version that would actually be installed when the command
	// did not pin one. Empty when nothing could resolve it, which is not the
	// same as the package being fine.
	Resolved string

	// NameLevel records that the command named no version and nothing could
	// resolve the one it would install, so Insight describes the package's
	// whole history rather than a release. A malware flag at that level means
	// "some version has been malicious", which is true of express and is not a
	// reason to refuse `npm i express`.
	NameLevel bool

	Vulns   []scan.EnrichedVuln
	Insight *vdb.CliPackageInsight

	// Unknown records that the lookup could not answer for this package: no
	// credential, no network, an unparseable manifest, a timeout. It is never a
	// verdict, and it never blocks. Absence of an answer is not an answer.
	Unknown bool
	// ExploitsGated records that the server withheld exploit intelligence
	// rather than reporting none.
	ExploitsGated bool
}

Assessment is everything known about one package the agent is about to add.

type Auth

type Auth struct {
	// Community is true when no credential of the user's own was found and the
	// built-in Community credentials are in use.
	Community bool
	// Missing is true when there is no credential at all, community included.
	Missing bool
}

Auth reports how the CLI is authenticated, so the guard can say when it is running on the shared pool.

type BashInput

type BashInput struct {
	Command     string `json:"command"`
	Description string `json:"description"`
}

BashInput is the tool_input shape for a shell call.

type Candidate

type Candidate struct {
	// Name is the registry name, scope included for npm.
	Name string
	// Version is the requested version when the command pinned one, empty when
	// the command left resolution to the package manager. Empty is the common
	// case and is not an error: the guard resolves what would be installed.
	Version string
	// Ecosystem is the purl-style ecosystem, matching what VDB expects.
	Ecosystem string
	// Manager is the tool that would perform the install, for the message.
	Manager string
}

Candidate is one package an agent is about to add, recovered from the command it was about to run.

func ParseInstallCommand

func ParseInstallCommand(command string) []Candidate

ParseInstallCommand recovers the packages a shell command would add.

Returns nil for anything that is not adding a named dependency, which is the overwhelmingly common case and must stay silent: a bare `npm install` reinstalls a manifest that is already committed, `pip install -r req.txt` is the same, and a git or filesystem spec is not a registry package this can say anything about.

The command is scanned in segments, so `cd web && npm i axios` and `NODE_ENV=x npm i axios && npm run build` both resolve.

type Capabilities added in v3.104.0

type Capabilities struct {
	SchemaVersion int    `yaml:"schema_version"`
	DetectedAt    string `yaml:"detected_at"`

	// Binaries reports which tools resolve on PATH.
	Binaries map[string]bool `yaml:"binaries"`
	// Repo reports which project markers exist in the tree.
	Repo map[string]bool `yaml:"repo"`
	// Derived is the small set of conclusions worth drawing once rather than in
	// every consumer.
	Derived CapabilityDerived `yaml:"derived"`
}

Capabilities is what this machine and this repository can do.

func DetectCapabilities added in v3.104.0

func DetectCapabilities(root string, auth Auth) Capabilities

DetectCapabilities inspects the machine and the repository.

type CapabilityDerived added in v3.104.0

type CapabilityDerived struct {
	PrimaryPackageManager string   `yaml:"primary_package_manager"`
	HasContainers         bool     `yaml:"has_containers"`
	HasIAC                bool     `yaml:"has_iac"`
	HasCI                 bool     `yaml:"has_ci"`
	DetectionStack        []string `yaml:"detection_stack,flow"`
	SBOMStack             []string `yaml:"sbom_stack,flow"`
	SOAR                  string   `yaml:"soar"`
	AuthStatus            string   `yaml:"auth_status"`
}

CapabilityDerived holds conclusions drawn from the two maps above.

type Decision

type Decision int

Decision is what the hook concluded.

const (
	// Silent says nothing at all. This is the correct outcome whenever the
	// repository's own policy is satisfied, and it is the outcome the guard
	// reaches most often.
	Silent Decision = iota
	// Inform adds what the hook knows to the model's context without
	// interrupting.
	Inform
	// Block refuses the tool call and tells the model why.
	Block
)

type EditInput

type EditInput struct {
	FilePath  string `json:"file_path"`
	Path      string `json:"path"`
	OldString string `json:"old_string"`
	NewString string `json:"new_string"`
	Content   string `json:"content"`
}

EditInput is the tool_input shape for a file write. Hosts spell the path key differently across their Edit and Write tools, so every spelling is decoded and the first non-empty one wins.

func (EditInput) TargetPath

func (e EditInput) TargetPath() string

TargetPath returns the file an edit would touch.

type Event

type Event string

Event is a hook lifecycle event.

Only the events this package answers are named. A host that fires something else gets an empty response rather than an error: an unrecognised event is not a failure, it is an event with nothing to say about it.

const (
	EventPreToolUse       Event = "PreToolUse"
	EventPostToolUse      Event = "PostToolUse"
	EventUserPromptSubmit Event = "UserPromptSubmit"
	EventSessionStart     Event = "SessionStart"
	EventStop             Event = "Stop"
)

type GuardPolicy

type GuardPolicy struct {
	Block []Signal `yaml:"block"`
	Warn  []Signal `yaml:"warn"`
}

GuardPolicy says which signals block and which merely inform.

A signal in neither list is silent. That default matters: a signal this version of the CLI has not heard of should not start interrupting people because a newer server began reporting it.

func (GuardPolicy) Decide

func (g GuardPolicy) Decide(s Signal) Decision

Decide maps a signal onto what to do about it.

type HookDialect

type HookDialect string

HookDialect is how a host spells its hook configuration.

const (
	// DialectNone means this build cannot configure the host's hooks.
	//
	// It is not the same as the host having none. Several hosts document a hook
	// system that has not been verified against this CLI, and claiming support
	// that has not been tested is how the previous install docs came to promise
	// a directory Codex does not read.
	DialectNone HookDialect = ""
	// DialectClaudeCode is settings.json with a hooks object keyed by event.
	DialectClaudeCode HookDialect = "claude-code"
	// DialectCodex is hooks.json with a description and a hooks object.
	DialectCodex HookDialect = "codex"
)

type Host

type Host struct {
	// ID is the stable identifier, matching what `gh skill install --agent`
	// calls the host wherever one exists.
	ID string
	// Name is what the host calls itself.
	Name string

	// SkillDirs are the user-scope skill directories, most preferred first.
	// Several hosts read the interoperable ~/.agents/skills alongside their own.
	SkillDirs []string
	// ProjectSkillDirs are the repository-scope equivalents.
	ProjectSkillDirs []string

	// HookDialect is how this host's hooks are configured, if this build can.
	HookDialect HookDialect
	// HookConfig is the user-scope file the hook configuration lives in.
	HookConfig string

	// MCP records whether the host can be pointed at an MCP server.
	MCP bool

	// Detect are paths whose existence means the host is installed. A missing
	// binary is not proof of absence — several hosts ship as an extension
	// rather than a command — so a configuration directory counts.
	Detect []string
}

Host is one coding agent this CLI can wire up.

The table is the single source of truth for what is supported. The installer acts on it, `agent hosts` prints it, and the documentation and marketing matrices are generated from that output — so a page cannot claim a capability the installer does not implement.

func DetectHosts

func DetectHosts() []Host

DetectHosts lists the hosts present on this machine.

func HostByID

func HostByID(id string) (Host, bool)

HostByID finds a host by its identifier.

func (Host) Installed

func (h Host) Installed() bool

Installed reports whether the host appears to be present on this machine.

func (Host) Supports

func (h Host) Supports(s Surface) bool

Supports reports whether this build can wire a surface for this host.

func (Host) Surfaces

func (h Host) Surfaces() []Surface

Surfaces lists what this build can wire for the host, in a stable order.

type InstallOptions

type InstallOptions struct {
	// DryRun reports what would change without writing.
	DryRun bool
	// Hooks configures lifecycle hooks where the host supports them.
	Hooks bool
}

InstallOptions controls what an install writes.

type InstallResult

type InstallResult struct {
	Host Host
	// Wired lists the surfaces configured.
	Wired []Surface
	// Changed is false when everything was already in place, which is the
	// common case on a re-run and is worth saying rather than reporting work
	// that did not happen.
	Changed bool
	// Notes carry anything the user should know, including surfaces this build
	// deliberately did not configure.
	Notes []string
	Err   error
}

InstallResult records what wiring one host actually changed.

func InstallHooks

func InstallHooks(h Host, opts InstallOptions) InstallResult

InstallHooks wires one host's hook configuration.

Existing configuration is preserved: the file is read, this CLI's entry is added or updated in place, and everything else is written back untouched. A tool that replaces a user's settings file to add one line is not one people run twice.

func Uninstall

func Uninstall(h Host, opts InstallOptions) InstallResult

Uninstall removes this CLI's hook entry, leaving the rest of the file as it was.

type Lookup

type Lookup interface {
	Assess(ctx context.Context, cands []Candidate) []Assessment
}

Lookup answers what is known about a set of packages.

An interface so the decision logic can be exercised without a network, which is most of what the guard's tests do: what to say is a policy question and should not need a credential to answer.

type Payload

type Payload struct {
	SessionID      string          `json:"session_id"`
	TranscriptPath string          `json:"transcript_path"`
	CWD            string          `json:"cwd"`
	HookEventName  Event           `json:"hook_event_name"`
	PermissionMode string          `json:"permission_mode"`
	ToolName       string          `json:"tool_name"`
	ToolInput      json.RawMessage `json:"tool_input"`
	ToolUseID      string          `json:"tool_use_id"`
	// UserInput carries the prompt on UserPromptSubmit. Hosts disagree on the
	// key, so both spellings are accepted.
	UserInput string `json:"user_input"`
	Prompt    string `json:"prompt"`
}

Payload is what a host sends on stdin.

The field set is the intersection of what Claude Code and Codex send, which covers everything this package reads. Both also send extras the other does not — Codex adds model and turn_id, Claude adds prompt_id and effort — and none of them change a verdict, so they are not decoded.

func DecodePayload

func DecodePayload(r io.Reader) (Payload, error)

DecodePayload reads a host's hook payload.

A payload that cannot be parsed is not an error the hook should surface. The host is mid-tool-call and the only useful thing to do is stand aside, so the caller treats a decode failure as Silent.

func (Payload) Command

func (p Payload) Command() string

Command returns the shell command a Bash tool call would run, or empty when this payload is not a shell call.

func (Payload) EditTarget

func (p Payload) EditTarget() string

EditTarget returns the path a file-writing tool call would touch, or empty.

func (Payload) UserPrompt

func (p Payload) UserPrompt() string

UserPrompt returns the submitted prompt, whichever key the host used.

type Policy

type Policy struct {
	// SafeHarbourStrategy picks which safe version counts as the target.
	//
	// Defaults to safest rather than the CLI's stable, matching the editor. An
	// agent adding a dependency on its own initiative gets less deliberation
	// than a version someone typed, so the default prefers the smallest risk
	// over the smallest change.
	SafeHarbourStrategy fix.Strategy `yaml:"safeHarbourStrategy"`

	// MaxMajorBump caps how far a recommendation may travel. Zero refuses major
	// versions, which are almost always a code change rather than a version
	// change.
	MaxMajorBump int `yaml:"maxMajorBump"`

	// CooldownDays is the age below which a version is considered too new to
	// trust. Zero disables the check.
	CooldownDays int `yaml:"cooldownDays"`

	DependencyGuard GuardPolicy `yaml:"dependencyGuard"`
	ChangeGuard     GuardPolicy `yaml:"changeGuard"`

	// Enabled turns the whole surface off without uninstalling it.
	Enabled *bool `yaml:"enabled"`
}

Policy is the whole agent-surface configuration for one repository.

func DefaultPolicy

func DefaultPolicy() Policy

DefaultPolicy blocks only what is never the right call and warns about the rest.

The split is the design: proceeding with a known-malicious package or a critical advisory that is being exploited in the wild is not a trade-off anyone makes deliberately, so those stop. Everything else is information the agent can weigh, and interrupting for it would train people to ignore the interruption.

func LoadPolicy

func LoadPolicy(root string) (Policy, error)

LoadPolicy reads the repository's policy, falling back to the default.

A missing file is the normal case and yields the default. A malformed file is reported, because silently guarding on something other than what someone wrote is worse than saying the file is broken.

func (Policy) Active

func (p Policy) Active() bool

Active reports whether the surface is switched on.

type Response

type Response struct {
	Event    Event
	Decision Decision
	// Message is the text the model receives, as context on Inform and as the
	// refusal reason on Block.
	Message string
}

Response is the hook's answer, before it is encoded for a host.

func EvaluateDependency

func EvaluateDependency(p Policy, assessments []Assessment) Response

EvaluateDependency decides what, if anything, to say about a set of packages an agent is about to add.

The rule the whole guard turns on: when every package satisfies the repository's own Safe Harbour policy, this returns Silent and the agent is not interrupted. A guard that comments on every install is one people switch off, and then it is not a guard.

func (Response) Encode

func (r Response) Encode(w io.Writer) error

Encode writes the response in the form both Claude Code and Codex accept.

A Silent decision writes nothing. An empty stdout is the documented way to say "no opinion", and it is what keeps the guard liveable: a hook that prints on every install is one people turn off.

type Runner

type Runner struct {
	Policy Policy
	Lookup Lookup
	// Root is the repository the hook is running in.
	Root string
}

Runner answers one hook invocation.

func (Runner) Run

func (r Runner) Run(ctx context.Context, p Payload) Response

Run routes a payload to whatever has something to say about it.

Every path that cannot reach an answer returns Silent. A hook sits between an agent and the tool it asked to run: an error here would interrupt work over a problem the user did not cause and cannot act on.

type Signal

type Signal string

Signal is one reason a guard might speak up. Naming them lets a repository tune the guard without the guard growing a flag per condition.

const (
	// SignalMalware is a package a malware feed has named. A version bump is
	// not a fix for it, so it is the one signal that blocks by default.
	SignalMalware Signal = "malware"
	// SignalKEVCritical is a critical advisory that is also on a known-exploited
	// list. Proceeding is never the right call without a decision being made.
	SignalKEVCritical Signal = "kev-critical"
	// SignalMalwareUnresolved is a malware flag against a package's name rather
	// than against a release: the command named no version, and the one it
	// would install could not be resolved. That "some version was malicious" is
	// true of express, so this informs rather than blocks by default.
	SignalMalwareUnresolved Signal = "malware-unresolved"
	// SignalSeverityHigh is a high-or-worse advisory with no exploitation
	// evidence.
	SignalSeverityHigh Signal = "severity-high"
	// SignalBelowTarget means a safer version exists under the configured
	// strategy. This is the ordinary case and only ever warns.
	SignalBelowTarget Signal = "below-target"
	// SignalEOL is a package past its end of life, where no fix is coming.
	SignalEOL Signal = "eol"
	// SignalUnpinned is a direct dependency added with a range rather than an
	// exact version.
	SignalUnpinned Signal = "unpinned"
	// SignalCooldown is a version published very recently, before the ecosystem
	// has had a chance to notice anything wrong with it.
	SignalCooldown Signal = "cooldown"
	// SignalSecret is a credential in a change about to be recorded.
	SignalSecret Signal = "secret"
)

type Surface

type Surface string

Surface is one way a host can be wired to Vulnetix.

const (
	// SurfaceSkills means the host reads Agent Skills from a directory.
	SurfaceSkills Surface = "skills"
	// SurfaceHooks means the host runs lifecycle hooks, so the dependency
	// guard can answer at the moment of a tool call.
	SurfaceHooks Surface = "hooks"
	// SurfaceMCP means the host speaks the Model Context Protocol and can be
	// pointed at mcp.vulnetix.com.
	SurfaceMCP Surface = "mcp"
)

type VDBLookup

type VDBLookup struct {
	Root       string
	CLIVersion string
	// contains filtered or unexported fields
}

VDBLookup is the real lookup, against cli.sca.

func NewVDBLookup

func NewVDBLookup(root, cliVersion string) *VDBLookup

NewVDBLookup prepares a lookup rooted at a repository.

func (*VDBLookup) Assess

func (l *VDBLookup) Assess(ctx context.Context, cands []Candidate) []Assessment

Assess looks up every candidate in one request.

Every failure path returns Unknown rather than an error. The caller is a hook standing between an agent and its tool call: there is nothing useful to do with an error, and reporting one would be claiming something about a package that was never checked.

func (*VDBLookup) AuthState

func (l *VDBLookup) AuthState() Auth

AuthState reports the credential the lookup resolved.

func (*VDBLookup) Warm added in v3.104.0

func (l *VDBLookup) Warm()

Warm resolves the credential without asking a question.

AuthState is only meaningful once a client has been built, and building one happens lazily inside Assess. A caller that wants the credential state and nothing else — capability detection, an installer reporting what it wired — would otherwise have to make a pointless query to find out.

Jump to

Keyboard shortcuts

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