Policies
Block dangerous tools at runtime. Glob-based rules, pre-call filter and post-decision check.
Policies
A policy is a rule that controls which tools an LLM can call. JamJet enforces policies at two points in the request lifecycle: before the request reaches the model (a pre-call filter) and after the model responds (a post-decision check). Both enforcement points are synchronous and in-process, with no network round-trip required.
Two actions that gate tools
Every policy rule pairs a glob pattern with an action. Two of those actions gate tool calls at runtime:
| Action | Behavior |
|---|---|
'block' | Matched tools are filtered out of the request before the LLM sees them. If the model still requests a blocked tool in its response, JamjetPolicyBlocked is thrown. |
'allow' | Explicitly permits matched tools. Useful for building an allowlist after a broad block. |
import { policy } from '@jamjet/cloud'
policy('block', 'wire_*') // block any tool whose name starts with wire_
policy('allow', 'wire_read') // except wire_read, allow it explicitlyimport jamjet.cloud as jamjet
jamjet.policy('block', 'wire_*') # block any tool starting with wire_
jamjet.policy('allow', 'wire_read') # except wire_read, allow it explicitlyA third declarative action, 'require_approval', does not gate at runtime. See require_approval does not gate below.
Glob pattern semantics
JamJet uses fnmatch-style glob matching:
| Pattern | Matches | Does not match |
|---|---|---|
wire_* | wire_transfer, wire_send, wire_read | read_wire |
payments.* | payments.send, payments.read | payments |
*_admin | db_admin, user_admin | admin_db |
?_transfer | a_transfer | ab_transfer |
* matches any sequence of characters including dots. ? matches exactly one character. Patterns are matched against the tool name as a whole string (implicitly anchored at both ends).
Last-matching-rule wins
When multiple rules match the same tool, the last rule in registration order wins. This lets you write a broad block first and then carve out exceptions:
import { init, policy, wrap } from '@jamjet/cloud'
import OpenAI from 'openai'
init({ apiKey: process.env.JAMJET_API_KEY!, project: 'my-app' })
// Rule 1: block everything in payments namespace
policy('block', 'payments.*')
// Rule 2: allow the safe read operation (registered after rule 1, wins for payments.read)
policy('allow', 'payments.read')
const openai = wrap(new OpenAI())import jamjet.cloud as jamjet
from openai import OpenAI
import os
jamjet.configure(api_key=os.environ['JAMJET_API_KEY'], project='my-app')
# Rule 1: block everything in payments namespace
jamjet.policy('block', 'payments.*')
# Rule 2: allow the safe read operation (registered after rule 1, wins for payments.read)
jamjet.policy('allow', 'payments.read')
client = jamjet.wrap(OpenAI())For payments.read: rule 1 says block, rule 2 says allow. Rule 2 was registered last, so allow wins and the tool is permitted.
For payments.delete: only rule 1 matches, so it is blocked.
Pre-call enforcement
Before the wrapped client sends a request to the LLM, JamJet inspects the tools array in the request. Any tool whose name is matched by a block rule (and not subsequently overridden by allow) is removed from the list. The LLM never sees the tool exists.
This prevents the model from being tempted to call a dangerous tool even if your prompt says not to.
Post-decision enforcement
After the LLM responds, JamJet inspects every tool_call in the response. If a tool_call matches a block rule:
- The span is marked with
policy_blocked: true. JamjetPolicyBlockedis thrown. The error'scauseproperty carries the original tool_call object.
import { JamjetPolicyBlocked } from '@jamjet/cloud'
try {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Transfer $10 to Alice.' }],
tools: [/* ... */],
})
} catch (err) {
if (err instanceof JamjetPolicyBlocked) {
console.error('Tool call blocked by policy:', err.cause)
// err.cause is the original tool_call from the LLM response
}
}from jamjet.cloud.errors import JamjetPolicyBlocked
try:
response = client.chat.completions.create(
model='gpt-4o',
messages=[{'role': 'user', 'content': 'Transfer $10 to Alice.'}],
tools=[...],
)
except JamjetPolicyBlocked as err:
print('Tool call blocked by policy:', err.cause)
# err.cause is the original tool_call from the LLM responserequire_approval: what works and what does not
There are two separate approval mechanisms, and they work differently depending on whether you are using the Cloud SDK or the open-source runtime.
Cloud SDK (TypeScript). The declarative policy('require_approval', tool) rule does not gate calls. Tools matched by that rule are forwarded to the model exactly as if no rule existed. For a working human gate from code, call requireApproval(action, { context, timeoutMs }) directly. It creates a pending approval in the dashboard Pending tab, then polls until a reviewer approves or rejects. On approval it returns; on rejection it throws JamjetApprovalRejected carrying the reviewer's reason. See Approvals for the full SDK flow.
Open-source runtime. A require_approval_for glob pattern in a node, workflow, or tenant policy parks the matching tool node before execution. The node does not run and the execution stays in the running state, held by a ToolApprovalRequired event. That event survives crashes and restarts, so there is no timeout and no lost hold. A reviewer decides via POST /executions/:id/approve. See Runtime Approvals for the full REST contract and an end-to-end example.
Within the same policy scope, blocked_tools wins over require_approval_for: a tool matched by both is refused outright, not parked. Across scopes, the most specific scope's rules are evaluated first (node over workflow over tenant), so a node-level require_approval_for fires before a workflow-level blocked_tools.
Policy simulation
Before you enforce a candidate rule, you can dry-run it against recorded events to see exactly which past tool calls it would have matched. This lets you tune a glob pattern without risking a bad block in production.
You can simulate inline on the Policies surface while editing a rule, or use the dedicated simulator at /dashboard/policies/simulate. Both run against your recorded events, so the result reflects real traffic rather than guesses. The simulator is backed by POST /v1/policy/simulate.
Other policy kinds
Beyond tool-gating rules, JamJet supports additional policy kinds:
tool_compactiontrims oversized tool results before the model call. See tool_compaction below.cache_injectis created by Optimize when you accept a one-click apply, and reuses repeated prompt prefixes to cut cost.budgetenforces a cost ceiling. See Budgets.
tool_compaction
The tool_compaction policy kind is opt-in. Its pattern is a tool-name glob, and its config requires max_result_tokens greater than 0. When an enforced call runs, the SDK head-truncates oversized matching tool results in the message history before the model call, then records tokens_saved and a compacted flag on the span. Recovered value from compaction shows up on the Savings surface.
tool_compaction is available in @jamjet/cloud 0.4.0-alpha.4, published under the npm next tag. It is not part of the current GA line.
// npm install @jamjet/cloud@next
import { policy } from '@jamjet/cloud'
// Truncate any search_* tool result that exceeds 2000 tokens.
policy('tool_compaction', 'search_*', { max_result_tokens: 2000 })Streaming enforcement
Policy enforcement in streaming mode (stream: true) is limited on the bare SDK because tool_calls arrive in fragments across chunks. Full streaming enforcement (buffering, reassembly, and the post-decision check) is provided by the @jamjet/cloud-vercel middleware.
See Vercel AI SDK integration for how to add jamjetMiddleware() to streamText.
Viewing policy decisions in the dashboard
Every blocked decision is recorded in the span. Open any span in the dashboard to see:
- Which tools were filtered at pre-call
- Which tool_calls were blocked post-decision
- Which rules matched (pattern and action)
For retrospective audit across many spans, use the Audit surface.