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

agentinstall

package
v0.2.8 Latest Latest
Warning

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

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

Documentation

Overview

Package agentinstall registers Grafo's MCP server with supported MCP clients.

The package is built from three parts: a declarative client registry (registry.go), adapters that either drive a client's own CLI or edit a documented user-level configuration file (adapters.go), and the injected Environment seam (env.go). Detection and dry-run planning only ever receive the read-only half of that seam, so neither can mutate anything.

Index

Constants

View Source
const (
	KindMCP          = "mcp"
	KindSkill        = "skill"
	KindSetupSkill   = "setup-skill"
	KindInstructions = "instructions"
	KindHooks        = "hooks"
)

Artifact kinds. Every Action carries one so callers can tell an MCP registration apart from installed guidance.

View Source
const ServiceOwner = "grafo-service"

ServiceOwner labels the background-service artifacts Grafo installs in the shared receipt ledger. Service definitions are recorded beside client registrations so there is exactly one ownership ledger on disk, and one place that can prove Grafo wrote a file before it is rewritten or removed.

Variables

This section is empty.

Functions

func CheckUserConfigRoot

func CheckUserConfigRoot(reader Reader, path string) error

CheckUserConfigRoot refuses paths that escape the user's configuration roots. It is the single containment boundary for everything Grafo writes outside a repository, shared by guidance installation and by service definitions.

func ConfigHomePath

func ConfigHomePath(reader Reader, elements ...string) (string, error)

ConfigHomePath resolves a path inside the XDG-style configuration directory ($XDG_CONFIG_HOME or ~/.config, %APPDATA% on Windows). Callers outside this package use it so the service registry lands in the same configuration root the installer already writes to.

func Digest

func Digest(contents string) string

Digest labels content the way receipts record it.

func ForgetOwnedFile

func ForgetOwnedFile(env Environment, owner, kind string) error

ForgetOwnedFile drops the receipt for an artifact Grafo has just removed, or for one the user deleted, so stale evidence can never authorize a later write.

func HomeDirPath

func HomeDirPath(reader Reader, elements ...string) (string, error)

HomeDirPath resolves a path relative to the user's home directory.

func HookCommand

func HookCommand(goos, executable, phase string) string

HookCommand renders the advisory hook command line for one phase. The executable is shell-quoted for the target platform, because clients run hook commands through a shell and Grafo's absolute path may contain spaces.

func JoinPath

func JoinPath(goos string, elements ...string) string

JoinPath joins path elements with the target platform's separator.

func ParentPath

func ParentPath(goos, path string) string

ParentPath returns the directory containing path, or "" when there is none.

func ProvesContents

func ProvesContents(receipt Receipt, contents string) bool

ProvesContents reports whether a receipt's digest proves Grafo wrote exactly these bytes. It says nothing about location: pair it with OwnedFileAt, which binds a receipt to one resolved path.

func ProvesFile

func ProvesFile(reader Reader, receipt Receipt, path, contents string) bool

ProvesFile reports whether a receipt proves Grafo wrote exactly these bytes at exactly this resolved path.

func RecordOwnedFile

func RecordOwnedFile(env Environment, owner, kind, target, resolvedTarget, contents string) error

RecordOwnedFile records a non-client artifact Grafo has just written. The resolved target must be the identity captured and checked before that write; it is revalidated here so a later parent retarget cannot be recorded as the destination of an earlier mutation.

func ResolvePath

func ResolvePath(reader Reader, path string) (string, error)

ResolvePath resolves every symlinked parent of path and returns the real location, leaving the final element unresolved.

func ResolveUserConfigPath

func ResolveUserConfigPath(reader Reader, path string) (string, error)

ResolveUserConfigPath returns the physical direct-write target after proving it remains inside the configured user roots. The leaf stays unresolved.

func RevalidateUserConfigPath

func RevalidateUserConfigPath(reader Reader, path, expected string) error

RevalidateUserConfigPath checks that a direct-write target still resolves to the physical path captured during planning and that its leaf is not a symlink.

Types

type Action

type Action struct {
	Client Client `json:"client"`
	Kind   string `json:"kind"` // KindMCP, KindSkill, KindSetupSkill, KindInstructions, KindHooks
	Scope  string `json:"scope"`
	Target string `json:"target"` // config path, artifact path, or client executable
	Change string `json:"change"` // "installed", "updated", "removed", "unchanged", "skipped"
	DryRun bool   `json:"dry_run,omitempty"`
	Detail string `json:"detail,omitempty"`
}

Action is what Install/Uninstall did (or, in dry-run, would do) for one artifact of one client.

func Install

func Install(ctx context.Context, env Environment, executable string, options Options) ([]Action, error)

Install registers executable as the Grafo MCP server for the selected clients and installs Grafo's guidance surfaces unless Options.MCPOnly is set. With no explicit targets (or Options.All) every supported client is considered and genuinely uninstalled ones are reported as skipped.

func Uninstall

func Uninstall(ctx context.Context, env Environment, options Options) ([]Action, error)

Uninstall removes only Grafo's own registration, guidance, and hooks from the selected clients. Anything whose ownership cannot be proven is left in place and reported as skipped.

type Client

type Client struct {
	Name    string `json:"name"`    // stable lowercase identity, e.g. "claude"
	Display string `json:"display"` // e.g. "Claude Code"
	Scope   string `json:"scope"`   // e.g. "user"
	Method  string `json:"method"`  // "cli" or "config"
}

Client is one supported MCP client.

func Clients

func Clients() []Client

Clients returns every supported client in deterministic order.

type Environment

type Environment interface {
	Reader
	Writer
}

Environment bundles every outside-world operation the installer needs.

type ExecRunner

type ExecRunner struct{}

ExecRunner configures clients through their installed command-line tools.

func (ExecRunner) LookPath

func (ExecRunner) LookPath(file string) (string, error)

func (ExecRunner) Output

func (ExecRunner) Output(ctx context.Context, name string, arguments ...string) ([]byte, error)

func (ExecRunner) Run

func (ExecRunner) Run(ctx context.Context, name string, arguments ...string) ([]byte, error)

type OSEnvironment

type OSEnvironment struct {
	ExecRunner
}

OSEnvironment is the production Environment: real processes, real files.

func NewOSEnvironment

func NewOSEnvironment() OSEnvironment

NewOSEnvironment returns the Environment backed by the host operating system.

func (OSEnvironment) GOOS

func (OSEnvironment) GOOS() string

func (OSEnvironment) Getenv

func (OSEnvironment) Getenv(key string) string

func (OSEnvironment) HomeDir

func (OSEnvironment) HomeDir() (string, error)

func (OSEnvironment) Lstat

func (OSEnvironment) Lstat(name string) (fs.FileInfo, error)

func (OSEnvironment) MkdirAll

func (OSEnvironment) MkdirAll(path string, perm fs.FileMode) error

func (OSEnvironment) ReadFile

func (OSEnvironment) ReadFile(name string) ([]byte, error)
func (OSEnvironment) Readlink(name string) (string, error)

func (OSEnvironment) Remove

func (OSEnvironment) Remove(path string) error

func (OSEnvironment) Stat

func (OSEnvironment) Stat(name string) (fs.FileInfo, error)

func (OSEnvironment) TempDir

func (OSEnvironment) TempDir() string

func (OSEnvironment) WriteFileAtomic

func (OSEnvironment) WriteFileAtomic(path string, data []byte, perm fs.FileMode) error

WriteFileAtomic writes to a temporary file in path's directory and renames it over path, so a failure leaves the original configuration untouched.

type Options

type Options struct {
	Targets []string // explicit client names; empty or ["all"] means every supported client
	All     bool
	DryRun  bool
	// MCPOnly installs or removes only the MCP registration, never guidance.
	MCPOnly bool
	// Hooks opts in to the advisory, fail-open hooks of clients that document a
	// safe hook API. Uninstall always removes Grafo's hooks regardless.
	Hooks bool
	// Refresh updates only artifacts that are already present, so it never adds
	// a registration, skill, block, or hook that the user has not installed.
	Refresh bool
	// Announce receives the full planned action list before any mutation runs.
	Announce func(planned []Action)
	// contains filtered or unexported fields
}

Options selects clients, selects artifact kinds, and controls mutation.

type Reader

type Reader interface {
	// LookPath resolves an executable on PATH.
	LookPath(file string) (string, error)
	// Output runs a read-only informational command (`--help`, `mcp list`).
	Output(ctx context.Context, name string, arguments ...string) ([]byte, error)
	// ReadFile reads a configuration file.
	ReadFile(name string) ([]byte, error)
	// Stat reports whether a path exists, following symlinks.
	Stat(name string) (fs.FileInfo, error)
	// Lstat reports on a path without following symlinks, so the installer can
	// refuse to write through one.
	Lstat(name string) (fs.FileInfo, error)
	// Readlink reads a symbolic link's target, so the installer can resolve a
	// path completely before deciding whether it stays inside the user's
	// configuration tree.
	Readlink(name string) (string, error)
	// GOOS reports the target operating system ("linux", "darwin", "windows").
	GOOS() string
	// HomeDir reports the current user's home directory.
	HomeDir() (string, error)
	// Getenv reads an environment variable.
	Getenv(key string) string
	// TempDir reports the directory used for temporary files.
	TempDir() string
}

Reader is the read-only view of the outside world. Detection and dry-run planning are restricted to this interface so that neither can mutate state: there is simply no write operation reachable from here.

type Receipt

type Receipt struct {
	Client         string `json:"client"`
	Kind           string `json:"kind"`
	Target         string `json:"target"`
	ResolvedTarget string `json:"resolved_target,omitempty"`
	Digest         string `json:"digest,omitempty"`
	// Commands are the exact command lines Grafo installed for a hook artifact,
	// in hookPhases order. Removal matches them exactly, so a user-authored
	// entry that merely resembles one is never claimed.
	Commands []string `json:"commands,omitempty"`
	Marker   string   `json:"marker,omitempty"`
	Guidance string   `json:"guidance_version,omitempty"`
	Grafo    string   `json:"grafo_version"`
	Updated  string   `json:"updated_at"`
}

Receipt records one artifact Grafo installed, so a later uninstall or upgrade can prove ownership instead of guessing from file contents alone.

func OwnedFile

func OwnedFile(reader Reader, owner, kind string) (Receipt, bool, error)

OwnedFile reports the receipt Grafo holds for one non-client artifact, such as a generated service definition. The boolean reports whether a receipt exists at all; a missing receipt is not an error.

func OwnedFileAt

func OwnedFileAt(reader Reader, owner, kind, path string) (Receipt, bool, error)

OwnedFileAt reports the receipt Grafo holds for one non-client artifact, but only when that receipt covers this exact location. Targets are compared after resolving every symlinked parent, so a receipt written for one path can never authorize a mutation at a different path that merely resolves nearby, and a path spelled differently from the recorded target still matches when both name the same file.

Keying ownership to the artifact kind alone is the defect PR #53 fixed for hook receipts; this is the same contract for generated service definitions.

func Receipts

func Receipts(reader Reader) ([]Receipt, error)

Receipts reports the artifacts Grafo has installed, newest content first in deterministic client and kind order. It never mutates anything.

type Runner

type Runner interface {
	LookPath(file string) (string, error)
	Output(ctx context.Context, name string, arguments ...string) ([]byte, error)
	Run(ctx context.Context, name string, arguments ...string) ([]byte, error)
}

Runner provides the process operations needed to configure a client. It is retained for compatibility with callers written against the original process-only seam; Environment embeds the same three operations.

type Status

type Status struct {
	Client     Client `json:"client"`
	Installed  bool   `json:"installed"`      // the client itself was found
	Path       string `json:"path,omitempty"` // client executable or config file
	Registered bool   `json:"registered"`     // grafo is already registered
	Detail     string `json:"detail,omitempty"`
}

Status is one row of detection output. Detect never mutates anything.

func Detect

func Detect(ctx context.Context, env Environment, targets []string) ([]Status, error)

Detect reports what is installed and whether Grafo is already registered. It uses only the read-only half of the environment and writes nothing.

type Writer

type Writer interface {
	// Run executes a client command that changes configuration.
	Run(ctx context.Context, name string, arguments ...string) ([]byte, error)
	// MkdirAll creates a configuration directory and its parents.
	MkdirAll(path string, perm fs.FileMode) error
	// WriteFileAtomic replaces path's contents by writing a temporary file in
	// the same directory and renaming it over path.
	WriteFileAtomic(path string, data []byte, perm fs.FileMode) error
	// Remove deletes one file or empty directory Grafo owns.
	Remove(path string) error
}

Writer holds the mutating operations. Only Install and Uninstall outside of dry-run mode ever receive a value that satisfies it.

Jump to

Keyboard shortcuts

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