Skip to content

Core concepts ​

Five ideas explain almost everything Aegis does: routes, stages, nodes and guardrails, verdicts, and RunState.

Request lifecycle ​

  1. Authenticate. The Authorization: Bearer aeg-… key resolves to a principal (/metrics is the only unauthenticated path). With --no-auth every request is the anonymous principal.
  2. Select a route. /v1/runs takes a route field; /v1/chat/completions uses the model field as the route name.
  3. Ingress. The route's ingress nodes run in order against the request.
  4. Execute. The route's provider is called with the (possibly masked) messages.
  5. Egress. Egress nodes run against the response — e.g. PII unmasking.
  6. Record. The run's status and full event log go to the run store; a hash-chained run_evidence record goes to the ledger.

Routes ​

A route binds a provider to a pipeline. Each route is compiled once at startup into its own LangGraph graph.

yaml
providers:
  cheap:
    type: fake
  careful:
    type: fake

guardrails:
  pii:
    pack: aegis.pii
    mode: mask
  classify:
    pack: aegis.classification

pipeline:              # the default pipeline for every route…
  ingress: [pii]
  egress: [pii]

routes:
  default:
    provider: cheap
  sensitive:
    provider: careful
    pipeline:          # …unless the route declares its own
      ingress: [classify, pii]
      egress: [pii]

Stages, nodes and guardrails ​

A guardrail entry in aegis.yaml names a pack. At startup the pack's factory turns that entry into one or more pipeline nodes, each assigned to a stage. That is why a single pii entry can place a mask node on ingress and an unmask node on egress.

Two kinds of object end up in a stage:

ContractReturnsUse it for
Guardrailasync scan(state) -> Verdicta verdictYes/no/pause decisions: residency, budgets, injection checks
Pipeline nodeasync run(state) -> RunStateDeltaa partial state updateTransformations: masking, labelling, retrieval

Guardrails are wrapped in a GuardNode, which runs its guards in order, records one event per verdict, and stops at the first block or pause.

Verdicts ​

Every guardrail returns exactly one verdict:

VerdictConstructorEffect
allowVerdict.allow()Continue unchanged.
sanitizeVerdict.sanitize(replacement)Continue, with message content replaced by replacement.
blockVerdict.block(reason)Stop. Run status blocked; the provider is never called if it happens on ingress.
require_approvalVerdict.require_approval(prompt)Checkpoint and pause. Run status paused until a reviewer resumes it.

Every verdict — including allow — becomes an event in the run's log and in the ledger, so "which guard let this through?" is answerable.

Targeted rewrites belong in nodes

sanitize replaces message content wholesale. For surgical edits (masking a single entity, redacting a span) write a pipeline node that returns rewritten messages in its RunStateDelta — that is how the PII pack works.

RunState ​

Nodes never import each other; they communicate only through RunState:

FieldPurpose
run_id, route, principalIdentity of the run.
messagesThe conversation, as the next node will see it.
labelsFree-form dict[str, str] for cross-pack signals — the classification pack writes labels["classification"].
mask_mapPlaceholder → original value. Never sent to the model.
eventsAppend-only audit log (stage, node, event_type, data).
usageToken and cost accounting.
response, statusOutput and one of running, completed, blocked, paused, denied.

A node returns a RunStateDelta; None fields are left untouched and events are appended.

Run statuses ​

StatusMeaning
completedAll stages passed; response is set.
blockedA guardrail returned block.
pausedWaiting for a reviewer (require_approval).
deniedA reviewer denied a paused run.
pending / runningBackground run not yet finished.
errorAn exception escaped the pipeline.

Next ​