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:
- was made by other approvers than the agent’s or runtime’s
(
OtherApprovers), so a permissive verifier built elsewhere in the application is never accepted; - was checked for another requirement than the one the request waits
for (
OtherRequirement), so a proof for"refund"cannot answer"payout"; - is older than the approvers’ proof lifetime (
ProofExpired, 60 s by default,with_proof_lifetime): a proof is evidence of a check made for this answer, not a standing grant.
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
-
NoApproversThe 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;
verifieris their name. -
OtherRequirement( proof: run.Requirement, request: run.Requirement, )The proof was checked for
proof, and the request waits forrequest. -
ProofExpired(age: Int, lifetime: Int)The proof is older than the proof lifetime (
with_proof_lifetime):ageandlifetimein 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_proof_error(error: ProofError) -> String
One line for logs, naming the setter where one fixes it.
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 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.