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

garm

module
v0.29.0 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: MIT

README

garm

The command line tool.

garm is what a person types. It scaffolds a proto tree, compiles governed tool declarations, refuses the bad ones, and builds the catalogue a runtime will serve. It runs for half a second and exits.

It is not the server. That is garmd.

It is not the contract either, any more. The annotations, the vocabularies and the plan compiler are github.com/garm-ai/contracts, a module this one depends on — see The contract is a dependency.

What you type

garm init                scaffold a proto tree, annotations vendored in
garm gen                 run the code generators (a thin wrapper on `buf generate`)
garm lint                check the catalogue.yaml inputs against the L, A, C, O and P rules
garm catalogue init      write the catalogue.yaml that describes this tree
garm catalogue build     compose the catalogue.yaml inputs into the artifact garmd loads
garm catalogue publish   put it, and the prompts it pins, on an object store
garm catalogue diff      what changed between two catalogues, in policy terms
                         (including tool sets, which decide reachability)
garm claims check        assert a claims policy only names vocabulary a catalogue declares
garm plugin              the protoc plugin, so a buf.gen.yaml can name this binary
garm version             what you are running

garm init writes a proto tree, a catalogue.yaml and the four annotation files into third_party/proto/, so an author imports "garm/tool/v1/tool.proto" by its real path without reaching a registry. garm gen then shells out to buf, which is a runtime dependency of this tool: garm drives buf over your protos, and buf invokes this binary back as a local plugin.

Upgrading across v0.19.0 renames the generated card helpers: the default is now Default<Service><Card> and the result builder <Service><Card>From, where a single-tool service or an agent used to get the bare DefaultInputCard and ResultCardFrom. A Go identifier is package level where an RPC name is scoped by its service, so five agent services in one proto package produced five DefaultInputCard and the package did not compile. The card's RPC name is unchanged, so an override is unaffected and no catalogue digest moved; regenerate, and rename any direct call to a default. docs/catalogue.md has the rule and why it differs from the RPC name.

What is here

cmd/garm/ The CLI
cmd/protoc-gen-garm-go/ The protoc plugin, under the conventional name, so go install puts it on PATH
internal/catalogue/ Assembly: the lint gate, the descriptor hashes, the synthesised cards, the marshalled artifact. It decides and returns; the command prints and writes
internal/compile/ Proto source to descriptors, in process — not by shelling out to buf, so an author needs no second tool and a catalogue's digest does not depend on whichever buf is on someone's PATH
internal/manifest/ catalogue.yaml: what it may say, which version the module graph resolves each entry to, and which input contributed which proto package
internal/compiler/ The reader, the lint rules, and the emitter
internal/plugin/ The plugin entry point both binaries share
internal/policydiff/ What catalogue diff compares — and, in coverage_test.go, a walk over ToolPolicy's descriptor that refuses a field which is neither compared nor ignored with a reason
conformance/ Golden proto cases and the diagnostics they must produce
docs/catalogue.md What a catalogue is, what it costs, and how it is sized

The binary contains the plugin. One install, not two — and the generator and the rules it enforces cannot drift apart, because they are the same artifact.

The contract is a dependency

The protos are not here. garm.tool.v1, garm.agent.v1, garm.card.v1, garm.meta.v1 and garm.tasks.v1, their generated Go, and the policy package that compiles field annotations into redaction plans all live in garm-ai/contracts. This repository requires that module like any other dependency:

require github.com/garm-ai/contracts v0.11.0

Why they are not here: ten Go modules compile against the contract and exactly none of them run this CLI. While the two shared a repository, every consumer of a message type inherited an S3 client, a CEL evaluator and a CLI framework to get it, and every release of the tool was a release of the contract. They are released separately now, and versioned separately.

What that means in practice:

  • The annotations garm init vendors are read out of the dependency's embed, so the bytes in your third_party/ are the contract's bytes at the version this binary was built against.
  • The descriptors are linked, not vendored: internal/compile blank-imports the contract's generated packages so their descriptors register in protoregistry.GlobalFiles, and the compiler resolves your import "garm/tool/v1/tool.proto" from that registry before it looks on disk. So this binary's copy is authoritative and a stale file in somebody's third_party/ cannot quietly change what an annotation means.
  • Upgrading across v0.18.0 changes the Go import paths you generate against: github.com/garm-ai/garm/contracts/... became github.com/garm-ai/contracts/..., and github.com/garm-ai/garm/policy became github.com/garm-ai/contracts/policy. Re-vendor with garm init --force, then regenerate.

The catalogue

garm catalogue build composes a catalogue from the inputs catalogue.yaml declares, and writes the artifact a daemon loads at boot — so adding a tool is a catalogue rebuild rather than a release of the governance binary. Two builds of identical source produce identical bytes, because the digest is the catalogue's identity.

# catalogue.yaml
schema: v1
name: bank
source: garm-ai/examples/bank      # free-form provenance, never parsed

include:
  - path: proto                    # this deployment's own declarations

  - module: github.com/garm-ai/contracts          # platform tools it operates
    packages: [garm.tasks.v1]
  - module: github.com/garm-ai/tools/web          # a tool package it adopts
    packages: [web.v1]

prompts: .

taxonomy:                          # this deployment's own vocabulary
  compartments:
    - name: financial
      description: Money movement and balances.
  tool_sets:
    - name: payments
      description: Initiating and inspecting payments.

An input is a directory in this tree or proto packages from a Go module the tree requires, and the deployment's own tree is one entry among the others rather than a privileged flag. garm catalogue build, with no arguments, is the whole command: the manifest is found by convention in the working directory.

The vocabulary is the deployment's. A tool requires compartments by name on each method; a deployment declares which words exist, in taxonomy:. Lint then refuses a name no entry declares — so adopting a tool that needs internet is a decision somebody makes rather than a word that appears. Before v0.22.0 the vocabulary was unioned out of file-level proto options across every file in the set, which meant adopting a tool silently extended the vocabulary governing who may see what, and a deployment could not see its own in one place. Declaring the key replaces the scrape entirely, and a tree without it keeps the old behaviour. The words still travel to the daemon on the same two catalogue fields, so this is not a contract change.

The manifest names what; go.mod says which version. Nothing new fetches anything — a tag is a Go module version, go mod download already maps one to the other, and the protos are read out of the module cache, so this works offline once the cache is warm and travels through whatever GOPROXY an enterprise already permits. The consequence is the invariant the file exists for: a module the tree does not require is refused. The generated Go already comes from that requirement, so a second place to pin a version would let a deployment compile against one tag and declare the descriptors of another, which is a descriptor mismatch that presents at run time as an agent that cannot mount. A version: key is permitted for readability and must equal what the module graph resolves to; a disagreement is an error, not a precedence rule.

Three more rules, all so that the artifact means one thing:

  • Two inputs declaring one proto package is refused, naming both. Never a merge and never last-wins — a silent winner is the drift a manifest replaces.
  • A package a module entry names and the module does not declare is refused, naming both.
  • include order is presentation. The inputs are sorted into the order garm.catalogue.v1.Provenance.inputs specifies before they are stamped, and the descriptor set's file order is sorted over every input together, so reordering the file does not move the digest.

The artifact then records what it was composed from: each input's module path and resolved version, or the local path and its source, with the proto packages it contributed. That is what makes a catalogue a bill of materials — "which version of the payment tool's contract is this deployment running" is a question the artifact answers, where before it was knowable only from a commit.

Writing the manifest: garm catalogue init

A tree that predates the manifest already states everything the file says, spread across three places, so garm catalogue init reads it out of them rather than asking anyone to retype it: buf.gen.yaml's input directories become path: entries, its exclude_paths name the adopted copies, each copy's own go_package says which module it came from, and go list resolves that to a requirement. Then the copies can be deleted, along with the exclude_paths and the drift gate that exist only because of them.

It does not guess. Every fact it writes was read from something: which package a copy declares comes from the compiled descriptor and not from the directory name. When a copy's module is one the tree does not require, §2's invariant admits no entry — there is no version to pin it to — so there is no entry, the copy is left exactly as it is, and the report names it and exits non-zero. A tree in that state compiles its declarations against descriptors it does not depend on, which is a defect rather than something to encode. It also refuses to overwrite an existing catalogue.yaml without --force, because a hand-edited manifest is the authority and a generator is not.

Required platform packages, and why lint reads the manifest

A declaration can depend on a capability another package provides, and then the catalogue has to declare that package. Rule P1: a tool declaring approval { mode: MODE_GRANT } requires garm.tasks.v1 in the same catalogue, and the build refuses one that does not have it.

This is a real outage rather than a tidiness rule. On 2026-09-29 tasksd ran all day serving eight tools while payments.v1.initiate_payment declared MODE_GRANT in a catalogue that did not declare garm.tasks.v1. Nothing was wrong with either half: the daemon mounted the catalogue, the service answered its subject, and there was no route between them — so the call parked on a task nobody could open, decide or see, the runner waited out the window and reported that no approval had arrived, which is indistinguishable from a person declining to act. Build time is the only place that is cheap to catch.

Whether the package is present is a question about the assembled catalogue, not about one declaration, which is why garm lint now takes the same inputs catalogue build does — a manifest found by convention, or -f to name one elsewhere. The same is true of A3's agent allowlist, A9's audience, the half of A7 that resolves a workflow step's with keys against the tool's request message, and A11's comparison of a set's state field against the response field it reads: a linter that saw only the deployment's own directory would pass a tree the build then refuses. The protoc plugin, which buf invokes once per directory, cannot answer any of them and says it did not check rather than passing silently. A7's other half — that a state field is read only where every path has written it — needs nothing but the agent's own declaration, so it is an error everywhere, the plugin included.

The flags
-f, --manifest The manifest to compose from. Default: catalogue.yaml in the working directory
-o, --out Where to write the artifact. Default: catalogue.binpb
--source Free-form provenance: a repository and commit, a pipeline id. Overrides the manifest's source:
--prompts-root Where an agent's prompts.*.path resolves against. Default: the manifest's prompts:
--stamp-time Record the build time. Breaks byte-reproducibility, and the help says so

A tree with no catalogue.yaml and no --manifest is refused, naming the directory and saying to run garm catalogue init.

The assembly itself is internal/catalogue, the manifest is internal/manifest, and the command is a thin front on both: catalogue.Build is handed a compiled tree and the resolved inputs, and returns the artifact, its digest and the lint diagnostics. Lint is not a step that front can skip — Build lints before it assembles and refuses on any error, so a catalogue that does not lint cannot be built by anything that calls it.

About 384 bytes per tool on disk and 9.3 KB retained, linear to at least 10,000 tools — mise run bench measures it, and docs/catalogue.md explains what it means for where a catalogue should be sized.

Nothing here depends on the server

That is the property this repository exists to hold. An engineer authoring a tool schema compiles it, lints it and generates from it without running, installing or cloning garmd. Enforcement at authoring time must not require the thing that enforces at run time.

What is deliberately not here

  • The contract. garm-ai/contracts, as above.
  • The governance chain. Declaring that a tool requires clearance is this repository's business; enforcing it is garmd's.
  • Any taxonomy. Compartments are an organisation's data-classification policy. The contract ships the mechanism — that compartments exist, that clearance compares, that a tool declares what it requires — never the vocabulary.
  • Commands that need the product. manifest, agent and tool index depend on packages that live in garmd today. They arrive here when their dependencies do, and not before — dragging the product across to keep a command would defeat the boundary above.

Working here

mise install        the toolchain
mise run test       go test ./... -race
mise run lint       go vet
mise run ci         what CI runs

Two tests read a garm-ai/contracts checkout, because their subject is the contract's source and a module dependency carries none: the platform's own protos against this linter, and real connect-go output against ConnectNames. Clone it beside this repository, or set GARM_CONTRACTS_DIR. They skip without one, and fail rather than skip when CI is set.

Status

Shipped: init, gen, lint, catalogue init|build|diff|publish, claims check, plugin, version. catalogue build composes from catalogue.yaml since v0.20.0; catalogue init writes one, garm lint reads one and the required-platform-package rule is enforced since v0.21.0. What the manifest still does not do — the taxonomy, and publish resolving a composed agent's prompts out of its own module — is in KNOWN-GAPS.md. Thirty-two tool lint rules (the L series), fifteen agent rules (A1–A15 — A12–A15 are the standing-grants consent template, since v0.27.0), three card rules (C1, C8, C9), one ownership rule (O1, a warning) and one platform-package rule (P1), with twenty conformance cases. What is still missing is in KNOWN-GAPS.md.

Directories

Path Synopsis
cmd
garm command
Command garm is the garm command line tool: it scaffolds a proto tree, compiles governed tool declarations, and refuses the bad ones.
Command garm is the garm command line tool: it scaffolds a proto tree, compiles governed tool declarations, and refuses the bad ones.
protoc-gen-garm-go command
Command protoc-gen-garm-go is the garm tool generator for Go, in the shape protoc and buf expect to find it: a standalone binary named protoc-gen-<name>, discoverable on PATH.
Command protoc-gen-garm-go is the garm tool generator for Go, in the shape protoc and buf expect to find it: a standalone binary named protoc-gen-<name>, discoverable on PATH.
contracts module
internal
catalogue
Package catalogue assembles the artifact a daemon loads at boot.
Package catalogue assembles the artifact a daemon loads at boot.
claimscheck
Package claimscheck reads the vocabulary a claims policy references — the compartments and tool sets its roles grant — so a CI gate can compare that vocabulary against what a tool catalogue declares.
Package claimscheck reads the vocabulary a claims policy references — the compartments and tool sets its roles grant — so a CI gate can compare that vocabulary against what a tool catalogue declares.
cli
Package cli holds the cobra conventions every garm binary shares.
Package cli holds the cobra conventions every garm binary shares.
compile
Package compile turns proto source into descriptors, in process.
Package compile turns proto source into descriptors, in process.
compiler
Package toolgen reads garm tool annotations off descriptors and validates them.
Package toolgen reads garm tool annotations off descriptors and validates them.
contractsrepo
Package contractsrepo locates a checkout of github.com/garm-ai/contracts.
Package contractsrepo locates a checkout of github.com/garm-ai/contracts.
manifest
Package manifest reads catalogue.yaml and resolves what it names.
Package manifest reads catalogue.yaml and resolves what it names.
plugin
Package plugin is the protoc plugin that emits garm tool bindings and fails the build on any error-level lint diagnostic.
Package plugin is the protoc plugin that emits garm tool bindings and fails the build on any error-level lint diagnostic.
policydiff
Package policydiff reports what changed about POLICY between two catalogues, and which direction it moved.
Package policydiff reports what changed about POLICY between two catalogues, and which direction it moved.

Jump to

Keyboard shortcuts

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