Browse the documentation

AEL 1.0.0

Hooks

A hook in AEL, the Agent Engineering Language, is a typed function that runs before or after other code, for authorization, auditing or cleanup. Hooks attach before or after any component, function or statement, run in ordered or parallel groups, and fail closed when authorization fails.

Declaring a hook

A hook lives in <name>.hook.ael and declares exactly one hook <name>(…), with private helpers like any component.

# src/auth.hook.ael
hook auth(context: EntryContext<Request>) -> HookDecision {
    return check_caller(context);
}

fn check_caller(context: EntryContext<Request>) -> HookDecision {
    # Your policy: allow this call, or deny it with a reason.
}

The hook receives a typed context and returns a typed decision. The body of check_caller is a placeholder, as A first agent explains.

A hook is always written in AEL. A hook file and its extension files hold no function in another language, and a hook declared in another language is refused. A hook attaches before or after a function in another language as it does to any function, and sees its typed outcome, but never attaches to anything inside that function's body, which is not AEL; see Functions in other languages.

Attaching a hook

There are three places to attach a hook:

# Before a component or function: @ and the hook's name.
@auth
agent billing(request: Request) -> Receipt {
    # …
}
# After a component or function: @ and the name, right after the closing brace.
agent billing(request: Request) -> Receipt {
    # …
}@audit
# Before one statement.
fn charge(amount: u64, balance: u64) -> u64 {
    @limit_check
    let rest: u64 = balance - amount;
    return rest;
}
  • A hook before a statement runs each time that statement is reached, once, just before it. A statement in a branch that does not run triggers no hook.
  • The values the statement uses are evaluated once. The hook sees those values; it never evaluates them a second time.
  • A package can export hooks too. You select one in the file header, like any other export, and attach it by name.

Ordered and parallel groups

Several hooks can attach at one place. order puts them into groups:

@auth(order = 0)
@rate_limit(order = 0)
@load_account(order = 1)
agent billing(request: Request) -> Receipt {
    # …
}@audit
  • A hook without order is in group 0.
  • Hooks with the same order run in parallel. The order in which you write them never makes them sequential.
  • Groups run in ascending order: group 1 starts only after every hook of group 0 has succeeded. Gaps in the numbers are allowed.
  • The protected code starts only after every group before it has succeeded.
  • When one hook fails, the remaining work of that attachment is cancelled and cleaned up, and later groups do not run.

The check refuses hooks of one group that could interfere with each other, such as two that change the same value.

Authorization fails closed

If an authorization hook denies the call, fails, times out or is cancelled, the protected code does not run. The call ends as denied; it never ends as if the protected code had succeeded.

Hooks after the code

A hook after a component or function runs once the code has been admitted by its entry hooks, whatever the outcome:

  • It runs when the code returns normally, returns an error or is cancelled, and its context says which of the three happened.
  • It runs before the code's result reaches the caller.
  • It does not run when an entry hook denied the call, because the code never started.
  • If the code failed, that failure stays the result, and a failing exit hook is reported alongside it. If the code succeeded and an exit hook fails, the call ends with that hook's failure.
  • An exit hook cannot undo something the code has already done outside the program, such as a sent message.
  • A panic, or a forced stop of the whole program, ends the program at once: exit hooks are not guaranteed to run then, and their cleanup is never reported as done.

What a hook sees

AttachedThe context holds
Before a component or functionWhich component and run it is, the parameters, the effective configuration, the authenticated caller, the deadline and the budget.
After a component or functionThe same, with the outcome: success with its result, an error, or cancellation.
Before a statementThe values the statement uses.
Before an HTTP route of the server/http packageThe method and route, typed path, query and body parameters, headers and cookies, and the authenticated caller.
  • The context is read-only unless the attachment says otherwise, and any change goes through a typed, checked result.
  • A hook cannot change who the caller is, reach secrets it was not given, or keep request data after the request ends.
  • Credentials and cookies are not logged by default.

Budgets and permissions

A hook runs inside the call it guards. It draws on that call's budget and uses that call's permissions: it never receives a budget or a permission of its own.

A hook runs only when your program runs. No hook runs while you check or build your code, and no hook can change what the compiler does. Hooks that call code carrying hooks of their own are allowed only to a bounded depth.