jevascript
esc

    Type to search · ↑↓ to move · ↵ to open

    Get started

    Scenarios

    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.

    Source: 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.

    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#

    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.

    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.