fabric/approvers

Who may answer an approval request, and the proof that someone was checked.

An agent (agent.with_approvers) or a graph runtime (graph.with_approvers) is given its Approvers once: a name and a verify function that turns a credential (a bearer token, a session) into the Reviewer it authenticates, after checking that the credential may answer the request’s run.Requirement. A request handler calls check with the credential it received and the pending request’s requirement, and passes the Proof it returns to fabric.approve, fabric.reject, graph.approve or graph.reject. Only check makes a Proof, and an answer refuses a proof that:

An agent or runtime without approvers refuses every answer (NoApprovers). The answer records the reviewer and the approvers’ name (run.Approval.reviewer, run.Approval.verifier).

let desk_approvers = hellhound_approvers(validator)  // the recipe below
let assert Ok(desk) =
  agent.new("desk", model, tools, policy)
  |> agent.with_approvers(desk_approvers)
  |> agent.build
// In the request handler that answers `pending`:
case approvers.check(desk_approvers, bearer, pending.reference.requirement) {
  Ok(proof) -> fabric.approve(handle, pending.reference, proof:, context:)
  Error(approvers.NotAuthenticated(_)) -> todo  // 401
  Error(approvers.NotAuthorized(_)) -> todo     // 403
  Error(approvers.Unavailable(_)) -> todo       // 503
}

Each call of new makes distinct approvers, even with the same name and function: a proof is accepted only by an agent or runtime given the very value that made it. Build the approvers once, at boot, and pass that value to the agent and to the request handlers. Tests that do not exercise authentication use fabric/testing.trusting_approvers(), whose credential is the reviewer itself.

With hellhound

A hellhound access token must be valid for the application’s audience and carry the scope approve:<requirement name>. Copy the module approvers_hellhound of consumers/approvers_hellhound into the application; that consumer compiles it and tests it against hellhound’s test issuer. Its function hellhound_approvers(validator) builds approvers named "hellhound": they verify the token with hellhound/resource.verify, require the scope, and answer with a reviewer made of the token’s sub and iss. A rejected or wrong-audience token is NotAuthenticated, a forbidden one NotAuthorized and an unavailable validator Unavailable. Call it once, at boot: the agent and the request handlers share the value.

Types

A verifier of credentials that answer approval requests. Make one with new.

pub type Approvers(credential) =
  @internal Approvers(credential, Denial)

Why verify refused a credential. The three cases are the classification: an application maps them to 401, 403 and 503.

pub type Denial {
  NotAuthenticated(reason: String)
  NotAuthorized(reason: String)
  Unavailable(reason: String)
}

Constructors

  • NotAuthenticated(reason: String)

    The credential proves no identity: missing, malformed, forged, expired, issued for another audience.

  • NotAuthorized(reason: String)

    The identity is authenticated but may not answer this requirement.

  • Unavailable(reason: String)

    The verifier could not decide: its key source or identity provider is unreachable. Trying again later may succeed.

Evidence that check verified a credential for one requirement. Only check makes one.

pub type Proof =
  @internal Proof

Why an answer refused a proof. This union may grow: keep a catch-all, or use describe_proof_error.

pub type ProofError {
  NoApprovers
  OtherApprovers(verifier: String)
  OtherRequirement(
    proof: run.Requirement,
    request: run.Requirement,
  )
  ProofExpired(age: Int, lifetime: Int)
}

Constructors

  • NoApprovers

    The agent or graph runtime has no approvers (agent.with_approvers, graph.with_approvers), so it accepts no answer.

  • OtherApprovers(verifier: String)

    The proof was made by other approvers than the agent’s or runtime’s; verifier is their name.

  • OtherRequirement(
      proof: run.Requirement,
      request: run.Requirement,
    )

    The proof was checked for proof, and the request waits for request.

  • ProofExpired(age: Int, lifetime: Int)

    The proof is older than the proof lifetime (with_proof_lifetime): age and lifetime in milliseconds.

Values

pub fn check(
  approvers: @internal Approvers(credential, Denial),
  credential: credential,
  requirement: run.Requirement,
) -> Result(Proof, Denial)

Verifies credential for an answer to a request that waits for requirement (run.PendingApproval.reference.requirement, graph.ApprovalRef.requirement), and returns the proof to answer it with.

pub fn describe_denial(denial: Denial) -> String

One line for logs.

pub fn describe_proof_error(error: ProofError) -> String

One line for logs, naming the setter where one fixes it.

pub const max_name_bytes: Int

The longest name, in bytes.

pub fn name(
  approvers: @internal Approvers(credential, Denial),
) -> String
pub fn new(
  name: String,
  verify: fn(credential, run.Requirement) -> Result(
    reviewer.Reviewer,
    Denial,
  ),
) -> @internal Approvers(credential, Denial)

Approvers named name (stored with every answer they verify, as run.Approval.verifier), which verify a credential with verify.

verify receives the credential and the requirement of the request it would answer, and returns the authenticated reviewer, or why it refused. It decides authorization from the requirement, for example by asking for the scope "approve:" <> requirement.name. It runs in the process that calls check, which waits for it: bound its network calls (a key fetch) with their own timeouts.

name is written in source code, so it is checked here: 1 to 64 bytes, or this panics.

pub fn requirement(proof: Proof) -> run.Requirement

The requirement the proof was checked for.

pub fn reviewer(proof: Proof) -> reviewer.Reviewer

The reviewer verify returned.

pub fn verifier(proof: Proof) -> String

The name of the approvers that made the proof.

pub fn with_proof_lifetime(
  approvers: @internal Approvers(credential, Denial),
  lifetime: run.Timeout,
) -> @internal Approvers(credential, Denial)

How long a proof from check stays good for an answer, judged by the approvers of the agent or runtime that receives it. A proof is made for one answer, right before it; one kept longer is refused with ProofExpired, so that a credential revoked or expired since is not honoured. Default 60 s; run.Infinity never expires a proof. The approvers it returns are the same approvers, with another lifetime.

✨ Search Document