Documentation
¶
Overview ¶
Package approval implements the ApprovalRequest FSM service.
States: PENDING -> APPROVED | DENIED | EXPIRED | CANCELLED
Transitions are single-direction and fail-closed: deciding an already-decided approval returns ErrAlreadyDecided. Every state change emits an audit event, actor_type=human for decisions or system for expirations/CancelForRun.
Index ¶
- Constants
- Variables
- func CancelForRun(ctx context.Context, st Store, runID uuid.UUID, reason string) (map[string]int, error)
- func Decide(ctx context.Context, st Store, id uuid.UUID, decidedByType types.ActorType, ...) (types.ApprovalRequest, error)
- func ExpireOne(ctx context.Context, st Store, id uuid.UUID, actor, reason string) error
- func ExpireStale(ctx context.Context, st Store, olderThan time.Duration) (int, error)
- func ExpireStaleByKind(ctx context.Context, st Store, olderThan time.Duration) (int, map[string]int, error)
- func Hold(ap types.ApprovalRequest, now time.Time) (held bool, until time.Time)
- func RequestApproval(ctx context.Context, st Store, req types.ApprovalRequest) (types.ApprovalRequest, error)
- func TallyKey(ap types.ApprovalRequest) string
- type Store
Constants ¶
const ( // TallyToolCallADO is an Azure DevOps capability escalation. TallyToolCallADO = "tool_call:azure_devops" // TallyReauthAWSSSO is a lapsed captured AWS SSO session's re-auth. TallyReauthAWSSSO = "credential_reauth:aws_sso" // TallyReauthADOSignIn is an Azure DevOps sign-in request. TallyReauthADOSignIn = "credential_reauth:azure_devops_signin" // TallyReauthADOConsent is an Azure DevOps consent request. TallyReauthADOConsent = "credential_reauth:azure_devops_consent" )
Tally keys for the rows one kind carries with two meanings (#151). Every other row is tallied under its bare kind. Documented on docs/AUDIT-ACTIONS.md's approval.cancel row.
Variables ¶
var ErrAlreadyDecided = types.ErrApprovalAlreadyDecided
ErrAlreadyDecided re-exports the store's sentinel (defined in internal/types) so errors.Is matches whichever layer raised it.
Functions ¶
func CancelForRun ¶ added in v0.7.2
func CancelForRun(ctx context.Context, st Store, runID uuid.UUID, reason string) (map[string]int, error)
CancelForRun transitions every still-PENDING approval for runID to CANCELLED, returning how many moved per TallyKey (counted where the CAS lands, so a row a human decided first is excluded). reason is the run's ending transition, recorded on each row and on the one audit event emitted.
Mirrors ExpireStale's shape: list PENDING, move each row with the same CAS primitive (DecideApproval's WHERE state='PENDING' IS the FSM guard) so a concurrent human decision wins and ErrAlreadyDecided is treated as a race, not an error. DecidedBy is "system": nobody decided this, the run ended.
ONE audit row for the whole batch (count + by_kind), not one per approval: these all belong to a single run transition an operator reads as one fact, keyed to the run so it lands in that run's evidence rail. Nothing pending means nothing emitted, keeping a re-kill idempotent in the audit log too.
Emitted on the failure path too, with however many rows DID move plus the error, so a partial cancel never leaves CANCELLED rows with no trail.
func Decide ¶
func Decide(ctx context.Context, st Store, id uuid.UUID, decidedByType types.ActorType, decision types.ApprovalDecision) (types.ApprovalRequest, error)
Decide transitions an approval to decision.State (APPROVED or DENIED; ExpireStale bypasses Decide for EXPIRED, since a sweep is not a decision). decidedByType is human (OIDC session or LocalMode operator) or system (bare admin-token caller); the audit event records that exact type so an admin-token decision is never mislabelled as a human approval (attribution honesty invariant).
func ExpireOne ¶ added in v0.8.0
ExpireOne transitions a single PENDING approval straight to EXPIRED, regardless of age — the same terminal state ExpireStaleByKind's sweep would eventually give it, but raised eagerly by the client giving up on it (wardyn-toolgate at its own -deadline). Without this, a row the gate already treats as denied stays PENDING and approvable for up to one sweep interval (#811).
Idempotent: a row already decided (human or concurrent sweep) is left untouched, ErrAlreadyDecided swallowed as the race it is.
Attributed to the run's agent, not the system: the sandbox can call this at any time, so nothing ties it to a deadline.
func ExpireStale ¶
ExpireStale transitions to EXPIRED every PENDING approval requested before the cutoff, or whose own ExpiresAt has passed, emitting one audit event per expiration. Returns the count expired. The cutoff is the deployment's ceiling and binds every row regardless of its run's wait.
func ExpireStaleByKind ¶ added in v0.7.6
func ExpireStaleByKind(ctx context.Context, st Store, olderThan time.Duration) (int, map[string]int, error)
ExpireStaleByKind is ExpireStale plus a tally of what it moved, keyed by TallyKey rather than the bare Kind: a credential_reauth row is either an Azure DevOps sign-in/consent or an AWS SSO re-auth, and keying on the bare Kind would fold both into one bucket, silently miscounting a metric whose HELP promises the AWS SSO population alone.
Tallied HERE, where the state actually changes, not at a later resolve that happens to meet a terminal row — that would count retries, not outcomes. Kept as a separate function so every existing caller's signature is unchanged.
func Hold ¶ added in v0.8.0
Hold reports whether ap is currently parking its run's sandbox and, for a bounded hold, when parking ends. Ported from ui's now-retired isHeld (same arm order, same three windows in internal/types/holds.go, no behavior change).
A row that is not PENDING is never held — a decision or the expiry sweeper already answered the question — so every decided/EXPIRED/CANCELLED row returns (false, zero) regardless of kind or age; only the server's own state ends a hold.
until is the zero time.Time for an unconditional hold (tool_call outside the ADO-escalation shape, credential_reauth) and for a row not held at all.
func RequestApproval ¶
func RequestApproval(ctx context.Context, st Store, req types.ApprovalRequest) (types.ApprovalRequest, error)
RequestApproval creates a new PENDING approval, or returns the existing PENDING approval when one already exists for the same run+kind+scope hash (deduplication guard).
func TallyKey ¶ added in v0.8.0
func TallyKey(ap types.ApprovalRequest) string
TallyKey is the key CancelForRun counts ap under, read off the stored row alone; restates internal/api's own lane-detection logic, and TestApprovalTallyKeyAgreesWithTheLanes pins that they agree.
Types ¶
type Store ¶
type Store interface {
CreateApproval(ctx context.Context, a types.ApprovalRequest) (types.ApprovalRequest, error)
GetApproval(ctx context.Context, id uuid.UUID) (types.ApprovalRequest, error)
// ListApprovals returns approvals filtered by state (empty = all).
ListApprovals(ctx context.Context, stateFilter types.ApprovalState) ([]types.ApprovalRequest, error)
// DecideApproval transitions PENDING to decision.State; returns
// ErrAlreadyDecided if not PENDING.
DecideApproval(ctx context.Context, id uuid.UUID, decision types.ApprovalDecision) (types.ApprovalRequest, error)
Record(ctx context.Context, ev types.AuditEvent) error
}
Store is the narrow persistence interface the approval FSM needs.