hitl

package
v0.40.0 Latest Latest
Warning

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

Go to latest
Published: Nov 10, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

package hitl is the pure logic behind human-in-the-loop interventions: the policy an environment is created with, the intervention record, and the state machine that moves it. it does no i/o; the orchestrator owns storage, waiting and delivery.

Index

Constants

View Source
const (
	// HeaderActor carries the actor id on any request.
	HeaderActor = "Fuse-Actor"
	// HeaderActorName carries the optional display name.
	HeaderActorName = "Fuse-Actor-Name"

	MaxActorIDLen   = 128
	MaxActorNameLen = 256
)
View Source
const (

	// MaxSecretValueLen bounds a value a human types or pastes.
	MaxSecretValueLen = 64 << 10
	// ReservedGuestDir holds fuse's own credentials in the guest; no secret
	// target may point into it.
	ReservedGuestDir = "/fuse"
)
View Source
const (
	// DefaultTimeout is the deadline when the spec sets none.
	DefaultTimeout = 10 * time.Minute
	// MinTimeout keeps a deadline above the reconcile granularity that
	// backs it up.
	MinTimeout = 10 * time.Second
	// MaxTimeout bounds every wait. no unbounded waits, ever.
	MaxTimeout = 24 * time.Hour
)
View Source
const AllowlistGate = "exec.allowlist"

AllowlistGate is the name the allowlist reports on interventions.

View Source
const DeciderTimeout = 5 * time.Second

DeciderTimeout bounds one decider call. it is fixed, not configurable, because the agent's call is blocked on it.

View Source
const HeaderSignature = "Fuse-Signature"

HeaderSignature carries the webhook signature: "t=<unix>,v1=<hex>", where v1 is HMAC-SHA256 over "<t>.<body>" keyed by the named secret.

View Source
const MaxGrantTTL = 24 * time.Hour

MaxGrantTTL bounds a ttl grant.

View Source
const MaxReviewBytes = 1 << 20

MaxReviewBytes bounds a review's content, inline or read from a file.

View Source
const MaxScopedKeyTTL = 24 * time.Hour

MaxScopedKeyTTL caps the lifetime of a key minted with scopes.

View Source
const MaxWebhooks = 5

MaxWebhooks bounds hitl.notify.

View Source
const SecretAnswer = "done"

SecretAnswer is the only answer an agent ever gets back for a secret.

Variables

View Source
var (
	// ErrInvalidActor is a malformed Fuse-Actor or Fuse-Actor-Name header.
	ErrInvalidActor = errors.New("invalid actor")
	// ErrActorMismatch is a Fuse-Actor header that differs from the actor
	// pinned on the key.
	ErrActorMismatch = errors.New("actor does not match the key's pinned actor")
	// ErrActorNotAllowed is a Fuse-Actor header from a key without
	// hitl:act-as. it is refused rather than dropped so the caller learns
	// the label was not recorded.
	ErrActorNotAllowed = errors.New("key may not assert an actor")
	// ErrSelfApproval is a decision by the key or actor that raised the
	// intervention.
	ErrSelfApproval = errors.New("an intervention cannot be decided by the key or actor that raised it")
	// ErrActorRequired is a decision without a pinned or asserted actor on
	// an environment whose policy sets require_actor.
	ErrActorRequired = errors.New("this environment requires a named actor to decide")
)

AllScopes is every scope, in display order.

View Source
var ComputerActions = []string{
	"screenshot", "cursor_position", "mouse_move",
	"left_click", "right_click", "middle_click", "double_click", "triple_click",
	"left_click_drag", "left_mouse_down", "left_mouse_up",
	"type", "key", "hold_key", "scroll", "wait", "zoom",
}

ComputerActions is fused's computer action enum (fused/computer.go). a computer gate matches on these names, never on coordinates: a layout shifts, an action type does not.

View Source
var ErrBadSignature = errors.New("webhook signature does not verify")

ErrBadSignature is a webhook signature that does not verify.

View Source
var ErrTakeoverHeld = errors.New("takeover already held")

ErrTakeoverHeld is returned when a second human tries to take over an environment someone already holds.

View Source
var GenesisHash = make([]byte, sha256.Size)

GenesisHash is the prev_hash of the first row.

LifecycleActions is every gateable action, in display order.

View Source
var SystemActor = Actor{ID: "system", Source: ActorSystem}

SystemActor is the actor recorded for decisions fuse makes on its own.

Functions

func AuditTime

func AuditTime(t time.Time) time.Time

AuditTime normalizes a timestamp to what postgres stores, so a hash computed before the write matches one recomputed after a read.

func CheckDecider

func CheckDecider(triggeredBy, decider Actor, requireActor bool) error

CheckDecider enforces who may decide an intervention: never the key that raised it, never the actor that raised it, and only a named actor when the policy requires one.

func Deadline

func Deadline(now time.Time, spec Spec, callTimeout time.Duration) time.Time

Deadline returns when an intervention opened at now must be resolved by. a per-call timeout can only shorten the spec's timeout, never extend it.

func Expired

func Expired(now, deadline time.Time) bool

Expired reports whether a pending intervention has passed its deadline.

func GrantKey

func GrantKey(gate string, call any, key []byte) string

GrantKey returns the key a grant matches on: a keyed hash of the gate and the action (the exact argv, or the lifecycle action), so the grants table never holds argv in the clear. key may be nil in dev mode.

func Grants

func Grants(scopes []Scope, want Scope) bool

Grants reports whether a key holding scopes may act with want. scopes nil is a full-access key, which holds read (it could already read interventions) but never decide, secret, takeover or act-as: those have to be minted on purpose, so the agent's own key cannot approve its own requests, and takeover, which opens a shell in the guest, stays as rare as attach, which is master-only. decide and secret imply read, since deciding means seeing what is being decided.

func NewID

func NewID(now time.Time) string

NewID returns an intervention id that sorts by creation time, so listing by id is listing oldest first.

func Program

func Program(argv []string) string

Program returns the basename of argv[0], or "" for an empty argv.

func Sign

func Sign(secret []byte, ts time.Time, body []byte) string

Sign returns the Fuse-Signature header value for body at time ts.

func StripWrappers

func StripWrappers(argv []string) []string

StripWrappers removes known wrappers (env and its VAR=x args, sudo, nice, timeout, xargs, nohup, stdbuf) from the front of argv, so the program a gate sees is the one that actually runs. a wrapper with nothing after it is left alone: it is the program.

func Transition

func Transition(id string, from, to State) error

Transition checks that from -> to is legal. the only legal moves are pending -> one of the terminal states.

func ValidateActor

func ValidateActor(id, name string) error

ValidateActor checks an actor id and name, as pinned on a key or sent in the headers.

func ValidateEditedText added in v0.39.0

func ValidateEditedText(in Intervention, text string) error

ValidateEditedText checks a human's replacement text for a held type.

func ValidateReason

func ValidateReason(reason string) error

ValidateReason bounds a decision reason.

func ValidateSecretFile

func ValidateSecretFile(p string) error

ValidateSecretFile checks a file target is an absolute, clean guest path outside the reserved directory.

func ValidateSecretMessage

func ValidateSecretMessage(msg string) error

ValidateSecretMessage bounds the agent's text on a secret request.

func ValidateSecretValue

func ValidateSecretValue(v string) error

ValidateSecretValue bounds a value before it is written into the guest.

func ValidateTakeoverReason

func ValidateTakeoverReason(reason string) error

ValidateTakeoverReason bounds the agent's reason.

func ValidateTimeout

func ValidateTimeout(d time.Duration) error

ValidateTimeout checks a deadline duration against the bounds.

func ValidateWebhookURL

func ValidateWebhookURL(raw string) error

ValidateWebhookURL checks a notify target is an absolute http(s) url.

func VerifyChain

func VerifyChain(entries []AuditEntry) error

VerifyChain checks a full audit log, oldest first. it returns a *ChainError at the first row whose content no longer matches its hash, or whose prev_hash does not match the row before it (an edit, a deletion or a reorder).

func VerifySignature

func VerifySignature(secret []byte, header string, body []byte, now time.Time, tolerance time.Duration) error

VerifySignature checks a Fuse-Signature header against body. tolerance bounds how old the timestamp may be, so a captured delivery cannot be replayed later; zero skips the age check.

Types

type Actor

type Actor struct {
	ID     string      `json:"id"`
	Name   string      `json:"name,omitempty"`
	Source ActorSource `json:"source"`
	KeyID  string      `json:"key_id,omitempty"`
}

Actor is a plain label for who raised or decided an intervention. fuse records it and never checks it.

func DeciderActor added in v0.39.0

func DeciderActor(gate string) Actor

DeciderActor is who a decider's decision is recorded as.

func ParseActorHeaders

func ParseActorHeaders(id, name string) (*Actor, error)

ParseActorHeaders reads the actor a caller named. it returns nil when neither header is set.

func ResolveActor

func ResolveActor(c Credential, named *Actor) (Actor, error)

ResolveActor decides who a request is recorded as, from the credential and the actor the caller named (nil when none):

  • a key with a pinned actor is that actor; naming anyone else is refused
  • a key with hitl:act-as is whoever it names, recorded as asserted by it
  • any other key is key:<id>, and naming an actor is refused
  • the master token is whoever it names, else "master"

func (Actor) Named

func (a Actor) Named() bool

Named reports whether the actor is a person someone vouched for (pinned on a key or asserted), rather than a label derived from a credential.

type ActorSource

type ActorSource string

ActorSource says how an actor was established.

const (
	// ActorPinned is an actor fixed on the authenticating key at mint time.
	ActorPinned ActorSource = "pinned"
	// ActorAsserted is an actor named by the caller in the Fuse-Actor
	// header, trusted because the key holds hitl:act-as (or is master).
	ActorAsserted ActorSource = "asserted"
)
const (
	// ActorKey is derived from the authenticating credential.
	ActorKey ActorSource = "key"
	// ActorSystem is fuse itself (deadline expiry, cancel on destroy).
	ActorSystem ActorSource = "system"
)
const ActorDecider ActorSource = "decider"

ActorDecider marks a decision a decider made.

type AskPayload

type AskPayload struct {
	Question string   `json:"question"`
	Choices  []string `json:"choices,omitempty"`
}

AskPayload is what the agent sends with an ask request.

func (AskPayload) Validate

func (p AskPayload) Validate() error

Validate checks an ask payload.

func (AskPayload) ValidateAnswer

func (p AskPayload) ValidateAnswer(answer string) error

ValidateAnswer checks an approval's answer against the ask it answers.

type AskRequests

type AskRequests struct{}

AskRequests configures `ask` requests. it has no fields yet; declaring the block is what allows the kind.

type AuditEntry

type AuditEntry struct {
	Seq            int64      `json:"seq"`
	Event          AuditEvent `json:"event"`
	InterventionID string     `json:"intervention_id,omitempty"`
	EnvID          string     `json:"env_id,omitempty"`
	Actor          Actor      `json:"actor"`
	RemoteAddr     string     `json:"remote_addr,omitempty"`
	// RecordingID links a takeover session's encrypted cast. empty for
	// every other event.
	RecordingID string    `json:"recording_id,omitempty"`
	At          time.Time `json:"at"`
	PrevHash    []byte    `json:"prev_hash"`
	Hash        []byte    `json:"hash"`
}

AuditEntry is one row of the hitl audit log. it holds who did what and when, never content: payloads stay on the encrypted intervention row.

each row carries the hash of the row before it, so editing or deleting a row breaks the chain at that point and VerifyChain reports where.

func (AuditEntry) ComputeHash

func (e AuditEntry) ComputeHash() []byte

ComputeHash returns the hash of the entry's content and prev_hash. every field is length-prefixed so no two different rows encode the same bytes.

type AuditEvent

type AuditEvent string

AuditEvent is what an audit row records.

const (
	AuditRaised       AuditEvent = "raised"
	AuditDecided      AuditEvent = "decided"
	AuditExpired      AuditEvent = "expired"
	AuditCancelled    AuditEvent = "cancelled"
	AuditInviteMinted AuditEvent = "invite_minted"
	AuditInviteUsed   AuditEvent = "invite_used"
	// AuditGrantCreated is an approval that stored an env or ttl grant.
	AuditGrantCreated AuditEvent = "grant_created"
	// AuditGrantUsed is an action a grant let through without a prompt.
	AuditGrantUsed AuditEvent = "grant_used"
)
const (
	AuditSecretAnswered  AuditEvent = "secret_answered"
	AuditTakeoverStarted AuditEvent = "takeover_started"
	AuditTakeoverEnded   AuditEvent = "takeover_ended"
	// AuditTakeoverSession closes one recorded shell session; its row links
	// the encrypted cast by RecordingID.
	AuditTakeoverSession AuditEvent = "takeover_session"
)

audit events for secrets and takeover (#312).

type ChainError

type ChainError struct {
	Seq    int64
	Reason string
}

ChainError says where an audit chain stops verifying.

func (*ChainError) Error

func (e *ChainError) Error() string

type ComputerCall added in v0.39.0

type ComputerCall struct {
	Action string
	Text   string
}

ComputerCall is a computer action as the gates see it: the action name and, for type, the text.

type ComputerPayload added in v0.39.0

type ComputerPayload struct {
	Action string          `json:"action"`
	Text   string          `json:"text,omitempty"`
	Input  json.RawMessage `json:"input"`
}

ComputerPayload is a held computer action. Input is the action body as the caller sent it: the action schema belongs to fused, so fuse replays it verbatim and only ever rewrites the text of a type.

func ParseComputerInput added in v0.39.0

func ParseComputerInput(body []byte) (ComputerPayload, error)

ParseComputerInput reads the fields a gate matches on out of an action body.

type ComputerResult added in v0.39.0

type ComputerResult struct {
	Output     string `json:"output,omitempty"`
	Screenshot string `json:"screenshot,omitempty"`
}

ComputerResult is the guest's answer to an approved computer action.

type Credential

type Credential struct {
	Master bool
	KeyID  string
	// Label is the key's label, recorded as the name of a key actor.
	Label string
	// Pinned is the actor fixed on the key, nil when none.
	Pinned *Actor
	// ActAs reports whether the key holds hitl:act-as.
	ActAs bool
}

Credential is what actor resolution needs to know about how a request authenticated.

type Decider added in v0.39.0

type Decider struct {
	URL    string `json:"url"`
	Secret string `json:"secret"`
}

Decider is a gate's decide webhook. Secret names an environment secret whose value signs each call (the Fuse-Signature header, as for notify).

type DeciderLifecycle added in v0.39.0

type DeciderLifecycle struct {
	Action LifecycleAction `json:"action"`
	Params json.RawMessage `json:"params,omitempty"`
}

DeciderLifecycle is a held lifecycle action as a decider sees it: the params as json, not the base64 a []byte would marshal to.

func NewDeciderLifecycle added in v0.39.0

func NewDeciderLifecycle(p *LifecyclePayload) *DeciderLifecycle

NewDeciderLifecycle converts a held lifecycle payload for a decider.

type DeciderNote added in v0.39.0

type DeciderNote struct {
	Verdict DeciderVerdict `json:"verdict"`
	Reason  string         `json:"reason,omitempty"`
	// Error is set when the call failed and fuse fell through to ask.
	Error string `json:"error,omitempty"`
}

DeciderNote records what the decider said about an action it sent on to a human: it escalated (ask), or it failed and fuse fell through.

type DeciderRequest added in v0.39.0

type DeciderRequest struct {
	EnvID       string            `json:"env_id"`
	Gate        string            `json:"gate"`
	On          Kind              `json:"on"`
	TriggeredBy Actor             `json:"triggered_by"`
	Exec        *ExecPayload      `json:"exec,omitempty"`
	Lifecycle   *DeciderLifecycle `json:"lifecycle,omitempty"`
	Computer    *ComputerPayload  `json:"computer,omitempty"`
	// Screenshot is a base64 png of the display, for a computer action. it
	// is empty when one could not be taken in time.
	Screenshot string    `json:"screenshot,omitempty"`
	Deadline   time.Time `json:"deadline"`
}

DeciderRequest is the body fuse posts to a decider. it carries the action itself, unlike a notify delivery: judging it is the point.

type DeciderResponse added in v0.39.0

type DeciderResponse struct {
	Decision DeciderVerdict `json:"decision"`
	// Reason goes back to the agent on a deny, and to the human on an ask.
	Reason string `json:"reason,omitempty"`
}

DeciderResponse is what a decider answers.

func ParseDeciderResponse added in v0.39.0

func ParseDeciderResponse(body []byte) (DeciderResponse, error)

ParseDeciderResponse reads a decider's answer. an unknown decision is an error, which the caller turns into ask.

type DeciderVerdict added in v0.39.0

type DeciderVerdict string

DeciderVerdict is a decider's answer.

const (
	DeciderAllow DeciderVerdict = "allow"
	DeciderDeny  DeciderVerdict = "deny"
	DeciderAsk   DeciderVerdict = "ask"
)

type Decision

type Decision struct {
	Outcome Outcome
	// Answer is the human's reply to an ask. empty unless approved.
	Answer string
	// Reason is text fed back to the agent. always set on a reject.
	Reason    string
	DecidedBy Actor
	DecidedAt time.Time
	// EditedArgv replaces a held exec's argv, set when the human edited it.
	EditedArgv []string
	// EditedText replaces a held type action's text.
	EditedText *string
	// Grant is how far the approval reaches: once, env or ttl:<duration>.
	Grant string
}

Decision is how an intervention was resolved.

type ErrResolved

type ErrResolved struct {
	ID    string
	State State
}

ErrResolved is returned for any transition out of a terminal state.

func (*ErrResolved) Error

func (e *ErrResolved) Error() string

type ExecCall

type ExecCall struct {
	// Argv is what the guest would run. a shell exec is ["sh", "-lc", script].
	Argv []string
	// Shell is true when the caller sent an opaque shell string.
	Shell bool
}

ExecCall is an exec as the gates see it.

type ExecPayload

type ExecPayload struct {
	Argv []string `json:"argv"`
	// Shell is the script when the caller sent shell:, in which case Argv
	// is the sh -lc that runs it.
	Shell     string `json:"shell,omitempty"`
	TimeoutMS int64  `json:"timeout_ms,omitempty"`
}

ExecPayload is a held exec.

type ExecPolicy

type ExecPolicy struct {
	// Default is ask or deny.
	Default Mode `json:"default"`
	// Allow lists program basenames that run without a gate.
	Allow []string `json:"allow"`
}

ExecPolicy is allowlist mode: an exec whose program is not in Allow is held with Default, interpreters and shells included.

type ExecResult

type ExecResult struct {
	ExitCode int    `json:"exit_code"`
	Stdout   string `json:"stdout"`
	Stderr   string `json:"stderr"`
}

ExecResult is the guest's answer to an approved exec.

type Gate

type Gate struct {
	// Name identifies the gate on interventions and grants.
	Name string `json:"name"`
	// On is the surface: exec or lifecycle.
	On Kind `json:"on"`
	// Match selects the actions the gate holds.
	Match GateMatch `json:"match"`
	// Mode is ask (the default), notify or deny.
	Mode Mode `json:"mode,omitempty"`
	// Grant is the default grant an approval creates: once (the default),
	// env, or ttl:<duration>. only meaningful for ask.
	Grant string `json:"grant,omitempty"`
	// TimeoutSeconds overrides the spec's deadline for this gate.
	TimeoutSeconds int64 `json:"timeout_seconds,omitempty"`
	// Reason is the text a deny returns, and is shown to the human on ask.
	Reason string `json:"reason,omitempty"`
	// Decide is a webhook asked before a human: it answers allow, deny or
	// ask. only for an ask gate on exec, lifecycle or computer.
	Decide *Decider `json:"decide,omitempty"`
}

Gate is one enforced gate from the fusefile.

func (Gate) DenyReason

func (g Gate) DenyReason() string

DenyReason is the text a deny of this gate returns.

func (Gate) EffectiveMode

func (g Gate) EffectiveMode() Mode

EffectiveMode returns the gate's mode with its default applied.

func (Gate) Timeout

func (g Gate) Timeout(spec Spec) time.Duration

Timeout returns the gate's deadline duration, falling back to the spec.

type GateMatch

type GateMatch struct {
	// Program matches the basename of argv[0] after known wrappers are
	// stripped, against any entry of the list.
	Program []string `json:"program,omitempty"`
	// Args matches when they appear in argv[1:] in order, not
	// necessarily next to each other.
	Args []string `json:"args,omitempty"`
	// Shell matches any opaque shell exec.
	Shell bool `json:"shell,omitempty"`
	// Action matches lifecycle actions (on: lifecycle) or fused's computer
	// actions (on: computer).
	Action []string `json:"action,omitempty"`
	// TextContains matches a computer type action whose text contains any
	// entry, ignoring case.
	TextContains []string `json:"text_contains,omitempty"`
	// Failures fires a stall after this many consecutive non-zero exec
	// exits.
	Failures int `json:"failures,omitempty"`
	// QuietSeconds fires a stall after this long with no exec or computer
	// call.
	QuietSeconds int64 `json:"quiet_seconds,omitempty"`
	// BeforeSeconds fires an expiry this long ahead of the idle_timeout or
	// max_runtime reap.
	BeforeSeconds int64 `json:"before_seconds,omitempty"`
}

GateMatch is the structured match. every field set must match.

type Grant

type Grant struct {
	ID    string
	EnvID string
	Gate  string
	// Key identifies the action, see GrantKey. it never holds the argv.
	Key            string
	Scope          GrantScope
	InterventionID string
	ExpiresAt      *time.Time
	CreatedAt      time.Time
}

Grant is a stored approval that covers repeats of the same action on the same gate.

func NewGrant

func NewGrant(in Intervention, spec GrantSpec, key string, now time.Time) (Grant, bool)

NewGrant builds the grant an approval creates. it returns false for a once grant, which is never stored.

func (Grant) Active

func (g Grant) Active(now time.Time) bool

Active reports whether the grant still covers actions at now.

type GrantScope

type GrantScope string

GrantScope is how long an approval keeps covering repeats.

const (
	// GrantOnce covers only the approved action. no grant row is written.
	GrantOnce GrantScope = "once"
	// GrantEnv covers repeats for the environment's lifetime.
	GrantEnv GrantScope = "env"
	// GrantTTL covers repeats for a fixed duration.
	GrantTTL GrantScope = "ttl"
)

type GrantSpec

type GrantSpec struct {
	Scope GrantScope
	TTL   time.Duration
}

GrantSpec is a parsed grant: once, env or ttl:<duration>.

func ParseGrant

func ParseGrant(raw string) (GrantSpec, error)

ParseGrant reads once, env or ttl:<duration>. empty is once.

func (GrantSpec) String

func (g GrantSpec) String() string

String is the inverse of ParseGrant.

type Intervention

type Intervention struct {
	ID    string
	EnvID string
	Kind  Kind
	State State
	// Gate names the gate that raised it; empty for a request.
	Gate string
	// Mode is the gate's mode; empty for a request.
	Mode Mode
	// Payload is the ask, for KindAsk.
	Payload AskPayload
	// Exec is the held exec, for KindExec.
	Exec *ExecPayload
	// Lifecycle is the held environment action, for KindLifecycle.
	Lifecycle *LifecyclePayload
	// Computer is the held computer action, for KindComputer.
	Computer *ComputerPayload
	// Secret, Takeover and Review are the payloads of those kinds; nil
	// otherwise.
	Secret   *SecretPayload
	Takeover *TakeoverPayload
	Review   *ReviewPayload
	// Decider is what the gate's decider said when it sent the action on
	// to a human, nil when the gate has none or it decided on its own.
	Decider *DeciderNote
	// Signal is what fuse saw, for KindStall and KindExpiry.
	Signal      *SignalPayload
	TriggeredBy Actor
	Deadline    time.Time
	OnTimeout   OnTimeout
	Decision    *Decision
	// Result is what the orchestrator got running the approved action.
	Result    *Result
	CreatedAt time.Time
	UpdatedAt time.Time
}

Intervention is one request for a human. Payload, Answer and Reason hold content and are encrypted at rest by the orchestrator; the rest is metadata the sweeper and list filters read in the clear.

func AnswerSecret

func AnswerSecret(in Intervention, by Actor, now time.Time) (Intervention, error)

AnswerSecret resolves a pending secret request once its value is in the guest. the decision records who answered and when, and the agent's answer is always SecretAnswer: the value itself never reaches the record.

func Cancel

func Cancel(in Intervention, reason string, now time.Time) (Intervention, error)

Cancel resolves a pending intervention that can no longer be answered, such as one whose environment was destroyed.

func Decide

func Decide(in Intervention, outcome Outcome, answer, reason string, by Actor, now time.Time) (Intervention, error)

Decide resolves a pending intervention with a human's decision. approving an ask must carry a valid answer; a reject always ends up with a reason.

func Expire

func Expire(in Intervention, now time.Time) (Intervention, error)

Expire resolves a pending intervention whose deadline passed, applying its on_timeout.

func Extend

func Extend(in Intervention, by time.Duration, decider Actor, reason string, now time.Time) (Intervention, error)

Extend approves a pending expiry, keeping the reaper off the environment until now+by.

func ReleaseTakeover

func ReleaseTakeover(in Intervention, note string, by Actor, now time.Time) (Intervention, error)

ReleaseTakeover ends a held takeover. the note is what the agent gets back, so it is the decision's answer.

func StartTakeover

func StartTakeover(in Intervention, by Actor, now time.Time) (Intervention, error)

StartTakeover marks a pending takeover as held by by. the intervention stays pending: it resolves on release, and its deadline bounds the lock.

func (Intervention) Argv

func (in Intervention) Argv() []string

Argv returns the argv an approved exec runs: the edit when there is one.

func (Intervention) ComputerInput added in v0.39.0

func (in Intervention) ComputerInput() ([]byte, error)

ComputerInput returns the action body an approved computer action runs: the original, with the text replaced when the human edited it.

func (Intervention) Locks

func (in Intervention) Locks() bool

Locks reports whether a pending intervention keeps the agent out of exec and computer calls: a held takeover does, and so does a stall in ask mode until a human acks it.

func (Intervention) Runs

func (in Intervention) Runs() bool

Runs reports whether fuse performs the action itself once approved: an ask gate holding an exec, a lifecycle or a computer action. a decider's allow is the exception: the caller goes on and performs the action itself, as if no gate had matched.

func (Intervention) Settled

func (in Intervention) Settled() bool

Settled reports whether a waiting caller has its final answer: resolved, and when fuse runs the action on approval, run.

type Kind

type Kind string

Kind is what raised an intervention: the ask request, or a gate surface (see gate.go).

const (
	// KindExec is an sdk/cli exec held by an exec gate.
	KindExec Kind = "exec"
	// KindLifecycle is an environment api action held by a lifecycle gate.
	KindLifecycle Kind = "lifecycle"
	// KindComputer is a computer action held by a computer gate.
	KindComputer Kind = "computer"
)
const (
	KindSecret   Kind = "secret"
	KindTakeover Kind = "takeover"
)

kinds added for secrets, takeover and signals (#312). kept in their own block so the gate kinds (#311) merge beside them cleanly.

const (
	KindStall  Kind = "stall"
	KindExpiry Kind = "expiry"
)

signal kinds: fuse noticing on its own that the agent looks stuck, or that the environment is about to be reaped. declared as gates with `on: stall` and `on: expiry`.

const (
	KindAsk Kind = "ask"
)
const KindReview Kind = "review"

KindReview is a review request: the agent submits a file or a text/diff blob, and a human approves or rejects it with comments. the comments are the decision's reason, so they reach the agent either way.

type LifecycleAction

type LifecycleAction string

LifecycleAction is an environment api action a lifecycle gate can hold.

const (
	ActionDestroy     LifecycleAction = "destroy"
	ActionFork        LifecycleAction = "fork"
	ActionMigrate     LifecycleAction = "migrate"
	ActionDrain       LifecycleAction = "drain"
	ActionSnapshot    LifecycleAction = "snapshot"
	ActionRotateToken LifecycleAction = "rotate-token"
)

type LifecyclePayload

type LifecyclePayload struct {
	Action LifecycleAction `json:"action"`
	Params []byte          `json:"params,omitempty"`
}

LifecyclePayload is a held environment action. Params is the action's options as the orchestrator will replay them.

type Mode

type Mode string

Mode is what a gate does when it matches.

const (
	// ModeAsk holds the action until a human decides.
	ModeAsk Mode = "ask"
	// ModeNotify lets the action through and records it.
	ModeNotify Mode = "notify"
	// ModeDeny blocks the action with the gate's reason, no human involved.
	ModeDeny Mode = "deny"
)

type Notification

type Notification struct {
	Event          AuditEvent `json:"event"`
	InterventionID string     `json:"intervention_id,omitempty"`
	EnvID          string     `json:"env_id"`
	Gate           string     `json:"gate"`
	On             string     `json:"on"`
	Deadline       *time.Time `json:"deadline,omitempty"`
	URL            string     `json:"url,omitempty"`
}

Notification is the whole body of a webhook delivery. it is content-free by design: a receiver learns that something needs a look and where, never what. the human fetches the content from the api with their own key.

type OnTimeout

type OnTimeout string

OnTimeout decides how an expired intervention resolves.

const (
	OnTimeoutReject OnTimeout = "reject"
	OnTimeoutAllow  OnTimeout = "allow"
)

type Outcome

type Outcome string

Outcome is the effective result of a resolved intervention. every terminal intervention has one, including expired (from on_timeout) and cancelled (always reject), so a caller only ever has to read this.

const (
	OutcomeApprove Outcome = "approve"
	OutcomeReject  Outcome = "reject"
)

type Reaper

type Reaper string

Reaper names the window an expiry signal warns about.

const (
	ReaperIdle       Reaper = "idle_timeout"
	ReaperMaxRuntime Reaper = "max_runtime"
)

type Requests

type Requests struct {
	Ask      *AskRequests      `json:"ask,omitempty"`
	Secret   *SecretRequests   `json:"secret,omitempty"`
	Takeover *TakeoverRequests `json:"takeover,omitempty"`
	Review   *ReviewRequests   `json:"review,omitempty"`
}

Requests declares which request kinds are allowed. a nil entry is not declared and is refused at call time.

type Result

type Result struct {
	// Exec is set for an exec that ran.
	Exec *ExecResult `json:"exec,omitempty"`
	// Computer is set for a computer action that ran.
	Computer *ComputerResult `json:"computer,omitempty"`
	// EnvID is the environment a fork or migrate produced.
	EnvID string `json:"env_id,omitempty"`
	// SnapshotID is the snapshot a snapshot action produced.
	SnapshotID string `json:"snapshot_id,omitempty"`
	// Error is set when the approved action could not be performed.
	Error      string    `json:"error,omitempty"`
	FinishedAt time.Time `json:"finished_at"`
}

Result is the outcome of running an approved action.

type ReviewFormat added in v0.39.0

type ReviewFormat string

ReviewFormat says how a review's content should be shown.

const (
	ReviewText ReviewFormat = "text"
	ReviewDiff ReviewFormat = "diff"
)

type ReviewPayload added in v0.39.0

type ReviewPayload struct {
	Path    string       `json:"path,omitempty"`
	Content string       `json:"content"`
	Format  ReviewFormat `json:"format"`
	Message string       `json:"message,omitempty"`
	// Truncated is set when a file was longer than MaxReviewBytes.
	Truncated bool `json:"truncated,omitempty"`
}

ReviewPayload is what is up for review. Path is set when the agent named a guest file; Content is then that file as fuse read it at request time, so the human reviews exactly what the agent submitted.

func ValidateReviewRequest added in v0.39.0

func ValidateReviewRequest(p ReviewPayload) (ReviewPayload, error)

ValidateReviewRequest checks what the agent sent: exactly one of a path and inline content, a known format, and a bounded message.

type ReviewRequests added in v0.39.0

type ReviewRequests struct{}

ReviewRequests configures review requests. it has no fields yet; declaring the block is what allows the kind.

type Scope

type Scope string

Scope is a permission an api key can be minted with. a key with no scopes (null in the store) is a full-access key, as every key was before hitl.

const (
	// ScopeRead lists and reads interventions and the audit log.
	ScopeRead Scope = "hitl:read"
	// ScopeDecide approves and rejects interventions.
	ScopeDecide Scope = "hitl:decide"
	// ScopeSecret answers secret requests.
	ScopeSecret Scope = "hitl:secret"
	// ScopeTakeover takes over an environment (shell access to the vm).
	ScopeTakeover Scope = "hitl:takeover"
	// ScopeActAs lets the key name the actor it acts for.
	ScopeActAs Scope = "hitl:act-as"
)

func ParseScopes

func ParseScopes(raw []string) ([]Scope, error)

ParseScopes checks a scope list and returns it deduplicated in display order.

type SecretPayload

type SecretPayload struct {
	Target  string `json:"target"`
	Label   string `json:"label"`
	Message string `json:"message,omitempty"`
}

SecretPayload is what the agent sends with a secret request, plus the declared label copied in at raise time. it never holds the value.

type SecretRequests

type SecretRequests struct {
	Targets []SecretTarget `json:"targets"`
}

SecretRequests configures `secret` requests: the only places a value may land. the agent names one of these targets; it can never pick a path or variable of its own, because it also writes the prompt the human reads.

func (*SecretRequests) Target

func (r *SecretRequests) Target(name string) (SecretTarget, bool)

Target returns the declared target the agent named, if any.

func (SecretRequests) Validate

func (r SecretRequests) Validate() error

Validate checks the declared targets.

type SecretTarget

type SecretTarget struct {
	Env   string `json:"env,omitempty"`
	File  string `json:"file,omitempty"`
	Label string `json:"label"`
}

SecretTarget is one declared destination. exactly one of Env and File is set. Label is what the human sees first, ahead of the agent's text.

func (SecretTarget) Name

func (t SecretTarget) Name() string

Name is how the agent refers to the target: the variable name or the file path.

type SignalPayload

type SignalPayload struct {
	// Failures is the run of consecutive non-zero exec exits (stall).
	Failures int `json:"failures,omitempty"`
	// LastExitCode is the exit code that completed the run (stall).
	LastExitCode int `json:"last_exit_code,omitempty"`
	// QuietSeconds is how long the agent made no exec or computer call
	// (stall).
	QuietSeconds int64 `json:"quiet_seconds,omitempty"`
	// Reaper and ReapAt say what will tear the environment down, and when
	// (expiry).
	Reaper Reaper     `json:"reaper,omitempty"`
	ReapAt *time.Time `json:"reap_at,omitempty"`
	// ExtendedUntil is set when a human approved an expiry: the reaper
	// leaves the environment alone until then.
	ExtendedUntil *time.Time `json:"extended_until,omitempty"`
}

SignalPayload is what fuse saw when it raised a signal.

type Spec

type Spec struct {
	// TimeoutSeconds is the default deadline for every intervention. zero
	// means DefaultTimeout.
	TimeoutSeconds int64 `json:"timeout_seconds,omitempty"`
	// OnTimeout is how an expired intervention resolves. empty means reject.
	OnTimeout OnTimeout `json:"on_timeout,omitempty"`
	// Requests are the request kinds the agent may raise.
	Requests Requests `json:"requests"`
	// RequireActor refuses decisions that do not carry a pinned or asserted
	// actor, so every approval is tied to a named person.
	RequireActor bool `json:"require_actor,omitempty"`
	// Notify are webhooks told about every intervention and audit event.
	Notify []Webhook `json:"notify,omitempty"`
	// Gates are the enforced exec, lifecycle, computer and signal gates.
	Gates []Gate `json:"gates,omitempty"`
	// Exec is allowlist mode for exec, nil when off.
	Exec *ExecPolicy `json:"exec,omitempty"`
}

Spec is the hitl policy an environment is created with. it is compiled from the fusefile `hitl:` block, carried on the create request, stored on the vm and inherited by forks and migrates. nil means hitl is off.

func (Spec) Allows

func (s Spec) Allows(k Kind) bool

Allows reports whether the spec declares the given request kind.

func (Spec) Deciders added in v0.39.0

func (s Spec) Deciders() []Decider

Deciders returns every decider the gates name, for resolving their signing secrets.

func (Spec) EffectiveOnTimeout

func (s Spec) EffectiveOnTimeout() OnTimeout

EffectiveOnTimeout returns the on_timeout with its default applied.

func (Spec) EvaluateComputer added in v0.39.0

func (s Spec) EvaluateComputer(call ComputerCall) Verdict

EvaluateComputer runs a computer action through the computer gates.

func (Spec) EvaluateExec

func (s Spec) EvaluateExec(call ExecCall) Verdict

EvaluateExec runs an exec through the spec's exec gates and allowlist.

func (Spec) EvaluateLifecycle

func (s Spec) EvaluateLifecycle(action LifecycleAction) Verdict

EvaluateLifecycle runs an environment action through the lifecycle gates.

func (Spec) FindGate

func (s Spec) FindGate(name string) (Gate, bool)

FindGate returns the named gate, including the synthetic allowlist gate.

func (Spec) SignalGates

func (s Spec) SignalGates(k Kind) []Gate

SignalGates returns the gates declared for one signal kind.

func (Spec) Timeout

func (s Spec) Timeout() time.Duration

Timeout returns the effective default deadline.

func (Spec) Validate

func (s Spec) Validate() error

Validate checks the spec's invariants. the fusefile compiler applies the same rules client-side; this is the copy a raw api caller is held to.

type State

type State string

State is the lifecycle of a single intervention. it starts pending and moves to exactly one terminal state. the move is one way: a resolved intervention is never re-opened.

const (
	StatePending   State = "pending"
	StateApproved  State = "approved"
	StateRejected  State = "rejected"
	StateExpired   State = "expired"
	StateCancelled State = "cancelled"
)

func (State) Terminal

func (s State) Terminal() bool

Terminal reports whether s is final.

func (State) Valid

func (s State) Valid() bool

Valid reports whether s is one of the five known states.

type TakeoverPayload

type TakeoverPayload struct {
	Via       TakeoverVia `json:"via"`
	Reason    string      `json:"reason,omitempty"`
	Holder    *Actor      `json:"holder,omitempty"`
	StartedAt *time.Time  `json:"started_at,omitempty"`
}

TakeoverPayload is a takeover's request and its live state. Holder and StartedAt are set once a human has taken control; until then the environment is not locked.

func (*TakeoverPayload) Held

func (p *TakeoverPayload) Held() bool

Held reports whether a human holds control right now.

type TakeoverRequests

type TakeoverRequests struct {
	Via []TakeoverVia `json:"via,omitempty"`
}

TakeoverRequests configures `takeover` requests. Via lists the allowed ways in; empty means terminal only.

func (*TakeoverRequests) Allows

func (r *TakeoverRequests) Allows(v TakeoverVia) bool

Allows reports whether a way in is declared.

func (TakeoverRequests) Validate

func (r TakeoverRequests) Validate() error

Validate checks the allowed ways in. port is designed but not built yet. that desktop has a desktop block to take over is checked at create, where the block is known.

type TakeoverVia

type TakeoverVia string

TakeoverVia is how a human takes over an environment.

const (
	// ViaTerminal gives the human their own shell on the attach path.
	ViaTerminal TakeoverVia = "terminal"
	// ViaDesktop gives the human the live desktop view. it needs the
	// environment to have a desktop block.
	ViaDesktop TakeoverVia = "desktop"
)

type Verdict

type Verdict struct {
	// Deny is the first matching deny gate, nil when none matched.
	Deny *Gate
	// Ask are the matching ask gates, in declaration order.
	Ask []Gate
	// Notify are the matching notify gates, in declaration order.
	Notify []Gate
}

Verdict is every gate that matched one action, split by mode.

func (Verdict) Mode

func (v Verdict) Mode() Mode

Mode returns the strictest mode that matched, or "" when nothing did.

type Webhook

type Webhook struct {
	URL    string `json:"url"`
	Secret string `json:"secret"`
}

Webhook is one notification target. Secret names an entry of the environment's secrets whose value signs each delivery; the value itself never appears in the spec.

Jump to

Keyboard shortcuts

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