# Agent guardrail

> An agent is about to run a tool call. Screen it with five checks that share one request, then let plain code decide whether to allow, confirm or block.

Section: Scenarios · HTML: https://jevascript.org/docs/scenarios/agent-guardrail · Markdown: https://jevascript.org/docs/scenarios/agent-guardrail.md

**Source:** [`examples/agent-guardrail.ts`](https://github.com/hakantapanyigit/jevascript/blob/main/examples/agent-guardrail.ts)

## The problem

Guardrails run on *every* tool call, so cost and latency are the binding constraint rather than peak intelligence. Five separate model calls per action would be too slow and too expensive. Five questions in one request are neither.

## The screen

The state is the tool call itself: the tool, its arguments, and what the user actually asked for. That last field is what makes "beyond the request" answerable.

```ts
// screen.ts
import { choose, is, score, semantic } from "jevascript"

interface ToolCall {
  tool: string
  args: Record<string, unknown>
  userAsked: string
}

const RISK_LEVELS = [
  "Affects nothing outside the current request.",
  "Affects a single record belonging to one user.",
  "Affects many records, or an entire customer account.",
  "Affects every tenant, or the production system as a whole.",
]

export async function screen(call: ToolCall) {
  return semantic(call).batch({
    irreversible: is("Running this permanently destroys data or cannot be undone.", {
      trueWhen: "Deletes, drops, truncates, overwrites, or sends something to a third party.",
      falseWhen: "Reads, lists, searches, or writes to a scratch area.",
    }),
    beyondRequest: is("This goes further than what the user actually asked for.", {
      falseWhen: "A reasonable, direct step toward the stated request.",
    }),
    touchesProduction: is("This operates on production data or live infrastructure."),
    blastRadius: score("how much this affects", { levels: RISK_LEVELS }),
    category: choose(
      {
        read: "Only reads or searches data",
        write: "Creates or updates data",
        destroy: "Deletes or irreversibly overwrites data",
        external: "Sends data outside the system, or spends money",
      },
      { instructions: "What kind of action is this" },
    ),
  })
}
```

`blastRadius` uses rubric levels because it is a genuine magnitude with four distinct situations, not a probability. Each level describes a situation, so the model can evaluate each one on its own.

## The decision

```ts
// gate.ts
const risk = await screen(call)

const verdict =
  risk.irreversible && risk.touchesProduction && risk.blastRadius > 60
    ? "block"
    : risk.beyondRequest || risk.irreversible || risk.category === "external"
      ? "confirm"
      : "allow"
```

The thresholds are reviewable, testable, and differ by how much being wrong would cost. Adding a sixth check is one more line in the batch and costs almost nothing.

## What this is not

The guardrail judges what the call *looks like*. It does not replace permissions. A `run_sql` call against production should also be gated by whether this agent is allowed to run SQL against production at all, which is a lookup, not a judgement. See [Not a security boundary](https://jevascript.org/docs/not-a-security-boundary).

## What to test

- `DELETE FROM sessions` on `prod` when the user asked to "clean up old sessions" → `confirm` (irreversible, but within the request).
- An email to `all-customers@` when the user asked for a draft → `confirm` on `beyondRequest`.
- A docs search → `allow`.
- Any single call → `provider.requestCount === 1`.
