All writing

Introducing Guardrail: AI Agents Should Not Get to Decide Their Own Boundaries

Clone Guardrail from GitHub

AI systems are getting increasingly good at doing things. Not merely answering questions. Not merely generating text. Doing things. Sending messages. Updating records. changing settings. Triggering workflows. Preparing transactions. Calling providers. Modifying external systems.

That is where the conversation around AI safety gets considerably more practical. If an agent proposes an action, there eventually has to be a point where the software answers a deceptively simple question: Is this system actually allowed to do that?

I have been working on that problem with Brivvvy Guardrail, a small TypeScript package built around a very specific idea: Given an intended action and observable context, determine what the system is permitted to do next. That is intentionally narrower than building another agent framework.

Guardrail does not call an LLM. It does not execute tools. It does not infer permissions. It does not decide that an action feels safe because a model explained itself convincingly enough. It evaluates explicit policy and returns a structured decision. That distinction is the whole point.

The Model Should Not Be the Security Boundary

Large language models are probabilistic systems. That is useful. It is why they can interpret messy information, reason about unfamiliar situations, generate novel responses, and operate across domains that would otherwise require enormous amounts of explicitly programmed logic. It is also exactly why I do not want one deciding its own execution boundaries.

Imagine an autonomous system that wants to send a message to a customer. The model may be extremely confident that the message is appropriate. That still does not answer several questions the application itself needs to know:

Does the current actor have permission to send it?
Is the model's confidence high enough for autonomous execution?
How risky is the action?
Can the action be reversed?
Has the business enabled autonomous external messaging?
Is the connected provider currently allowing writes?
Should a person approve this first?
Is this situation unusual enough that it should be escalated?

Those are not really language-model questions. They are policy questions. So Guardrail keeps them outside the model.

At a conceptual level, the flow is intentionally boring:

Action Context
     │
     ▼
Runtime Validation
     │
     ▼
Exact Policy Match
     │
     ├── Permissions
     ├── Confidence
     ├── Risk
     ├── Reversibility
     ├── Provider Constraints
     └── Settings
     │
     ▼
Guardrail Decision

Boring is good here. When software is deciding whether an autonomous system can alter something outside itself, I do not want magic. I want something I can inspect.

Autonomy Is Not Binary

One of the design decisions I particularly like in Guardrail is that an agent is not simply reduced to:

ALLOW
DENY

Real systems need more nuance than that. Guardrail defines six possible dispositions:

observe
recommend
draft
request_approval
execute
escalate

Those states represent different degrees of autonomy. observe means the system can inspect what is happening but should not prepare or execute a change. recommend means it can determine what it thinks should happen. draft lets it prepare the work while stopping short of taking the external action. request_approval puts a human directly into the execution path. execute means policy permits the application to continue. escalate means the situation requires human judgment before proceeding.

Importantly, those are outcomes, not mandatory stages. An action does not have to march through all six states like some elaborate enterprise workflow. A routine, low-risk, reversible action might move directly to execution. A high-risk action with missing privileges might go directly to escalation. The autonomy boundary depends on the policy and the evidence available at that moment.

A Policy Is Just Data

Here is a simplified policy from the package:

import {
  createGuardrail,
  definePolicy,
} from "@brivvvy/guardrail";

const sendExternalMessage = definePolicy({
  id: "send-external-message",
  action: "send_external_message",
  description: "Gate execution of an external message.",
  onPass: "execute",

  controls: [
    {
      kind: "permissions",
      allOf: ["message:send"],
      onFailure: "escalate",
    },
    {
      kind: "confidence",
      atLeast: 0.95,
      onFailure: "request_approval",
    },
    {
      kind: "risk",
      atMost: "low",
      onFailure: "request_approval",
    },
    {
      kind: "reversibility",
      mustBe: true,
      onFailure: "request_approval",
    },
  ],
});

const guardrail = createGuardrail({
  policies: [sendExternalMessage],
});

There is no prompt in there. There is no:

You are a responsible AI system.
Please determine whether this action is safe.

There is also no arbitrary callback buried in the policy that can quietly execute whatever application logic it wants. The policy is declarative. To execute automatically, the action needs the required permission, at least 95% supplied confidence, sufficiently low risk, and reversibility.

If a permission is missing, the policy says to escalate. If confidence, risk, or reversibility fail their respective requirements, a human approval is required. That makes the boundary visible in code review. More importantly, it makes the boundary testable.

Then Give Guardrail the Evidence

The policy itself does not know what is happening in your application. The application supplies a context:

const decision = guardrail.evaluate({
  action: "send_external_message",
  permissions: ["message:send"],
  confidence: 0.91,
  risk: "low",
  reversible: true,
});

In this example, everything looks fine except confidence. The policy requires:

atLeast: 0.95

The application supplied:

confidence: 0.91

So Guardrail returns a structured decision resembling:

{
  action: "send_external_message",
  matchedPolicy: true,
  policyId: "send-external-message",

  disposition: "request_approval",

  canExecute: false,
  requiresHuman: true,

  controlsEvaluated: 4,
  controlsFailed: 1,

  reasons: [
    {
      code: "CONFIDENCE_BELOW_THRESHOLD",
      control: "confidence",
      message:
        "Confidence is below the minimum required by policy.",
      details: {
        actual: 0.91,
        minimum: 0.95,
      },
    },
  ],
}

Nothing has been sent. Nothing has been executed. Guardrail has simply answered the question it was designed to answer: Given what we know, what level of autonomy is permitted?

The application can then decide how its approval UI, workflow engine, audit system, or escalation process should handle that result.

The Most Important Line Comes After Guardrail

There is an important architectural detail here. Calling Guardrail is not enough. Its decision has to be enforced where the real action occurs.

Conceptually:

const decision = guardrail.evaluate(context);

if (!decision.canExecute) {
  return {
    performed: false,
    decision,
  };
}

await externalProvider.performAction();

That boundary matters enormously. Guardrail is not authentication. It is not authorization. It is not your provider validation. It is not your database security model. It is another explicit layer inside that system.

A more complete execution path might look like:

Authenticate
    ↓
Authorize
    ↓
Build trusted action context
    ↓
Guardrail evaluate
    ↓
Approval / escalation / continue
    ↓
Provider validation
    ↓
Execute
    ↓
Audit outcome

I think separating those responsibilities makes the system easier to reason about. Authentication answers who someone is. Authorization determines what that identity is allowed to access. Guardrail determines how much autonomy the system receives for a particular action under the current observable conditions.

Those are related concerns. They are not the same concern.

Unknown Does Not Mean Safe

This is another design choice I strongly agree with. Suppose a policy requires:

{
  kind: "reversibility",
  mustBe: true,
  onFailure: "request_approval",
}

Now suppose the application does not provide a reversible value at all. Guardrail does not say: Well, it probably is. The control fails.

The same philosophy applies when confidence or risk is required. Missing evidence is not positive evidence. That sounds obvious when written down, yet software systems routinely become unsafe through optimistic defaults. Guardrail deliberately refuses to make that assumption.

Failure Can Only Reduce Autonomy

Each failing control has one of two outcomes:

onFailure: "request_approval"

or:

onFailure: "escalate"

There is no control failure that somehow increases autonomy. If several controls fail, Guardrail returns all of their structured reasons rather than stopping at the first one. And if any failed control requires escalation, escalation wins.

That gives the evaluation model a useful safety property: failure moves the system toward human judgment, never toward execution.

Missing policies behave similarly. If the application evaluates an action for which no policy exists, Guardrail fails closed. By default, that produces an escalation rather than execution. You can configure a missing policy to require approval instead. You cannot configure a missing policy to execute. That is an intentionally small set of options.

Permissions Are Only One Part of the Decision

Guardrail v0.1 currently understands several kinds of deterministic controls. Permissions can require every capability:

{
  kind: "permissions",
  allOf: ["record:read", "record:write"],
  onFailure: "escalate",
}

Or at least one capability:

{
  kind: "permissions",
  anyOf: ["role:owner", "role:operator"],
  onFailure: "escalate",
}

Or explicitly forbid capabilities or states:

{
  kind: "permissions",
  noneOf: ["account:suspended"],
  onFailure: "escalate",
}

Confidence controls establish a minimum threshold:

{
  kind: "confidence",
  atLeast: 0.92,
  onFailure: "request_approval",
}

Risk controls establish a maximum:

{
  kind: "risk",
  atMost: "medium",
  onFailure: "request_approval",
}

with the deterministic ordering:

low < medium < high < critical

Provider constraints can account for conditions imposed by an external integration:

{
  kind: "provider_constraints",
  noneOf: ["read_only", "writes_disabled"],
  onFailure: "escalate",
}

And application settings can become part of policy:

{
  kind: "setting",
  key: "external_actions_enabled",
  equals: true,
  onFailure: "request_approval",
}

What Guardrail does not do is manufacture those facts. It does not calculate risk. It does not calculate confidence. It does not query the provider. The surrounding application remains responsible for supplying trustworthy context.

That separation is important because the policy engine should not quietly become a second application.

Determinism Is a Feature, Not a Limitation

Guardrail evaluation uses no network calls, model calls, wall clock, randomness, database access, environment lookup, or hidden global configuration. Give it the same validated policy and the same context, and it produces the same decision.

That makes the behavior unusually easy to reason about. You can reproduce it. You can unit test it. You can inspect why something failed. You can construct exact boundary conditions. You can put policy changes through normal code review.

And perhaps most importantly for AI systems, the thing being governed cannot simply talk the policy engine into changing its mind. A model can propose an action. The policy boundary remains outside of that model.

Some Missing Features Are Missing on Purpose

Version 0.1 is intentionally small. It does not include model inference, prompt-based policies, tool execution, an approval UI, audit persistence, authentication, tenant isolation, provider adapters, wildcard matching, policy priorities, inheritance, remote policy loading, or arbitrary JavaScript predicate functions.

That list could look like a roadmap of missing features. It is not entirely that. Some omissions are safety properties.

Arbitrary predicates, for example, would make the policy system much more expressive. They would also make policies harder to inspect, harder to serialize, harder to validate, and easier to turn into another pile of application logic. For this release, Guardrail favors a smaller declarative surface.

There is exactly one policy per action. Actions use exact identifiers. There is no hidden precedence. Every control gets evaluated. Escalation dominates approval. Unknown required values fail. Policy stays data.

That is a pretty opinionated design. I think that is appropriate for something whose entire reason for existing is defining boundaries.

TypeScript Is Helpful. Runtime Validation Still Matters.

Guardrail is strongly typed, but it does not pretend TypeScript protects a running application from malformed input. Public inputs are validated before evaluation.

The package rejects things such as empty action identifiers, duplicate policy IDs, multiple policies for one action, confidence outside the 0..1 range, NaN, infinite numeric values, unknown risk levels, empty permission clauses, duplicate identifiers, unsupported failure dispositions, and unsupported setting values.

Invalid input produces validation or configuration errors. A perfectly valid decision that says:

do not execute

does not.

That is an important distinction. Policy denial is normal application behavior. Malformed policy is a programming or configuration problem.

Small Enough to Understand

The package is intentionally compact. Its source is split across a small set of focused modules for errors, evaluation, registry behavior, policy construction, types, and validation.

The public runtime API is similarly small:

createGuardrail
definePolicy
evaluatePolicy

GuardrailError
GuardrailValidationError
GuardrailConfigurationError

RISK_LEVELS
GUARDRAIL_DISPOSITIONS

The generated package has zero runtime dependencies and is framework-independent. The repository currently targets Node.js 20 or newer for development and TypeScript 5.x for source builds.

The full local verification command is:

npm run check

which runs type checking, more than 106 behavioral and invariant tests, a production build, and a repository secret-hygiene check.

That test suite covers things like exact confidence boundaries, every risk ordering combination, missing contextual values, permissions, provider constraints, setting equality, escalation precedence, missing-policy behavior, deterministic repeated evaluation, mutation invariants, and the convenience flags attached to every disposition.

For a package whose value depends heavily on predictable behavior, that is exactly where I want the complexity to live: in proving the small core behaves correctly.

Installing It

The package is designed to be installed as:

npm install @brivvvy/guardrail

And the quickest conceptual starting point is:

const policy = definePolicy({
  id: "prepare-change",
  action: "prepare_change",
  description: "This action may only prepare a draft.",
  onPass: "draft",
  controls: [],
});

const guardrail = createGuardrail({
  policies: [policy],
});

const decision = guardrail.evaluate({
  action: "prepare_change",
});

Even with every control passing, that policy returns:

{
  disposition: "draft",
  canExecute: false,
  requiresHuman: false,
  // ...
}

That example captures something important about the package. A successful policy does not have to mean execution. Sometimes success means: You may draft this. You may not send it. That is a much more useful model of autonomy than a universal green light.

Better Agents Need Better Boundaries

I do not think the answer to increasingly capable agents is to make them less useful. Quite the opposite. I want agents capable of doing substantial work.

But usefulness and unrestricted execution are not the same thing. A system can be extremely capable while still operating inside explicit boundaries. In fact, I think that is probably how useful autonomous systems become trustworthy enough to handle increasingly consequential work.

The model should reason. The application should authenticate and authorize. The provider should enforce its own constraints. And somewhere immediately before action, there should be a very boring piece of deterministic software asking: Given what we know, what is this system actually allowed to do?

That is the job Brivvvy Guardrail is trying to do. The latest version is deliberately small. I like it that way. When the software exists to tell autonomous systems where the line is, understanding exactly how the line works seems like a pretty good place to start.