Skip to main content

Guardrails

The policy layer that gates every agent tool call before it dispatches, spend budgets, rate limits, and human approval, enforced server-side.

4 min read
View MarkdownEdit on GitHub

Guardrails

Server-side, not advisory

Guardrails run in the CodeSpar runtime, before a tool call reaches a provider, not in your agent's prompt. A blocked call never dispatches, no matter what the model decided. The same rules apply whether the call came from session.execute() or the chat-loop session.send().

Guardrails let you bound what an agent is allowed to do with money. You define policies per project; the runtime evaluates every governed tool call against them and either allows it, blocks it, or holds it for human approval. Every decision is written to the audit chain.

Policy types

PolicyWhat it bounds
BudgetA spend cap over a window, e.g. R$ 5.000/day or a monthly ceiling per project. Charges and payouts that would exceed it are blocked.
Rate limitHow often a tool can be called: calls per minute / per hour, enforced with durable counters (not best-effort in-memory).
Approval requiredRoutes a matching call to a human before it executes. The call is held; an operator approves or rejects it in /dashboard/approvals; on approval it dispatches, on rejection it never does.

Policies can scope to a tool, a meta-tool, an amount threshold, or a time window, and compose. A payout might be inside budget but still need approval above a threshold.

What this page does not cover

The internal scoring and risk logic CodeSpar uses to evaluate fraud and anomaly signals is proprietary and intentionally out of scope here. This page documents the policy surface you control, not the engine internals.

How a decision flows

agent calls a tool

guardrails evaluate the call against the project's policies
  ↓                    ↓                         ↓
allow                deny                  approval-required
  ↓                    ↓                         ↓
dispatches      returns a deny result      held → /dashboard/approvals
                                            → approve → dispatches
  ↓                    ↓                         ↓
every outcome is appended to the audit chain

Approvals

When a call matches an approval-required rule, it does not dispatch. The runtime holds it and writes a row to the approvals queue at /dashboard/approvals, where an operator with admin or owner role approves or rejects it with a reason. Every held call carries an expiry: if nobody decides in time, the approval expires and the call never runs.

On the caller's side, the blocked call returns an approval id. Your agent or SDK can poll for the outcome:

GET /v1/approvals/:id/status

The endpoint is authenticated with your normal API key and scoped to your org and project. approval_status is one of:

StatusMeaning
pendingAwaiting an operator decision. The only non-terminal status; keep polling.
approvedDecided and executed. The held call is re-executed server-side; execution_result in the response carries the upstream outcome.
deniedThe operator rejected it. Nothing executed.
expiredThe expiry elapsed before a decision. Nothing executed.
execution_failedApproved, but the replayed call errored. execution_result.error explains.

Policies themselves are created and ordered in the dashboard at /dashboard/policies.

Chat-loop coverage

The chat loop is the natural-language tool-use path: session.send("Charge R$5 via Pix") runs a tool-use loop on the backend, picks the tools, and dispatches each one through the runtime. Every tool dispatch inside that loop runs through the same guardrail hooks, in the same fixed order, as a direct session.execute():

  1. Deny-list. Non-overridable safety rails (Projects). A test cannot mock past these.
  2. Policy evaluation. Your rules evaluate against the tool name plus the input the model picked. allow, deny, or approval-required.
  3. Session mock store (test-mode sessions only). See Test mode.
  4. Upstream provider call, if not denied, held, or mocked.
  5. Audit chain append. Every completed call writes a tool_call.succeeded or tool_call.failed event, same event names and shape as the direct-execute path, so existing audit and SIEM queries pick chat-loop events up without changes.
  6. Commerce-memory capture, when the per-tool capture predicate matches.

Rules are tool-and-input scoped, not call-site scoped. A rule matching a payments tool fires whether your code named the tool or the model picked it inside session.send(); you do not need to duplicate rules per path. There is no "allow direct execute but deny chat-loop" distinction in the rule grammar. The conservative pattern for high-blast-radius tools is approval-required: the chat loop hits the approvals queue and your operators decide per call. When an approval-required rule fires inside the loop, the loop receives a structured approval_required tool result and the same queue row is written, so check /dashboard/approvals volume after your next chat-loop test run.

The cleanest way to confirm coverage is a test-mode session that mocks the relevant tool. The chat loop runs the hooks identically to direct execute, so one fixture round-trip proves both paths:

import { CodeSpar } from "@codespar/sdk";

const cs = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY });

const session = await cs.create("user_test", {
  servers: ["asaas"],
  mocks: {
    "asaas/create_payment": { id: "pay_test_42", status: "PENDING" },
  },
});

const result = await session.send("Charge R$5 via Pix");

// The loop picked asaas/create_payment, the policy engine evaluated it,
// the mock store returned the fixture, and the audit chain recorded it.
const charge = result.tool_calls.find((tc) => tc.tool_name === "asaas/create_payment");
expect(charge?.output?.id).toBe("pay_test_42");

See Test mode for the full mocks contract and the type-narrowed guards (isPolicyDenied, isApprovalRequired) for parsing tool-result envelopes.

See also

  • Audit chain: every guardrail decision is recorded
  • Wallets: programmable spend limits on agent wallets
  • Test mode: policy and audit hooks fire on mocked calls too
Guardrails | CodeSpar