approval

package
v0.10.1 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package approval implements AgentSSH's optional async approval backend.

It is deliberately out-of-band: run requests that need approval return immediately, operators adjudicate later, and execution happens only when the agent reruns the command through the normal policy path.

Index

Constants

View Source
const (
	DefaultSessionTTL  = 12 * time.Hour
	DefaultWaitTimeout = 10 * time.Minute
)
View Source
const (
	ChannelCLI  = "cli"
	ChannelTUI  = "tui"
	ChannelExit = "exit"
	ChannelPlan = "plan"
)

Variables

View Source
var (
	ErrInvalidPlanID = errors.New("invalid plan id")
	ErrPlanNotFound  = errors.New("plan not found")
	ErrPlanScope     = errors.New("plan approvals support --once or --session only")
	ErrPlanNoPending = errors.New("plan has no pending requests")
	ErrPlansDirUnset = errors.New("plan store directory is not configured")
)
View Source
var (
	ErrInvalidID         = errors.New("invalid approval id")
	ErrPendingNotFound   = errors.New("approval request not found")
	ErrAlreadyResolved   = errors.New("approval request already resolved")
	ErrCorruptResolution = errors.New("corrupt approval resolution")
)
View Source
var ErrInvalidUTF8Command = errors.New("approval command is not valid UTF-8")

ErrInvalidUTF8Command rejects commands that are not valid UTF-8. Go's regexp has no way to express a single invalid byte: `\xF3` in a pattern denotes the *rune* U+00F3 (encoded C3 B3), not the byte 0xF3. A matcher generated from such a command would therefore fail to match the approved command while matching a different one, so these commands are rejected outright rather than approved with a matcher that cannot mean what it says.

View Source
var ErrNULCommand = errors.New("approval command contains NUL")

Functions

func CheckHostGrantRule

func CheckHostGrantRule(rule policy.Rule) error

CheckHostGrantRule reports whether a persisted approval rule is still safe to honor. Beyond the string invariant it rejects patterns generated before '+' was escaped: those act as quantifiers and authorize commands the operator never approved.

func NewID

func NewID() (string, error)

func NewPlanID added in v0.10.0

func NewPlanID() (string, error)

func RequestDigest

func RequestDigest(req PendingRequest, scope Scope) string

Types

type ApplyOptions

type ApplyOptions struct {
	Pending    PendingStore
	Sessions   SessionStore
	Audit      audit.Store
	Bundle     policy.Bundle
	PolicyPath string
	SessionTTL time.Duration
	Channel    string
	SavePolicy func(policy.Config) error
}

type ApplyResult

type ApplyResult struct {
	Request    PendingRequest `json:"request"`
	Resolution Resolution     `json:"resolution"`
	Grant      *Grant         `json:"grant,omitempty"`
	RuleName   string         `json:"rule_name,omitempty"`
}

func ApplyDecision

func ApplyDecision(opts ApplyOptions, id string, verdict Verdict, scope Scope) (ApplyResult, error)

func ApplyPlanDecision added in v0.10.0

func ApplyPlanDecision(opts ApplyOptions, id string, verdict Verdict, scope Scope) ([]ApplyResult, error)

ApplyPlanDecision adjudicates every still-pending member of a plan with one verdict. Approvals are capped at once/session: host promotion widens policy permanently and must stay a deliberate per-command decision.

type Authorization

type Authorization struct {
	Status          AuthorizationStatus
	Decision        policy.Decision
	GrantScope      Scope
	GrantMatcher    string
	ApprovalMatcher Matcher
}

func Authorize

func Authorize(cfg policy.Config, inv inventory.Inventory, sessionStore SessionStore, runtime RuntimeConfig, sessionID string, host string, command string, stdinSHA256 string, reqID string) (Authorization, error)

Authorize decides one run request. A matching once grant is claimed under reqID (two-phase consumption): the caller must settle the claim with SessionStore.Commit once the command reaches the remote, or SessionStore.Release if it verifiably never executed. stdinSHA256 is empty for runs without stdin; when set, grants must carry the same stdin hash, persistent host approval rules never match, and the candidate matcher is forced to exact and non-promotable.

func PreflightAuthorize

func PreflightAuthorize(cfg policy.Config, inv inventory.Inventory, sessionStore SessionStore, runtime RuntimeConfig, sessionID string, host string, command string, stdinSHA256 string) (Authorization, error)

PreflightAuthorize is the side-effect-free variant used to preview a batch before executing any of it.

type AuthorizationStatus

type AuthorizationStatus string
const (
	AuthAllow         AuthorizationStatus = "allow"
	AuthAllowByGrant  AuthorizationStatus = "allow_by_grant"
	AuthHardDeny      AuthorizationStatus = "hard_deny"
	AuthNeedsApproval AuthorizationStatus = "needs_approval"
)

type Grant

type Grant struct {
	Scope      Scope       `json:"scope"`
	Kind       MatcherKind `json:"kind"`
	Regex      string      `json:"regex"`
	Prefix     []string    `json:"prefix,omitempty"`
	SourceCmd  string      `json:"source_cmd"`
	Host       string      `json:"host"`
	GrantedTS  string      `json:"granted_ts"`
	ExpiresTS  string      `json:"expires_ts"`
	ApprovalID string      `json:"approval_id"`
	ReqID      string      `json:"req_id"`
	Channel    string      `json:"channel"`
	// StdinSHA256 binds the grant to one exact stdin payload. Empty means the
	// approved command had no stdin; a grant never matches a run whose stdin
	// hash differs from the one the operator approved.
	StdinSHA256 string `json:"stdin_sha256,omitempty"`
	// ClaimReqID/ClaimTS implement two-phase once-grant consumption: Claim marks
	// the grant as reserved by one run request; Commit deletes it once the command
	// reached the remote; Release restores it when the command never executed.
	// A claim never expires by wall clock: a crash between claim and settle leaves
	// the grant unusable (fail-closed), same as a consumed grant.
	ClaimReqID string `json:"claim_req_id,omitempty"`
	ClaimTS    string `json:"claim_ts,omitempty"`
}

type HostGrantMode

type HostGrantMode string
const (
	HostGrantExact      HostGrantMode = "exact"
	HostGrantSafePrefix HostGrantMode = "safe-prefix"
	HostGrantPrefix     HostGrantMode = "prefix"
)

type Matcher

type Matcher struct {
	Kind       MatcherKind `json:"kind"`
	Regex      string      `json:"regex"`
	Prefix     []string    `json:"prefix,omitempty"`
	Promotable bool        `json:"promotable"`
	SourceCmd  string      `json:"source_cmd"`
}

Matcher is the reusable command matcher that can be stored in session grants or generated host rules.

func Exact

func Exact(command string) (Matcher, error)

func Generalize

func Generalize(command string, mode HostGrantMode) (Matcher, error)

func (Matcher) Match

func (m Matcher) Match(command string) (bool, error)

func (Matcher) SHA256

func (m Matcher) SHA256() string

type MatcherKind

type MatcherKind string
const (
	MatcherExact  MatcherKind = "exact"
	MatcherPrefix MatcherKind = "prefix"
)

type PendingRequest

type PendingRequest struct {
	Version        int     `json:"version"`
	ID             string  `json:"id"`
	ReqID          string  `json:"req_id"`
	SessionID      string  `json:"session_id"`
	Host           string  `json:"host"`
	Cmd            string  `json:"cmd"`
	CmdSHA256      string  `json:"cmd_sha256"`
	Candidate      Matcher `json:"candidate_matcher"`
	MatcherSHA256  string  `json:"matcher_sha256"`
	Kind           string  `json:"kind"`
	Promotable     bool    `json:"promotable"`
	TS             string  `json:"ts"`
	ProposedScopes []Scope `json:"proposed_scope"`
	// StdinSHA256/StdinBytes describe the stdin payload the run would feed the
	// command. The content itself never enters the approval store; the operator
	// adjudicates on hash + size, and the resulting grant is pinned to the hash.
	StdinSHA256 string `json:"stdin_sha256,omitempty"`
	StdinBytes  int64  `json:"stdin_bytes,omitempty"`
	// PlanID/PlanSeq/PlanTotal tag requests minted by one `plan submit` so the
	// operator can review and adjudicate the batch as a unit. Authoritative plan
	// membership lives in the plan manifest (plans/<id>.json); these fields are
	// display metadata on the requests this submit created.
	PlanID    string `json:"plan_id,omitempty"`
	PlanSeq   int    `json:"plan_seq,omitempty"`
	PlanTotal int    `json:"plan_total,omitempty"`
}

type PendingStore

type PendingStore struct {
	PendingDir   string
	ResponsesDir string
	PlansDir     string
	Now          func() time.Time
}

func (PendingStore) Create

func (PendingStore) CreatePlan added in v0.10.0

func (s PendingStore) CreatePlan(manifest PlanManifest) (PlanManifest, error)

func (PendingStore) Get

func (s PendingStore) Get(id string) (PendingRequest, error)

func (PendingStore) GetPlan added in v0.10.0

func (s PendingStore) GetPlan(id string) (PlanManifest, error)

func (PendingStore) List

func (s PendingStore) List() ([]PendingRequest, error)

func (PendingStore) PlanStatus added in v0.10.0

func (s PendingStore) PlanStatus(id string) (PlanStatus, error)

PlanStatus resolves every member's current status. A member whose pending file has been reaped after resolution counts as expired — fail-closed, the plan never reports approved from unknowable members — but expiry is kept distinct from denied so an approved-then-reaped plan is not misreported as operator-rejected.

func (PendingStore) Resolve

func (s PendingStore) Resolve(req PendingRequest, verdict Verdict, scope Scope) (Resolution, error)

func (PendingStore) Status

func (s PendingStore) Status(id string) (StatusResult, error)

func (PendingStore) Wait

func (s PendingStore) Wait(id string, timeout time.Duration) (StatusResult, error)

func (PendingStore) WaitPlan added in v0.10.0

func (s PendingStore) WaitPlan(id string, timeout time.Duration) (PlanStatus, error)

WaitPlan polls until every member is resolved or the timeout elapses, mirroring PendingStore.Wait for single approvals.

type PlanManifest added in v0.10.0

type PlanManifest struct {
	Version   int      `json:"version"`
	ID        string   `json:"id"`
	SessionID string   `json:"session_id"`
	Host      string   `json:"host"`
	TS        string   `json:"ts"`
	MemberIDs []string `json:"member_ids"`
}

PlanManifest is the authoritative membership record for one submitted plan, written once (O_EXCL) at submit time. Member requests resolve individually through the ordinary pending/response stores.

type PlanMember added in v0.10.0

type PlanMember struct {
	ApprovalID string          `json:"approval_id"`
	Status     string          `json:"status"` // pending | approved | denied | expired
	Scope      Scope           `json:"scope,omitempty"`
	Request    *PendingRequest `json:"request,omitempty"`
}

PlanMember pairs one member request with its current resolution status.

type PlanStatus added in v0.10.0

type PlanStatus struct {
	ID        string       `json:"id"`
	SessionID string       `json:"session_id"`
	Host      string       `json:"host"`
	Status    string       `json:"status"` // pending | approved | denied | expired
	Pending   int          `json:"pending"`
	Approved  int          `json:"approved"`
	Denied    int          `json:"denied"`
	Expired   int          `json:"expired,omitempty"`
	Members   []PlanMember `json:"members"`
}

PlanStatus is the aggregate view returned by plan status/wait.

type Resolution

type Resolution struct {
	Version   int     `json:"version"`
	ID        string  `json:"id"`
	ReqDigest string  `json:"req_digest"`
	Verdict   Verdict `json:"verdict"`
	Scope     Scope   `json:"scope,omitempty"`
	TS        string  `json:"ts"`
}

type RuntimeConfig

type RuntimeConfig struct {
	Enabled       bool
	HostGrantMode HostGrantMode
	SessionTTL    time.Duration
	WaitTimeout   time.Duration
}

func RuntimeConfigFromPolicy

func RuntimeConfigFromPolicy(cfg policy.Approval, envValue string) (RuntimeConfig, error)

type Scope

type Scope string
const (
	ScopeOnce    Scope = "once"
	ScopeSession Scope = "session"
	ScopeHost    Scope = "host"
)

type SessionStore

type SessionStore struct {
	Dir string
	Now func() time.Time
}

func (SessionStore) Claim added in v0.10.0

func (s SessionStore) Claim(sessionID string, host string, command string, stdinSHA256 string, reqID string) (Grant, bool, error)

Claim matches a grant for one run request. A matching once grant is marked as claimed by reqID (in the same lock, so two concurrent runs can never claim the same once grant); session grants match without side effects. The caller must settle every once claim with Commit or Release.

func (SessionStore) Commit added in v0.10.0

func (s SessionStore) Commit(sessionID string, reqID string) error

Commit consumes every once grant claimed by reqID. Call it as soon as the command has been handed to the remote: from that point re-running requires a fresh approval.

func (SessionStore) End

func (s SessionStore) End(sessionID string) error

func (SessionStore) Grant

func (s SessionStore) Grant(sessionID string, host string, scope Scope, matcher Matcher, stdinSHA256 string, approvalID string, reqID string, ttl time.Duration, channel string) (Grant, error)

func (SessionStore) Peek

func (s SessionStore) Peek(sessionID string, host string, command string, stdinSHA256 string) (Grant, bool, error)

Peek reports whether a grant would authorize the command without reserving or consuming anything. Once grants already claimed by an in-flight run are invisible: they can no longer authorize a different request.

func (SessionStore) Release added in v0.10.0

func (s SessionStore) Release(sessionID string, reqID string) error

Release restores every once grant claimed by reqID. Call it only when the command verifiably never executed (local cancel, transport failure before the remote ran it, audit append failure before execution).

type StatusResult

type StatusResult struct {
	ID      string          `json:"id"`
	Verdict Verdict         `json:"verdict,omitempty"`
	Scope   Scope           `json:"scope,omitempty"`
	Status  string          `json:"status"`
	Request *PendingRequest `json:"request,omitempty"`
}

type Verdict

type Verdict string
const (
	VerdictApproved Verdict = "approved"
	VerdictDenied   Verdict = "denied"
)

Jump to

Keyboard shortcuts

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