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

contracts

package module
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: MIT Imports: 1 Imported by: 1

README

contracts

The contract garm's tools are written against, as a Go module of its own.

A tool service, a daemon, a runner and a token exchange all have to agree on what a governed call looks like: the annotations that declare it, the message types that carry it, the subject it travels on, the row it leaves behind, and the token that says a person approved it. That agreement is this repository.

It is not the command line tool. That is garm, which depends on this. Nothing here depends on anything of garm's.

Contents

Why it is a module of its own

go.mod has three requires and no indirects:

google.golang.org/protobuf every generated type
buf.build/gen/go/bufbuild/protovalidate/... the request constraints garm.tasks.v1 declares, read by cards
github.com/go-jose/go-jose/v4 verifying an approval grant's signature, in grants

Before the split those types lived in the CLI's module. A service that imported one message type inherited the AWS SDK, cobra, pflag, protocompile, cel-go, an ANTLR runtime and yaml — from a binary it never ran. sink drains a ledger to Parquet and was pulling cobra.

boundary_test.go is what keeps the list short, and it is a package boundary as well as a module one: nothing an author imports may reach the plan compiler. See CLAUDE.md.

Who imports it

garmd the daemon — policy, policy/redact, audit, callctx, grant, ledger, wire, and the catalogue, ledger and tool types
agentd the agent runner — the agent, card, catalogue, ledger, meta and tool types, and policy
tasksd the task service — garm.tasks.v1, garm.card.v1, callctx, grant, grants, wire
tool-go what a Go tool service imports — callctx, wire, the tool types
sts the token exchange — grant, to compute the digest it signs over
sink the ledger drain — the ledger types and wire
stack, examples, tools the development stack and the tool trees written against the annotations

Ten Go modules across nine repositories. They reach these packages through github.com/garm-ai/garm today, at versions from v0.4.0 to v0.17.0; moving them to this path is the next step, and KNOWN-GAPS.md says what that costs.

What is in here

The generated Go sits at the module root, so an import reads github.com/garm-ai/contracts/garm/tool/v1 rather than stuttering through a contracts/ directory.

proto/garm/tool/v1/ The tool annotations, the invocation envelope, the error and attribution types. The only namespace a daemon's version check covers
proto/garm/agent/v1/ The agent manifest — mode, principal, model, bounds, prompts, the tool allowlist
proto/garm/card/v1/ The card vocabulary a renderer draws, and the result_card / task_card templates an author declares
proto/garm/tasks/v1/ The task queue as governed tools: the contract this repository publishes and tasksd serves. get_task_grant is the runner's read back of a decided approval, VERB_READ in the escalation set — the decided event carries a reference and never the grant; KNOWN-GAPS.md has what its gate cannot check yet
proto/garm/catalogue/v1/ The artefact garm catalogue build writes and a daemon loads, and the provenance that records every input it was composed from — each local tree, and each Go module at the version go.mod resolved it to
proto/garm/ledger/v1/ One event shape for both planes
proto/garm/meta/v1/ Ownership — the owner a service or method carries
garm/**/v1/ The generated Go for each of the above. Regenerated, never edited
policy/ The plan compiler: field annotations become a redaction plan, resolved against a caller and applied per call. garmd imports this rather than reimplementing it — two implementations of plan compilation would be a governance bug
policy/redact/ The redactions a plan can name
cards/ The three card endpoints every tool serves: the names, and the defaults built from the descriptor
callctx/ Encoding InvocationContext for the NATS hop, behind one function so the envelope can move without touching every call site
wire/ A route becomes a subject, a queue group and an endpoint name. Both sides of the hop were deriving these separately
ledger/ The shape of a governed call's record, and the idempotency key for one event
audit/ The fail-closed half of the record. ledger.Recorder must never fail a call; a tool declaring audit: { fail_closed: true } says the opposite, so they cannot be one interface
grant/ The digest over the values a person approved. A minter and a verifier in two repositories have to agree on it byte for byte
grants/ Verifying a grant — the part that is contract logic rather than daemon policy
embed.go The annotation bytes garm init vendors, verbatim, and AnnotationSchemaVersion

Using it

go get github.com/garm-ai/contracts@vX.Y.Z
import (
    toolv1 "github.com/garm-ai/contracts/garm/tool/v1"
    "github.com/garm-ai/contracts/callctx"
    "github.com/garm-ai/contracts/wire"
)

It is not published to a registry and is consumed at a git tag. This module's own releases start at v0.1.0; the nineteen tags that came over from garm with the history were deleted, because each of them describes the command line tool and points at a tree with no go.mod — so @latest would have resolved, on the day this repository got a remote, to something no consumer could build.

Proto package names did not move in the split. garm.tool.v1 is still garm.tool.v1, so no subject, service name or wire identity changed — only the Go import path and the go_package option did.

Regenerating

Two templates, and both are needed:

buf generate                                  # the contract, into garm/**/v1
buf generate --template buf.gen.testdata.yaml # the policy fixture and its connect-go mirror

Or mise run gen, which runs both. mise run gen-check fails if the committed output differs from what they produce — that check is the one that matters most here, because stale generated code in this module is a consumer's build failure rather than ours.

The testdata template is easy to forget and deliberately does not generate a tool registry: that binds to the enforcing package, which lives in the daemon's repository.

Never hand-edit a .pb.go. A generated file embeds its own FileDescriptorProto as a length-prefixed byte string. Editing a path inside it leaves the prefix describing a length that is no longer there, and the result is a binary that panics in filedesc at init with a slice bound out of range, at the first import, with nothing in the message about what you changed. Change the .proto and regenerate.

What a regeneration moves, and what it does not. Changing a file option — go_package, say — changes the catalogue digest, because a catalogue embeds a FileDescriptorSet and a file descriptor carries its options. It does not change a descriptor hash: that is computed from message full names, field numbers, names, cardinality and kind, and never reads an option. So a service and the catalogue do not have to be rebuilt in lockstep for routing to keep working; only something pinning a catalogue digest sees it.

The relationship to garm

garm is the command line tool: it scaffolds a proto tree, lints governed declarations, runs the generators and builds the catalogue. It depends on this module. This module depends on nothing of garm's, and boundary_test.go refuses a build that changes that.

The split is one direction only. garm keeps cmd/garm, cmd/protoc-gen-garm-go, internal/ and conformance/ — conformance tests the CLI's own compiler, so it belongs beside it.

Working here

mise install    the toolchain
mise run gen    both generators
mise run test   go test ./... -race
mise run ci     what CI runs

GOWORK=off matters here. There is a go.work above this checkout, and the boundary test forces the workspace off on its own subprocess so that it sees what a consumer sees rather than what a developer inside the workspace does. Every task in mise.toml does the same, for the same reason.

KNOWN-GAPS.md says what is not done.

Licence

MIT. See LICENSE.

Documentation

Overview

Package contracts is the root of garm's generated tool contracts.

It is a separate Go module, and now a separate repository, on purpose. A tool service depends on this module and on garmtool, and on nothing else: the boundary is what stops a tool importing toolplane and attempting the governance chain locally — a second, unreviewed implementation of a policy that already ran before the request crossed NATS.

The second reason is weight. This module requires three things. The command line tool it was extracted from requires eleven, including an S3 client and a CLI framework, and every consumer of a message type used to inherit them.

It is not published to a registry. Consume it at a git tag:

go get github.com/garm-ai/contracts@v0.1.0

Package garm embeds the contract this repository publishes.

The annotations are vendored into a consumer's tree by `garm init` rather than fetched from a registry. That is deliberate: a registry is one more thing an enterprise build has to be allowed to reach, and the annotations are small, stable, and worth reading by the people writing tool schemas against them.

The bytes here are the same bytes `garm init` writes, verbatim and with no added header, so a later drift check is a byte comparison rather than a parse.

Index

Constants

View Source
const AnnotationSchemaVersion = 1

AnnotationSchemaVersion is the version of the garm.tool.v1 vocabulary this build speaks, stamped into every catalogue it produces.

garm.tool.v1 only. The sibling vocabularies — garm.agent.v1, garm.card.v1, garm.meta.v1 — are read by the runner and never by the daemon, so a change to them is not a change to what a daemon must understand and never bumps this. cmd/garm's catalogue tests hold that line.

A daemon reads the current version and the two previous and refuses anything outside that window at boot. It is an integer rather than a semver because the only question asked of it is "can this binary read that catalogue", and a range check wants a number.

Bump it when a change to the annotations means an older daemon would misread a declaration — not when a field is added that an older daemon can safely ignore.

View Source
const VendoredAgentAnnotationsPath = "third_party/proto/garm/agent/v1/agent.proto"

VendoredAgentAnnotationsPath is where AgentAnnotationsProto belongs in a consumer's tree.

The same third_party module as the tool annotations, for the same reason: every module in a buf v2 workspace is an input, so annotations under proto/ would have Go generated for them, and two packages registering one proto file panic at init. The tail of the path is part of the contract — an import of "garm/agent/v1/agent.proto" has to resolve.

View Source
const VendoredAnnotationsPath = "third_party/proto/garm/tool/v1/tool.proto"

VendoredAnnotationsPath is where AnnotationsProto belongs in a consumer's tree.

Under third_party rather than beside your own protos, and that is not cosmetic: every module in a buf v2 workspace is an input, so annotations living under proto/ get Go generated for them — output that is never usable, because the real one is in this module's contracts package and two packages registering one proto file panic at init.

The tail of the path is part of the contract: an import of "garm/tool/v1/tool.proto" has to resolve, so only the root is a choice.

View Source
const VendoredCardAnnotationsPath = "third_party/proto/garm/card/v1/card.proto"

VendoredCardAnnotationsPath is where CardAnnotationsProto belongs in a consumer's tree. Same module, same reasoning as the two above; an import of "garm/card/v1/card.proto" has to resolve.

View Source
const VendoredMetaAnnotationsPath = "third_party/proto/garm/meta/v1/meta.proto"

VendoredMetaAnnotationsPath is where MetaAnnotationsProto belongs in a consumer's tree; an import of "garm/meta/v1/meta.proto" has to resolve.

Variables

View Source
var AgentAnnotationsProto []byte

AgentAnnotationsProto is proto/garm/agent/v1/agent.proto, exactly as published.

View Source
var AnnotationsProto []byte

AnnotationsProto is proto/garm/tool/v1/tool.proto, exactly as published.

View Source
var CardAnnotationsProto []byte

CardAnnotationsProto is proto/garm/card/v1/card.proto, exactly as published: the card vocabulary and the result_card / task_card / card_role templates an author declares beside a tool or an agent.

View Source
var MetaAnnotationsProto []byte

MetaAnnotationsProto is proto/garm/meta/v1/meta.proto, exactly as published: the owner annotation a service or method carries.

Functions

This section is empty.

Types

This section is empty.

Directories

Path Synopsis
Package audit is the durable, separately-retained half of the record.
Package audit is the durable, separately-retained half of the record.
Package callctx encodes the InvocationContext for the NATS hop.
Package callctx encodes the InvocationContext for the NATS hop.
Package cards synthesises the three card endpoints every tool serves.
Package cards synthesises the three card endpoints every tool serves.
garm
Package grant computes what a human approved.
Package grant computes what a human approved.
Package grants is the part of grant verification that is contract logic rather than daemon policy.
Package grants is the part of grant verification that is contract logic rather than daemon policy.
Package ledger is the shape of a governed call's record.
Package ledger is the shape of a governed call's record.
Package toolpolicy resolves what a principal may see, from annotations on the schema.
Package toolpolicy resolves what a principal may see, from annotations on the schema.
redact
Package redact turns a denied field value into what the caller may see.
Package redact turns a denied field value into what the caller may see.
Package wire is how a route becomes a NATS address.
Package wire is how a route becomes a NATS address.

Jump to

Keyboard shortcuts

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