approval

package
v0.8.0 Latest Latest
Warning

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

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

Documentation

Overview

Package approval implements the human-in-the-loop gate (CLAUDE.md headline feature). A side-effecting tool call that policy gates pauses until a human approves or denies it. Policy evaluation is deterministic — no LLM decides whether something needs approval.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Decision

type Decision struct {
	Approved bool
	Reason   string
	By       string
}

Decision is the outcome of an approval request.

type Gate

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

Gate is the human-in-the-loop approval queue. It persists pending approvals, notifies push channels, and blocks the calling (tool) goroutine until a human resolves the request via the API/CLI/web — or the run's context is cancelled.

func NewGate

func NewGate(store storage.Store, policy Policy, notifier Notifier, log *slog.Logger) *Gate

NewGate constructs a Gate. notifier may be nil (no push channel).

func (*Gate) Create

func (g *Gate) Create(ctx context.Context, req Request) (storage.ApprovalRecord, bool, error)

Create evaluates policy and, if approval is required, persists a pending approval and notifies push channels, returning the record (required=true). Unlike Request, it does NOT block — the caller (e.g. the SDK over HTTP) polls the approval's status instead. Returns required=false when policy allows the call outright.

func (*Gate) Get

func (g *Gate) Get(ctx context.Context, id string) (storage.ApprovalRecord, error)

Get returns a single approval by id.

func (*Gate) Pending

func (g *Gate) Pending(ctx context.Context) ([]storage.ApprovalRecord, error)

Pending returns the currently-pending approvals.

func (*Gate) Request

func (g *Gate) Request(ctx context.Context, req Request) (Decision, string, error)

Request gates a side-effecting call. If approval is not required it returns an approved Decision immediately. Otherwise it persists a pending approval, notifies push channels, and BLOCKS until the request is resolved or ctx is done (run cancel / time budget). This is how a side-effecting call "pauses".

func (*Gate) RequestUnder added in v0.7.0

func (g *Gate) RequestUnder(ctx context.Context, req Request, policy Policy) (Decision, string, error)

RequestUnder is Request, but evaluates the supplied policy instead of the gate's default. It lets a run enforce its own policy bundle's approval rules per-run (a referenced policyRef) rather than only the daemon-global policy.

func (*Gate) Required

func (g *Gate) Required(tool, sideEffect string) bool

Required reports whether a call needs approval under the gate's policy.

func (*Gate) Resolve

func (g *Gate) Resolve(ctx context.Context, id string, approved bool, reason, by string) error

Resolve records a human decision and wakes any blocked waiter. Returns storage.ErrNotFound if the approval is unknown or already resolved.

type Notifier

type Notifier interface {
	Notify(ctx context.Context, a storage.ApprovalRecord)
}

Notifier pushes a newly-pending approval to a channel (e.g. a webhook). The CLI and web page are pull channels and do not implement this.

func CombineNotifiers added in v0.6.0

func CombineNotifiers(notifiers ...Notifier) Notifier

CombineNotifiers returns a single Notifier over the given channels, dropping nil ones. Returns nil when none remain (the Gate treats nil as "no push channel"), or the sole notifier unwrapped when only one is configured.

type Policy

type Policy struct {
	// RequireFor lists match rules; a call needs approval if it matches ANY rule.
	RequireFor []Rule
	// DefaultSafe, when true, requires approval for ANY call with a non-empty side
	// effect even if no rule matched — fail closed on side effects.
	DefaultSafe bool
}

Policy is the deterministic rule set deciding which calls need approval. It mirrors the api/v1 ApprovalPolicy schema.

func (Policy) Requires

func (p Policy) Requires(tool, sideEffect string) bool

Requires reports whether a tool call needs human approval.

type Request

type Request struct {
	RunID      string
	StepIndex  int32
	Tool       string
	SideEffect string
	Arguments  map[string]any
}

Request describes a side-effecting tool call seeking approval.

type Rule

type Rule struct {
	Tool       string
	SideEffect string
}

Rule matches a tool call by exact tool name or by a side-effect glob (e.g. "*write*"). A rule with both set matches if EITHER matches.

type SlackAction added in v0.6.0

type SlackAction struct {
	ApprovalID string
	Approve    bool
	By         string
	Channel    string // for updating the source message
	MessageTS  string
}

SlackAction is a parsed Approve/Deny click: which approval, the decision, the human who clicked, and the message to update.

type SlackNotifier added in v0.6.0

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

SlackNotifier is a push channel that posts a pending approval to a Slack channel with Approve/Deny buttons, and verifies the signed interaction Slack sends back when a human clicks one. Outbound uses a bot token (chat.postMessage); inbound is authenticated by the app's signing secret (not the daemon's API token, which Slack can't send). The bot token and signing secret are secrets — read from the environment, never logged. See SECURITY.md.

func NewSlackNotifier added in v0.6.0

func NewSlackNotifier(botToken, channel, signingSecret string, log *slog.Logger) *SlackNotifier

NewSlackNotifier returns a notifier, or nil if the bot token or channel is unset (no Slack push channel). A missing signing secret still allows outbound posts but makes the inbound interaction endpoint fail closed (it can't verify Slack).

func (*SlackNotifier) Notify added in v0.6.0

Notify posts the approval to Slack asynchronously (best-effort; a flaky Slack must never wedge a governed run).

func (*SlackNotifier) ParseInteraction added in v0.6.0

func (s *SlackNotifier) ParseInteraction(payload []byte) (SlackAction, bool)

ParseInteraction decodes a Slack block_actions payload into a SlackAction, or (_, false) if it isn't a recognized Approve/Deny click.

func (*SlackNotifier) UpdateResolved added in v0.6.0

func (s *SlackNotifier) UpdateResolved(ctx context.Context, act SlackAction, a storage.ApprovalRecord)

UpdateResolved rewrites the source Slack message to show the decision, replacing the buttons so it can't be actioned twice. Best-effort.

func (*SlackNotifier) VerifySignature added in v0.6.0

func (s *SlackNotifier) VerifySignature(timestamp, signature string, body []byte) bool

VerifySignature checks a Slack request signature (v0 scheme: HMAC-SHA256 of "v0:{timestamp}:{body}" keyed by the signing secret), rejecting a stale timestamp to defeat replay. Returns false (fail closed) if no signing secret is configured. Constant-time compare.

type WebhookNotifier

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

WebhookNotifier POSTs a JSON notification to a user-configured URL when an approval becomes pending. This is user-configured outbound network (like the OTLP endpoint) — RiskKernel calls it only because the user set the URL. It does NOT include any provider keys or secrets. See SECURITY.md.

func NewWebhookNotifier

func NewWebhookNotifier(url string, log *slog.Logger) *WebhookNotifier

NewWebhookNotifier returns a notifier, or nil if url is empty (no push channel).

func (*WebhookNotifier) Notify

Notify fires the webhook asynchronously (best-effort; failures are logged, never fatal — a flaky webhook must not wedge a governed run).

Jump to

Keyboard shortcuts

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