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
- Variables
- func AuditTime(t time.Time) time.Time
- func CheckDecider(triggeredBy, decider Actor, requireActor bool) error
- func Deadline(now time.Time, spec Spec, callTimeout time.Duration) time.Time
- func Expired(now, deadline time.Time) bool
- func GrantKey(gate string, call any, key []byte) string
- func Grants(scopes []Scope, want Scope) bool
- func NewID(now time.Time) string
- func Program(argv []string) string
- func Sign(secret []byte, ts time.Time, body []byte) string
- func StripWrappers(argv []string) []string
- func Transition(id string, from, to State) error
- func ValidateActor(id, name string) error
- func ValidateEditedText(in Intervention, text string) error
- func ValidateReason(reason string) error
- func ValidateSecretFile(p string) error
- func ValidateSecretMessage(msg string) error
- func ValidateSecretValue(v string) error
- func ValidateTakeoverReason(reason string) error
- func ValidateTimeout(d time.Duration) error
- func ValidateWebhookURL(raw string) error
- func VerifyChain(entries []AuditEntry) error
- func VerifySignature(secret []byte, header string, body []byte, now time.Time, ...) error
- type Actor
- type ActorSource
- type AskPayload
- type AskRequests
- type AuditEntry
- type AuditEvent
- type ChainError
- type ComputerCall
- type ComputerPayload
- type ComputerResult
- type Credential
- type Decider
- type DeciderLifecycle
- type DeciderNote
- type DeciderRequest
- type DeciderResponse
- type DeciderVerdict
- type Decision
- type ErrResolved
- type ExecCall
- type ExecPayload
- type ExecPolicy
- type ExecResult
- type Gate
- type GateMatch
- type Grant
- type GrantScope
- type GrantSpec
- type Intervention
- func AnswerSecret(in Intervention, by Actor, now time.Time) (Intervention, error)
- func Cancel(in Intervention, reason string, now time.Time) (Intervention, error)
- func Decide(in Intervention, outcome Outcome, answer, reason string, by Actor, ...) (Intervention, error)
- func Expire(in Intervention, now time.Time) (Intervention, error)
- func Extend(in Intervention, by time.Duration, decider Actor, reason string, now time.Time) (Intervention, error)
- func ReleaseTakeover(in Intervention, note string, by Actor, now time.Time) (Intervention, error)
- func StartTakeover(in Intervention, by Actor, now time.Time) (Intervention, error)
- type Kind
- type LifecycleAction
- type LifecyclePayload
- type Mode
- type Notification
- type OnTimeout
- type Outcome
- type Reaper
- type Requests
- type Result
- type ReviewFormat
- type ReviewPayload
- type ReviewRequests
- type Scope
- type SecretPayload
- type SecretRequests
- type SecretTarget
- type SignalPayload
- type Spec
- func (s Spec) Allows(k Kind) bool
- func (s Spec) Deciders() []Decider
- func (s Spec) EffectiveOnTimeout() OnTimeout
- func (s Spec) EvaluateComputer(call ComputerCall) Verdict
- func (s Spec) EvaluateExec(call ExecCall) Verdict
- func (s Spec) EvaluateLifecycle(action LifecycleAction) Verdict
- func (s Spec) FindGate(name string) (Gate, bool)
- func (s Spec) SignalGates(k Kind) []Gate
- func (s Spec) Timeout() time.Duration
- func (s Spec) Validate() error
- type State
- type TakeoverPayload
- type TakeoverRequests
- type TakeoverVia
- type Verdict
- type Webhook
Constants ¶
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 )
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" )
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 )
const AllowlistGate = "exec.allowlist"
AllowlistGate is the name the allowlist reports on interventions.
const DeciderTimeout = 5 * time.Second
DeciderTimeout bounds one decider call. it is fixed, not configurable, because the agent's call is blocked on it.
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.
const MaxGrantTTL = 24 * time.Hour
MaxGrantTTL bounds a ttl grant.
const MaxReviewBytes = 1 << 20
MaxReviewBytes bounds a review's content, inline or read from a file.
const MaxScopedKeyTTL = 24 * time.Hour
MaxScopedKeyTTL caps the lifetime of a key minted with scopes.
const MaxWebhooks = 5
MaxWebhooks bounds hitl.notify.
const SecretAnswer = "done"
SecretAnswer is the only answer an agent ever gets back for a secret.
Variables ¶
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") )
var AllScopes = []Scope{ScopeRead, ScopeDecide, ScopeSecret, ScopeTakeover, ScopeActAs}
AllScopes is every scope, in display order.
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.
var ErrBadSignature = errors.New("webhook signature does not verify")
ErrBadSignature is a webhook signature that does not verify.
var ErrTakeoverHeld = errors.New("takeover already held")
ErrTakeoverHeld is returned when a second human tries to take over an environment someone already holds.
var GenesisHash = make([]byte, sha256.Size)
GenesisHash is the prev_hash of the first row.
var LifecycleActions = []LifecycleAction{ActionDestroy, ActionFork, ActionMigrate, ActionDrain, ActionSnapshot, ActionRotateToken}
LifecycleActions is every gateable action, in display order.
var SystemActor = Actor{ID: "system", Source: ActorSystem}
SystemActor is the actor recorded for decisions fuse makes on its own.
Functions ¶
func AuditTime ¶
AuditTime normalizes a timestamp to what postgres stores, so a hash computed before the write matches one recomputed after a read.
func CheckDecider ¶
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 ¶
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 GrantKey ¶
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 ¶
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 ¶
NewID returns an intervention id that sorts by creation time, so listing by id is listing oldest first.
func StripWrappers ¶
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 ¶
Transition checks that from -> to is legal. the only legal moves are pending -> one of the terminal states.
func ValidateActor ¶
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 ¶
ValidateReason bounds a decision reason.
func ValidateSecretFile ¶
ValidateSecretFile checks a file target is an absolute, clean guest path outside the reserved directory.
func ValidateSecretMessage ¶
ValidateSecretMessage bounds the agent's text on a secret request.
func ValidateSecretValue ¶
ValidateSecretValue bounds a value before it is written into the guest.
func ValidateTakeoverReason ¶
ValidateTakeoverReason bounds the agent's reason.
func ValidateTimeout ¶
ValidateTimeout checks a deadline duration against the bounds.
func ValidateWebhookURL ¶
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
DeciderActor is who a decider's decision is recorded as.
func ParseActorHeaders ¶
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"
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) 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 ¶
ChainError says where an audit chain stops verifying.
func (*ChainError) Error ¶
func (e *ChainError) Error() string
type ComputerCall ¶ added in v0.39.0
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
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 ¶
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 ¶
DenyReason is the text a deny of this gate returns.
func (Gate) EffectiveMode ¶
EffectiveMode returns the gate's mode with its default applied.
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.
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 ¶
ParseGrant reads once, env or ttl:<duration>. empty is once.
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).
kinds added for secrets, takeover and signals (#312). kept in their own block so the gate kinds (#311) merge beside them cleanly.
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 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 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.
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 ¶
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) Deciders ¶ added in v0.39.0
Deciders returns every decider the gates name, for resolving their signing secrets.
func (Spec) EffectiveOnTimeout ¶
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 ¶
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) SignalGates ¶
SignalGates returns the gates declared for one signal kind.
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.
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.