Help us shape the future of Go! Take the 2026 Go Developer Survey

registry

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package registry holds the one contract every mutating tool must satisfy before an agent is ever allowed to call it: a declared reversibility class and an allowlist argument schema. A tool with neither does not load - this is deliberate, not an oversight; see ARCHITECTURE.md.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RequireReviewed

func RequireReviewed(r *Registry) error

RequireReviewed is the strict-catalog gate: it returns an error naming every tool in r that has no reviewed_by/reviewed_at, or whose reviewed_at doesn't parse as a real date. cmd/rinwall calls this right after loading a catalog, only when require_reviewed_tools is set in config.yaml - never in zero-config mode, which has no config file to set it in and whose entire premise (scan and start with no human in the loop) is structurally incompatible with this gate anyway.

This exists because classification accuracy was never the risk this closes - blastradius's own hand-validation found 82-100% precision by class, genuinely good for a heuristic. The risk is procedural: nothing stops `blastradius --mcp <url> --emit-registry > tools.yaml` from being piped straight into a running production proxy with zero human judgment applied to the result. Same philosophy as the loopback-bind refusal in zero-config mode - make the unsafe path structurally impossible rather than warn about it and hope someone reads the warning.

Types

type FieldSpec

type FieldSpec struct {
	Type      string   `yaml:"type"` // "string" | "number" | "bool"
	Required  bool     `yaml:"required"`
	Enum      []string `yaml:"enum,omitempty"`
	Pattern   string   `yaml:"pattern,omitempty"`
	MaxLength int      `yaml:"max_length,omitempty"`
	MaxValue  *float64 `yaml:"max_value,omitempty"` // numeric ceiling, e.g. caps a result-count/limit argument directly in the schema
}

FieldSpec is a deliberately small, allowlist-only schema - not full JSON Schema. Type, required, enum, pattern and max length cover the shipped tools; a proper JSON Schema library can replace this without touching callers if a tool ever needs more.

type PreImage

type PreImage struct {
	Method  string `yaml:"method,omitempty"`
	Path    string `yaml:"path,omitempty"`
	MCPTool string `yaml:"mcp_tool,omitempty"`
}

PreImage says how to read a Class A resource's prior state before it's mutated, so compensation has something to restore from. Two shapes, one per upstream kind: Method/Path for an HTTP upstream, MCPTool for an MCP one (called with a single argument, the tool's ResourceArg, set to the resource being mutated). Exactly one shape is set - see validateToolDefinition.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

func FromTools

func FromTools(tools []Tool) (*Registry, error)

FromTools builds a registry from tools assembled in memory rather than read from a file - what zero-config mode needs after scanning a live MCP server. Every tool goes through the exact same validateToolDefinition as a YAML catalog would: a discovered tool is held to the identical contract as a hand-written one, so auto-discovery can never introduce a tool shape a file could not.

func LoadFromYAML

func LoadFromYAML(path string) (*Registry, error)

func (*Registry) All

func (r *Registry) All() map[string]Tool

func (*Registry) Get

func (r *Registry) Get(name string) (Tool, bool)

type ReversibilityClass

type ReversibilityClass string
const (
	ClassA ReversibilityClass = "A" // compensable
	ClassB ReversibilityClass = "B" // idempotent / retry-safe
	ClassC ReversibilityClass = "C" // irreversible
)

type Tool

type Tool struct {
	Name string `yaml:"name"`
	// Description is what an agent sees in tools/list when this proxy
	// fronts an MCP server - carried through from the upstream's own
	// description. Without it an agent behind the proxy can't choose tools.
	Description        string               `yaml:"description,omitempty"`
	ReversibilityClass ReversibilityClass   `yaml:"reversibility_class"`
	Method             string               `yaml:"method"`
	Path               string               `yaml:"path"`
	ResourceArg        string               `yaml:"resource_arg"`
	ArgsSchema         map[string]FieldSpec `yaml:"args_schema"`
	SanitizeFields     []string             `yaml:"sanitize_fields,omitempty"`   // dot-paths stripped from the upstream response
	CompensateWith     string               `yaml:"compensate_with,omitempty"`   // Class A - empty means the tool re-issues itself
	CompensateFields   []string             `yaml:"compensate_fields,omitempty"` // Class A - arg names restored from the pre-image on compensation
	PreImage           *PreImage            `yaml:"pre_image,omitempty"`         // Class A
	Deferrable         bool                 `yaml:"deferrable,omitempty"`        // Class C tier 2
	HoldSeconds        int                  `yaml:"hold_seconds,omitempty"`      // Class C tier 2
	ReviewerRole       string               `yaml:"reviewer_role,omitempty"`     // Class C tier 3

	// ReviewedBy/ReviewedAt are purely informational in normal operation -
	// nothing in validateToolDefinition checks them, and a catalog missing
	// both loads exactly as it always has. They only become load-bearing
	// when a deployment opts into require_reviewed_tools (cmd/rinwall's
	// Config, checked via RequireReviewed below): the structural gate
	// against the one realistic way to end up running an unreviewed
	// catalog in production - piping `blastradius --emit-registry`
	// straight into service without a human ever looking at it. See
	// RequireReviewed's own doc comment for the full reasoning.
	ReviewedBy string `yaml:"reviewed_by,omitempty"`
	ReviewedAt string `yaml:"reviewed_at,omitempty"` // RFC3339 or YYYY-MM-DD

	// ProvenanceJudge opts this specific tool into the model-based
	// provenance add-on (internal/provenance) - off by default. An org
	// enables it per sensitive tool (close_account) while leaving
	// low-stakes ones on the cheaper self-reported/no-taint path. Only
	// takes effect when the caller also supplies a context window with
	// the call; the tool setting alone does nothing without that.
	ProvenanceJudge bool `yaml:"provenance_judge,omitempty"`

	// VisualProvenanceJudge opts this tool into the screenshot-reasoning
	// analog of ProvenanceJudge (internal/visualprovenance) - for the
	// "computer use" tool shape (one generic tool, an action
	// discriminator, no semantic tool name to classify) rather than the
	// text judge's confused-deputy check. Deliberately triggered
	// INDEPENDENT of ReversibilityClass in internal/handler, not folded
	// into the existing A/C-only taint gate - that gate would silently
	// never reach a Class-B-registered "computer" tool, which is the
	// realistic outcome for a tool whose danger varies per call rather
	// than by name. Requires the caller to supply a screenshot AND their
	// own stated reasoning for the action alongside the call; absent
	// either, this setting does nothing, same as ProvenanceJudge without
	// a context window.
	VisualProvenanceJudge bool `yaml:"visual_provenance_judge,omitempty"`

	// VisualJudgeActionArg/VisualJudgeTriggerValues are the cost filter: a
	// real multimodal model call on every scroll and screenshot is a cost
	// nobody wants, and most of a computer-use tool's calls are exactly
	// that - inert. VisualJudgeActionArg names which argument holds an
	// action-type discriminator (e.g. "action" on Anthropic's own
	// computer-use tool shape); the visual judge only runs when that
	// argument's value is one of VisualJudgeTriggerValues (e.g.
	// left_click, type) - not on screenshot/scroll/arrow-key calls, which
	// can't meaningfully be "manufactured by deceptive content" the way a
	// click or a typed value can. Leaving VisualJudgeActionArg empty means
	// no filter - every call to a VisualProvenanceJudge tool triggers the
	// check, the honest (costlier) default for a tool with no action-type
	// convention to filter on at all.
	VisualJudgeActionArg     string   `yaml:"visual_judge_action_arg,omitempty"`
	VisualJudgeTriggerValues []string `yaml:"visual_judge_trigger_values,omitempty"`

	// DBRole/Query/QueryArgs make this tool database-backed instead of
	// HTTP-backed - the real Vault dynamic-secrets path (a genuine
	// database/creds/<DBRole> mint, executing a real CREATE ROLE against
	// the actual database) rather than a biscuit token verified by an
	// HTTP upstream. DBRole names a Vault database secrets engine role;
	// Query is a parameterized SQL statement ($1, $2, ...); QueryArgs
	// names which validated args_schema fields fill those placeholders,
	// in order. Mutually exclusive with Method/Path - see
	// validateToolDefinition. Restricted to Class B for now: a raw SQL
	// mutation's compensation semantics (what's the "pre-image" of an
	// UPDATE with an arbitrary WHERE clause?) aren't designed yet.
	DBRole    string   `yaml:"db_role,omitempty"`
	Query     string   `yaml:"query,omitempty"`
	QueryArgs []string `yaml:"query_args,omitempty"`

	// AWSRole/S3Bucket/S3KeyArg make this tool AWS-backed instead of
	// HTTP- or DB-backed - the real Vault AWS dynamic-secrets path (a
	// genuine sts:AssumeRole against the actual AWS account). Unlike
	// DBRole, a fresh credential is minted on EVERY call, not cached per
	// session - a real AssumeRole is cheap and AWS-TTL-enforced, unlike a
	// database CREATE ROLE, so there's no cost reason to reuse one across
	// calls, and every reason not to (shorter-lived exposure per call).
	// AWSRole names a Vault AWS secrets engine role; S3Bucket is the
	// bucket to read from; S3KeyArg names which validated args_schema
	// field supplies the object key. Scoped to a single S3 GetObject for
	// now, matching DBRole's own scope discipline (Class B, read-only).
	AWSRole  string `yaml:"aws_role,omitempty"`
	S3Bucket string `yaml:"aws_s3_bucket,omitempty"`
	S3KeyArg string `yaml:"aws_s3_key_arg,omitempty"`

	// MCPTool makes this tool MCP-backed: the proxy forwards the call
	// onward to an upstream MCP server (internal/mcpclient) as a real
	// tools/call, instead of an HTTP route, a SQL query, or an S3 read.
	// This is the shape that lets the firewall sit in front of an MCP
	// server someone already runs, which is the whole adoption story - an
	// MCP tool advertises no upstream HTTP route, so before this existed a
	// scanned catalog could not be loaded without a human hand-writing
	// method/path for every entry.
	//
	// Unlike DBRole and AWSRole, this is NOT restricted to Class B. Those
	// two are read-only scopes this build deliberately hasn't extended to
	// mutations yet; an upstream MCP server's catalog is the general case
	// and routinely contains genuinely irreversible tools - refusing to
	// load them would just push exactly the calls that most need gating
	// back outside the firewall. Class A works too, via PreImage.MCPTool.
	//
	// The name may differ from Tool.Name: the agent-facing name is
	// whatever this catalog calls it, MCPTool is what the upstream server
	// calls it. Empty means "same as Name," which is the normal case for
	// a catalog produced by scanning that server.
	MCPTool string `yaml:"mcp_tool,omitempty"`
}

func (Tool) IsAWSBacked

func (t Tool) IsAWSBacked() bool

func (Tool) IsDBBacked

func (t Tool) IsDBBacked() bool

func (Tool) IsMCPBacked

func (t Tool) IsMCPBacked() bool

func (Tool) UpstreamMCPTool

func (t Tool) UpstreamMCPTool() string

UpstreamMCPTool is the name to call on the upstream MCP server - the explicit mcp_tool if the catalog renamed it, otherwise this tool's own name.

Jump to

Keyboard shortcuts

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