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
- Variables
- func CapabilitiesFresh(root string) bool
- func CapabilitySummary(c Capabilities) string
- func ExpandHome(p string) string
- func HookCommand() string
- func SortedHostIDs() []string
- func WriteCapabilities(root string, c Capabilities) (string, error)
- type Assessment
- type Auth
- type BashInput
- type Candidate
- type Capabilities
- type CapabilityDerived
- type Decision
- type EditInput
- type Event
- type GuardPolicy
- type HookDialect
- type Host
- type InstallOptions
- type InstallResult
- type Lookup
- type Payload
- type Policy
- type Response
- type Runner
- type Signal
- type Surface
- type VDBLookup
Constants ¶
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.
const PolicyFile = ".vulnetix/agent.yaml"
PolicyFile is where a repository states what its agents may do without being interrupted.
Variables ¶
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
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 ¶
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
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 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 ¶
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 ¶
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.
type GuardPolicy ¶
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.
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 ¶
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 ¶
Command returns the shell command a Bash tool call would run, or empty when this payload is not a shell call.
func (Payload) EditTarget ¶
EditTarget returns the path a file-writing tool call would touch, or empty.
func (Payload) UserPrompt ¶
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 ¶
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.
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.
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.
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" // 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 ¶
VDBLookup is the real lookup, against cli.sca.
func NewVDBLookup ¶
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) 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.