Documentation
¶
Overview ¶
Package agent triages the reach of an agentic system's tool chain.
geiger's other modules answer "what does this credential unlock". That is the wrong unit for an agent: a config whose every secret is stored correctly — OS env, a keychain, an OAuth flow — can still wire the agent to a filesystem server rooted at /, a shell server, and the corporate wiki. The credential hygiene is irrelevant to the blast radius; the TOOL CHAIN is the unit.
The package types each configured server into a set of reach primitives (offline, from the catalog and from argv/env heuristics), optionally confirms them by enumerating the server's real tool list read-only, and reports the chains that are worth more than the sum of their parts — the lethal trifecta, bulk read plus a way out, an exec tool, a secret store.
That primitive list is inventory and does not set a tier on its own: a config file cannot say whether the reach it describes is appropriate for the machine it sits on. Severity comes from the chains. See the note at the top of findings.go.
See docs/design/agentic-reach.md.
Index ¶
- Variables
- func EnumerateRemote(ctx context.Context, c *recon.Client, s *Server)
- func EnumerateStdio(ctx context.Context, s *Server, o SpawnOptions)
- func ServerMaps(root map[string]any) map[string]any
- func ToolNames(tools []Tool) []string
- func Type(s *Server)
- type Cap
- type Capability
- type Caps
- type Chain
- type Hook
- type Runtime
- type Server
- type Set
- type SpawnOptions
- type Surface
- type Tool
- type Transport
Constants ¶
This section is empty.
Variables ¶
var ErrSpawnNotPermitted = errors.New("stdio enumeration requires --live --spawn-stdio")
ErrSpawnNotPermitted is returned when stdio enumeration was not authorized.
Functions ¶
func EnumerateRemote ¶
EnumerateRemote lists a remote server's tools read-only. It tries the stateless 2026 shape first and falls back to the 2025 handshake, so a server on either side of the break is enumerated by the same call.
func EnumerateStdio ¶
func EnumerateStdio(ctx context.Context, s *Server, o SpawnOptions)
EnumerateStdio starts a local stdio server, asks it for its tool list, and tears it down. It is a no-op unless both gates are open.
func ServerMaps ¶
ServerMaps returns the server map from any known config layout. Clients agree on the per-server shape and disagree on where the map lives, so this is the only place the layouts differ.
Types ¶
type Cap ¶
type Cap uint32
Cap is one reach primitive. A tool, a server, and a whole agent surface are each described by a set of these.
const ( // CapExec runs code on the host: shell, python eval, docker, k8s exec, a CI // trigger. The strongest primitive there is — it closes over every other // credential on the box. CapExec Cap = 1 << iota // CapCorpusSearch searches or bulk-reads a whole document corpus: wiki, // ticketing, chat, drive, mail, code search, a vector index. Separated from // CapDataRead because it needs no chaining to be an incident: "give me every // credential in Confluence" is one tool call, and it is the highest-yield // move in a real engagement. CapCorpusSearch // CapSecretsRead reads a secret store, environment, or keychain — the edge // from the tool chain into every credential geiger already triages. CapSecretsRead // CapCodeWrite pushes code, opens PRs, or publishes packages (supply chain). CapCodeWrite // CapCloudControl reaches a cloud control plane. CapCloudControl // CapDestructive deletes, wipes, or terminates. CapDestructive // CapIdentityAdmin writes to an IdP or directory. CapIdentityAdmin // CapFSRead / CapFSWrite touch the local filesystem. Their weight depends // entirely on the root scope, which is why Capability carries one. CapFSRead CapFSWrite // CapDataRead reads scoped private data: a record, a bounded table. The // non-bulk sibling of CapCorpusSearch. CapDataRead // CapNetEgress sends data outward: fetch with a caller-controlled URL, a // webhook, mail, a chat post. The exfiltration half of every data chain. CapNetEgress // CapUntrustedIn ingests attacker-influenceable content: the web, GitHub // issues, mail, tickets. The prompt-injection entry point. CapUntrustedIn )
func (Cap) Flag ¶
Flag says how a capability line is reported.
Inventory carries no weight, so the answer is normally FlagNone. The one exception is filesystem reach at a broad root: a server rooted at / or $HOME and one rooted at ./project are the same package two orders of magnitude apart, and the config says which it is.
type Capability ¶
type Capability struct {
Cap Cap
// Scope bounds the primitive where one is knowable — a filesystem root, a
// database name, a corpus host. Empty means unbounded or unknown.
Scope string
// Broad marks a scope that is effectively unbounded (a filesystem server
// rooted at / or at $HOME, a wildcard host).
Broad bool
// Evidence is what typed it: a catalog package name, an argv pattern, or an
// enumerated tool name.
Evidence string
}
Capability is one primitive as found on a specific server, with the scope it applies at and the evidence for it.
func (Capability) String ¶
func (c Capability) String() string
String renders the capability for a finding line.
type Caps ¶
type Caps []Capability
Caps is an ordered, deduplicated list of capabilities.
func ClassifyTools ¶
ClassifyTools types an enumerated tool list. It returns the capability set and the number of tools no rule matched, so the note can state its own coverage rather than implying the list was fully understood.
func (Caps) Add ¶
func (cs Caps) Add(c Capability) Caps
Add appends a capability, merging into an existing entry for the same primitive rather than repeating it. The broader scope wins, since that is the reach an operator has to assume.
type Chain ¶
type Chain struct {
Name string
// Why states the composition in the operator's terms.
Why string
// Via names the servers that supply each leg, so the reader can act on it.
Via []string
// Flag is the finding's significance.
Flag module.FlagLevel
}
Chain is one composition found on a surface.
type Server ¶
type Server struct {
Name string
Transport Transport
Command string
Args []string
URL string
// EnvNames are the env var names the server config sets, values omitted —
// the names alone say which credentials the server is handed.
EnvNames []string
// HeaderNames are the auth header names on a remote server.
HeaderNames []string
// Caps is the union of statically-typed and enumerated reach.
Caps Caps
// Label is the catalog's human name for the server, when it matched.
Label string
// Unpinned marks a launcher that refetches the package at every start
// (npx -y, uvx, pipx run without a version). The capability set has no shelf
// life: the code that runs tomorrow is not the code typed today.
Unpinned bool
// PlaintextHTTP marks a remote server reached over http:// — the token in
// its headers crosses the wire in clear.
PlaintextHTTP bool
// AutoApproved lists the tools (or "*") this server is pre-approved for by
// the runtime's configuration, so no human sees the call.
AutoApproved []string
// Enumerated is set once a real tool list was observed. Until then every
// capability above is geiger's claim about a package name, not a fact.
Enumerated bool
// Tools are the enumerated tool names.
Tools []string
// ResourceCount / PromptCount are the enumerated resource and prompt counts.
ResourceCount int
PromptCount int
// Asked records that geiger actually sent the server a request. It separates
// "we tried and it did not answer" from "we never tried", which are
// different answers to the reader's question and need different advice.
Asked bool
// EnumErr explains why enumeration did not happen or did not succeed.
EnumErr string
// Unauthenticated records that the remote server answered tools/list with no
// credential at all — its whole tool surface is available to anyone who can
// route to it.
Unauthenticated bool
// AuthServer is the authorization server named by a 401's protected-resource
// metadata (RFC 9728), when the server gates access.
AuthServer string
}
Server is one configured MCP server, before or after enumeration.
func (Server) CredentialEnvNames ¶
CredentialEnvNames returns the env var names that hand this server a credential. Used to say, in the note, which of geiger's other findings the server inherits.
func (Server) Invocation ¶
Invocation is the lowercased string the catalog matches against: the command and arguments for a stdio server, the host and path for a remote one.
func (Server) OpenData ¶
OpenData reports the stronger case: the server served CONTENT, not just the names of its tools, to the same credential-free request.
The distinction decides the weight. A hosted MCP endpoint commonly publishes its tool list on purpose and gates the calls, and geiger cannot tell the difference — it never issues tools/call, which is the line between enumerating reach and exercising it. So a public tool list is a warn: it says what the server offers, not that anyone can use it. Resources are the other case. They are the data itself, and they came back to a caller with no credential at all.
func (Server) OpenSurface ¶
OpenSurface reports an unauthenticated tool surface worth reporting: the server answered geiger's credential-free request, and it is not on this machine.
Loopback stands the finding down for the same reason cleartext to loopback does. A server on 127.0.0.1 is the normal way to run one, "anyone who can route to it" there is a process already on the host, and reporting it would put every developer laptop at the top of the scale.
type Set ¶
type Set uint32
Set is a union of primitives.
type SpawnOptions ¶
type SpawnOptions struct {
// Permitted is the --spawn-stdio gate. False means no process is started.
Permitted bool
// Live mirrors --live; both must hold.
Live bool
// Timeout overrides spawnTimeout (tests).
Timeout time.Duration
// Record receives the argv about to be executed, for the audit trail. It is
// called BEFORE the process starts so a killed or hanging spawn is still
// attributable.
Record func(argv string)
}
SpawnOptions controls local stdio enumeration.
type Surface ¶
type Surface struct {
Runtime Runtime
File string
Servers []Server
// SkipPermissions records a runtime-wide bypass of the approval prompt
// (Claude Code's dangerouslySkipPermissions, Cline/Roo YOLO mode). It is the
// single most consequential setting in an agent config: it removes the human
// from every chain at once.
SkipPermissions bool
// AllowRules are the runtime's blanket pre-approvals (Claude Code's
// permissions.allow), which auto-approve matching tool calls.
AllowRules []string
// Hooks are shell commands the runtime runs on lifecycle events. They
// execute with NO model in the loop at all, which is strictly more reach
// than any MCP tool on the same surface.
Hooks []Hook
// Skills and Subagents are instruction sets the agent loads and follows.
Skills []string
Subagents []string
// Live and SpawnStdio record which enumeration the run was allowed to do,
// so the evidence line names a flag the reader has not already passed.
Live bool
SpawnStdio bool
}
Surface is a parsed agent configuration.
func ParseSurface ¶
ParseSurface builds a Surface from a decoded agent config. It is layout-aware but transport-agnostic: nothing here runs or dials anything.
func (Surface) AutoApproved ¶
AutoApproved reports whether the surface's calls can proceed without a human: a runtime-wide bypass, a blanket allow rule, or per-server pre-approval.
func (Surface) Caps ¶
Caps is the union of every server's reach — what the agent can touch through its tool chain as a whole.
func (Surface) Enumerated ¶
Enumerated reports whether any server's real tool list was observed. Until one was, every capability on the surface is geiger's reading of a package name, which is what the note's evidence line says.
func (Surface) Summarize ¶
Summarize builds the note and applies the Undetermined rule.
A config is not a credential. For a credential, Undetermined means geiger could not establish the thing is even live, so putting a tier on it would invent a severity. Here the config file is the observation: "server-filesystem /" in the arguments is read off disk, and what it grants is not in doubt. Only the exact tool list is, and that changes precision, not the class of reach.
Withholding a tier until enumeration would also make the common case useless. Most servers are local, and enumerating those needs --spawn-stdio, which runs third-party code and often cannot be run at all — so the default mode, the one people actually use, would never report anything.
Undetermined is kept for the case it was meant for: servers are configured but nothing about their reach could be established.
func (Surface) Untypeable ¶
Untypeable reports that servers are configured but nothing is known about what any of them exposes.
type Transport ¶
type Transport string
Transport is how the client reaches a server.
const ( // TransportStdio launches a local process. Enumerating it means EXECUTING // the configured command, which is why it is gated behind --spawn-stdio. TransportStdio Transport = "stdio" // TransportHTTP is a remote server over streamable HTTP (or the deprecated // HTTP+SSE). Enumerating it is an ordinary read-only HTTP call. TransportHTTP Transport = "http" )