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

approver

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package approver is the portable approval channel: one interface over what an operator surface must show and may decide, and the store-backed implementation of it. A chat bot, a browser front end, or a command line written over Approver shows an approval, decides it bound to the hash it showed, or cancels the run, and never touches the database directly. approver/webhook is the reference HTTP channel over it; cmd/agentrt is the terminal one, written before this package and the model for what a channel must show.

Nothing here resumes a run: that needs the consumer's agent, tools, and policy, so it stays in the consumer's own process (agentrt.Driver.Resume). A granted approval waits there until it is resumed or, with Limits.GrantTTL, expires.

Identity is a label. By is recorded exactly as given and never verified, as cmd/agentrt's -by is; the channel in front of this package is where a person is authenticated, and the database is the trust boundary: anyone who can write it can approve anything in it.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrDecision = errors.New("approver: decision is incomplete")

ErrDecision means a decision named no run, no approval, or no identity. A missing hash is the runtime's agentrt.ErrApprovalChanged instead.

View Source
var ErrExpired = errors.New("approver: approval has expired")

ErrExpired means the approval had passed its expiry when the decision arrived: the runtime expired it and cancelled the run as approval_expired. An error matching it also matches the runtime's agentrt.ErrApprovalExpired and agentrt.ErrNotPending, which an expired approval has always returned.

Functions

This section is empty.

Types

type Approver

type Approver interface {
	// Pending returns at most limit of a run's pending approvals, in
	// creation order, when the run is waiting for approval; an empty list
	// for a run that is not waiting, since nothing on it can be decided;
	// and agentrt.ErrNotFound for a run that does not exist.
	Pending(ctx context.Context, runID string, limit int) ([]Pending, error)
	// Show reads one approval of a run, whatever its status, for
	// presenting; agentrt.ErrNotFound when the run has no such approval.
	Show(ctx context.Context, runID, approvalID string) (Pending, error)
	// Approve grants the approval. The run stays waiting until its owner
	// resumes it.
	Approve(ctx context.Context, d Decision) error
	// Reject refuses the approval, which ends the run as CANCELLED.
	Reject(ctx context.Context, d Decision) error
	// Cancel ends a run that is not terminal, waiting or not, without
	// deciding its approvals.
	Cancel(ctx context.Context, runID, by, note string) error
}

Approver is what a channel needs. Every method addresses a run by id: there is no listing across runs, so a channel that holds one run's id learns nothing about another's. Errors are the runtime's own (agentrt.ErrNotFound, ErrRunState, ErrNotPending, ErrApprovalChanged, ErrApprovalHash), wrapped so errors.Is matches them, plus ErrExpired and ErrDecision from this package.

type Decision

type Decision struct {
	RunID      string `json:"run_id"`
	ApprovalID string `json:"approval_id"`
	Hash       string `json:"hash"`
	By         string `json:"by"`
	Note       string `json:"note,omitempty"`
}

Decision is what a channel supplies to decide an approval. Hash is the hash of the approval as it was shown, and the decision is refused with agentrt.ErrApprovalChanged if the stored approval differs from it. An empty Hash differs from every stored hash, so Local refuses it with agentrt.ErrApprovalChanged, not ErrDecision, and nothing is decided; approver/webhook refuses a body without one before it reaches the Approver, as 400 bad_request. By is an identity label, recorded as given; an empty one is ErrDecision.

type Local

type Local struct {
	// Now, when set before the Local is used, is the clock that judges
	// expiry and stamps decisions, for a channel's tests; nil is the wall
	// clock. It is not for a consumer's run clock: a deterministic
	// Config.Now also stamps the run's own records, one reading per
	// recorded state change, so a decision stamped through the same clock
	// here takes a reading the run did not, and every later timestamp of
	// the run shifts. casework's replay recordings stopped matching when
	// it shared its scenario clock this way; a consumer leaves Now nil.
	Now func() time.Time
	// contains filtered or unexported fields
}

Local is the Approver over a store this process can open: the runtime's own decisions without a driver (agentrt.Operator), which judge expiry by the wall clock unless Now is set, as cmd/agentrt uses them.

Example

A channel shows a run's pending approval and decides it bound to the hash it showed; a hash that is not the stored one decides nothing, and a decision without an identity is refused before the runtime sees it.

ctx := context.Background()
store, err := agentrt.OpenStore(":memory:")
if err != nil {
	panic(err)
}
defer store.Close()
push := tool("push", agentrt.RemoteMutation)
agent := &scripted.Agent{Decisions: []agentrt.Decision{scripted.ToolCall("push", `{"n":7}`, "publish"), scripted.Complete(`{}`)}}
n := 0
d, err := agentrt.NewDriver(agentrt.Config{Store: store, Agent: agent, Policy: agentrt.DefaultPolicy(), Tools: []agentrt.Tool{push},
	NewID: func() string { n++; return fmt.Sprintf("id-%d", n) }, Now: func() time.Time { return time.Unix(1700000000, 0) }})
if err != nil {
	panic(err)
}
run, err := d.Start(ctx, "publish the counter", agentrt.DefaultLimits())
if err != nil {
	panic(err)
}

ap := approver.New(store, nil)
pending, err := ap.Pending(ctx, run.ID, 10)
if err != nil {
	panic(err)
}
a := pending[0]
fmt.Println(a.ID, a.Kind, a.Request.Spec.Name, string(a.Request.Args), len(a.Hash), a.Expired)

err = ap.Approve(ctx, approver.Decision{RunID: run.ID, ApprovalID: a.ID, Hash: "not what was shown", By: "chat:joey"})
fmt.Println(errors.Is(err, agentrt.ErrApprovalChanged))
err = ap.Approve(ctx, approver.Decision{RunID: run.ID, ApprovalID: a.ID, Hash: a.Hash})
fmt.Println(errors.Is(err, approver.ErrDecision))
err = ap.Approve(ctx, approver.Decision{RunID: run.ID, ApprovalID: a.ID, Hash: a.Hash, By: "chat:joey", Note: "ship it"})
fmt.Println(err)

// The grant waits for the consumer's own process to resume the run.
got, err := d.Resume(ctx, run.ID)
fmt.Println(got.Status, len(push.calls), err)
Output:
id-3 remote_mutation push {"n":7} 64 false
true
true
<nil>
COMPLETED 1 <nil>

func New

func New(store *agentrt.Store, obs agentrt.Observer) *Local

New is a Local over a store the caller opened and closes. obs, which may be nil, receives the runtime's events for each decision after commit.

func Open

func Open(path string, obs agentrt.Observer) (*Local, error)

Open is New over the database at path, opened for writing through agentrt.OpenExisting, which never creates or migrates a database: the file must already carry this build's schema, as the consumer's own agentrt.OpenStore leaves it, and must not be a symbolic link. A missing file is an error matching fs.ErrNotExist; a database the runtime never opened is refused as "not an agent-runtime database"; one an older build wrote, or a newer build migrated, is agentrt.ErrSchemaVersion, and the older one opens once the consumer's own process, at this build, has opened it with agentrt.OpenStore. Close closes it.

func (*Local) Approve

func (l *Local) Approve(ctx context.Context, d Decision) error

Approve implements Approver through agentrt.Operator.ApproveShown.

func (*Local) Cancel

func (l *Local) Cancel(ctx context.Context, runID, by, note string) error

Cancel implements Approver through agentrt.Operator.Cancel.

func (*Local) Close

func (l *Local) Close() error

Close closes a store Open opened and does nothing for one New was given.

func (*Local) Pending

func (l *Local) Pending(ctx context.Context, runID string, limit int) ([]Pending, error)

Pending implements Approver through agentrt.Store.ListApprovalsPage, so no row is read whole: an argument object over agentrt.MaxPageText is read as its size, and DecodeError says so.

func (*Local) Reject

func (l *Local) Reject(ctx context.Context, d Decision) error

Reject implements Approver through agentrt.Operator.RejectShown.

func (*Local) Run added in v0.3.2

func (l *Local) Run(ctx context.Context, runID string) (agentrt.Run, error)

Run implements RunReader.

func (*Local) Show

func (l *Local) Show(ctx context.Context, runID, approvalID string) (Pending, error)

Show implements Approver. The row is read whole, as cmd/agentrt reads the one it decides, and bounded for showing.

type Pending

type Pending struct {
	agentrt.Approval
	// Reason is the policy's reason for asking, from the decision that
	// paused the run (agentrt.Store.ApprovalReason), bounded as an
	// argument string is. It may carry text the model chose.
	Reason string `json:"reason,omitempty"`
	// Expired reports that ExpiresAt has passed by the Local's clock at
	// the read. Nothing is written: the runtime expires the approval, and
	// cancels the run, when the approval is next decided.
	Expired bool `json:"expired,omitempty"`
}

Pending is an approval as a channel presents it: the stored approval with its capability, presentation, and arguments bounded the way cmd/agentrt bounds them (view.BoundApproval), the policy's reason for it, and whether its expiry has passed. Hash is the hash of the stored approval, which a decision is bound to; the bounding changes what is shown, never what is hashed.

type RunReader added in v0.3.2

type RunReader interface {
	// Run returns the run as a front end reads one it may not trust:
	// every text column capped at agentrt.MaxPageText
	// (agentrt.Store.GetRunCapped), with Goal, ReasonDetail, and Result
	// then bounded as an approval's fields are (view.BoundValue,
	// view.BoundRaw). agentrt.ErrNotFound for a run that does not exist.
	Run(ctx context.Context, runID string) (agentrt.Run, error)
}

RunReader reads the run a channel is about to decide or cancel, so it can show the run without opening the store itself. It is separate from Approver, which it does not change: a channel that needs it asserts it, or holds a *Local, which implements both.

Directories

Path Synopsis
Package webhook is the reference HTTP approval channel over approver.Approver: an http.Handler that shows one run's pending approvals, shows one approval as JSON or as sanitised plain text, and takes a decision bound to the hash it showed.
Package webhook is the reference HTTP approval channel over approver.Approver: an http.Handler that shows one run's pending approvals, shows one approval as JSON or as sanitised plain text, and takes a decision bound to the hash it showed.

Jump to

Keyboard shortcuts

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