18 KiB
Policies
Policies are declarative gates that enforce rules on agent behavior. They evaluate agent actions at specific enforcement points and return one of three verdicts:
- ALLOW -- the action proceeds.
- DENY -- the action is blocked; the agent receives an error.
- ASK -- the action is paused for user approval; approved becomes ALLOW, refused becomes DENY.
Policies compose: multiple policies can be active at once. The engine evaluates them in declaration order. A DENY from any policy short-circuits the rest.
Who configures policies
Policies are set at three levels. Each level serves a different persona:
| Level | Who | How | Evaluated |
|---|---|---|---|
| Server-wide | Admin | policies in server config YAML, or REST API |
Last |
| Agent spec | Agent developer | policies in agent YAML |
Middle |
| Session | End user | Session settings panel in the UI | First |
Session policies evaluate first and can short-circuit (DENY) before spec or admin policies run.
For server admins
Setting up server-wide policies
Server-wide (default) policies apply to every session. They act as organizational guardrails.
1. Choose policies. Browse the builtin policy registry (see Builtin policies below) or install community/custom policy modules.
2. Register custom policy modules (optional). If you use policies outside the builtins, add their module paths to the server config so they appear in the registry:
# server_config.yaml
policy_modules:
- myorg_policies
- github_mcp_policy
3. Add policies to the server config.
# server_config.yaml
policies:
session_budget:
type: function
handler: omnigent.policies.builtins.cost.cost_budget
factory_params:
max_cost_usd: 10.00
ask_thresholds_usd: [5.00]
global_rate_limit:
type: function
handler: omnigent.policies.builtins.safety.max_tool_calls_per_session
factory_params:
limit: 200
4. Start the server.
omnigent server --config server_config.yaml
After starting, you can also add or remove policies at runtime through the REST API (see Admin policy REST API).
For agent developers
Adding policies to an agent spec
Policies are declared under policies at the top level of the agent YAML. They are evaluated in declaration order.
name: github_agent
prompt: You are a coding assistant with access to GitHub.
executor:
harness: claude-sdk
model: databricks-claude-sonnet-4-6
tools:
github:
type: mcp
url: https://api.githubcopilot.com/mcp/
policies:
limit_tool_calls:
type: function
handler: omnigent.policies.builtins.safety.max_tool_calls_per_session
factory_params:
limit: 100
github_access:
type: function
handler: omnigent.policies.builtins.github.github_policy
factory_params:
write_repos:
- myorg/my-repo
write_branches:
- "feature/*"
google_policy:
type: function
handler: omnigent.policies.builtins.google.gdrive_policy
factory_params:
read_all: true
allow_create: true
Policy declaration syntax
Each policy entry has:
| Field | Required | Description |
|---|---|---|
type |
yes | "function" |
handler |
yes | Dotted Python import path to the callable or factory |
factory_params |
no | Key-value arguments passed to a factory at build time |
Direct callable (no parameters):
approve_file_ops:
type: function
handler: omnigent.policies.builtins.safety.ask_on_os_tools
Factory (with parameters -- called once at build time to produce the evaluator):
rate_limit:
type: function
handler: omnigent.policies.builtins.safety.max_tool_calls_per_session
factory_params:
limit: 50
For session users
Adding policies to a running session
Session-level policies let you customize agent behavior for your current task. There are two ways to add them:
- UI -- Open the information window to browse available policies and toggle them on or off.
- Chat -- Tell the agent directly, e.g. "add a policy that asks me before running shell commands". The agent has a built-in
sys_add_policytool and will configure the policy for you.
Session policies evaluate before agent spec and admin policies, so they can enforce stricter rules or add additional gates for your specific workflow.
Builtin policies
Safety
max_tool_calls_per_session
Limits the total number of tool calls in a session. DENYs after the limit is reached.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 100 |
Maximum tool calls allowed |
rate_limit:
type: function
handler: omnigent.policies.builtins.safety.max_tool_calls_per_session
factory_params:
limit: 50
ask_on_os_tools
Requires user approval before any sys_os_read, sys_os_write, sys_os_edit, or sys_os_shell tool call. No parameters (direct callable).
approve_file_ops:
type: function
handler: omnigent.policies.builtins.safety.ask_on_os_tools
block_skills
Prevents the agent from loading specific skills.
| Parameter | Type | Default | Description |
|---|---|---|---|
blocked |
string[] | (required) | Skill names to block (case-insensitive) |
no_deploy_skill:
type: function
handler: omnigent.policies.builtins.safety.block_skills
factory_params:
blocked: [deploy, rollback]
enforce_sandbox
Forces a specific sandbox configuration on agent start.
| Parameter | Type | Default | Description |
|---|---|---|---|
sandbox_type |
string | "linux_bwrap" |
Sandbox backend (linux_bwrap, darwin_seatbelt, none) |
allow_network |
boolean | true |
Allow network access |
write_paths |
string[] | null |
Writable paths (null inherits agent config) |
read_paths |
string[] | null |
Read-only paths (null inherits agent config) |
env_passthrough |
string[] | null |
Env vars allowed through sandbox |
deny_pii_in_llm_request
Scans user messages and LLM prompts for PII patterns (SSN, credit card, email, phone).
| Parameter | Type | Default | Description |
|---|---|---|---|
pii_types |
string[] | ["ssn", "credit_card", "email", "phone"] |
PII categories to scan |
action |
string | "DENY" |
Action when PII detected (DENY or ASK) |
Cost
cost_budget
Gates a session on cumulative LLM spend, at the request phase (before the LLM turn, so text-only turns are budgeted too) and the tool-call phase. ASKs the first time spend crosses each soft warning threshold. At the hard limit it acts as a downgrade gate, not a hard stop: it DENYs (the whole turn on request, or each tool call on tool_call) only while the session is on an expensive model -- telling the user to switch to a cheaper one with /model -- and allows them again once the session has switched.
| Parameter | Type | Default | Description |
|---|---|---|---|
max_cost_usd |
number | (required) | Hard spend limit in USD. Once reached, the turn / tool calls are blocked while the session is on an expensive model. |
ask_thresholds_usd |
number[] | null |
Soft warning checkpoints that ASK the first time spend crosses each (each must be < max_cost_usd) |
expensive_models |
string[] | Fable + Opus + GPT-5 (excl. -mini/-nano) |
Case-insensitive substring tokens for the model tiers blocked once over budget (e.g. "opus" matches any Opus deployment). The default's broad gpt-5 token matches the whole GPT-5 family except the cheap -mini/-nano variants; an explicit list is matched literally with no exclusions. [] disables the hard limit, leaving only the soft thresholds. |
budget:
type: function
handler: omnigent.policies.builtins.cost.cost_budget
factory_params:
max_cost_usd: 5.00
ask_thresholds_usd: [1.00, 3.00]
expensive_models: ["opus", "gpt-5"]
user_daily_cost_budget
Same ASK / downgrade-gate behavior as cost_budget, but the budget is the session owner's cumulative spend across all their sessions for the current UTC day. The soft-threshold approval is remembered per user+day, so an approved checkpoint won't re-prompt that user again today -- even from a different session. Useful as a server-wide per-user daily cap.
| Parameter | Type | Default | Description |
|---|---|---|---|
max_cost_usd |
number | (required) | Hard daily limit in USD. Once the owner's spend for the day reaches it, the turn / tool calls are blocked while on an expensive model. |
ask_thresholds_usd |
number[] | null |
Soft daily warning checkpoints that ASK the first time the owner's daily spend crosses each (each must be < max_cost_usd) |
expensive_models |
string[] | Fable + Opus + GPT-5 (excl. -mini/-nano) |
Case-insensitive substring tokens for the model tiers blocked once over the daily budget. An explicit list is matched literally with no exclusions. [] disables the hard limit, leaving only the soft thresholds. |
# server_config.yaml -- a per-user daily cap applied to every session
daily_budget:
type: function
handler: omnigent.policies.builtins.cost.user_daily_cost_budget
factory_params:
max_cost_usd: 25.00
ask_thresholds_usd: [10.00, 20.00]
GitHub
github_policy
Controls GitHub access across MCP tools and git/gh shell commands. Restricts reads to an allowlist (unless read_all) and writes to specific repos/branches. Shell commands with ambiguous targets return ASK.
| Parameter | Type | Default | Description |
|---|---|---|---|
read_all |
boolean | true |
Allow all reads |
read_repos |
string[] | [] |
Repos readable when read_all is false (owner/repo or URLs) |
write_repos |
string[] | [] |
Repos the agent may write to |
write_branches |
string[] | [] |
Branches writable within allowed repos (empty = any) |
mcp_tool_prefixes |
string[] | ["mcp__github__", "github__"] |
Tool-name prefixes to match |
shell_tools |
string[] | ["sys_os_shell"] |
Shell tools whose commands are parsed for git/gh |
github_access:
type: function
handler: omnigent.policies.builtins.github.github_policy
factory_params:
write_repos:
- myorg/frontend
- myorg/backend
write_branches:
- "feature/*"
- "fix/*"
Google Workspace
gdrive_policy
Controls Google Drive / Docs / Sheets / Slides access. Writes are restricted to files the agent created in the current session plus explicitly allowed files.
| Parameter | Type | Default | Description |
|---|---|---|---|
read_all |
boolean | true |
Allow all reads |
read_files |
string[] | [] |
File IDs or URLs readable when read_all is false |
allow_create |
boolean | false |
Allow creating new files |
write_files |
string[] | [] |
File IDs or URLs always writable |
comment_files |
string[] | [] |
File IDs or URLs the agent may comment on |
tool_prefixes |
string[] | ["mcp__google__", "google__"] |
Tool-name prefixes to match |
gmail_policy
Controls Gmail access. Defaults to read + draft but no send.
| Parameter | Type | Default | Description |
|---|---|---|---|
allow_read |
boolean | true |
Allow reading mail |
allow_send |
boolean | false |
Allow sending mail |
allow_drafts |
boolean | true |
Allow creating/editing own drafts |
allow_modify |
boolean | false |
Allow modifying messages (labels, trash) |
tool_prefixes |
string[] | ["mcp__google__", "google__"] |
Tool-name prefixes |
gcalendar_policy
Controls Google Calendar access. Defaults to read-only.
| Parameter | Type | Default | Description |
|---|---|---|---|
allow_read |
boolean | true |
Allow reading calendars/events |
allow_create_events |
boolean | false |
Allow creating events |
allow_modify_events |
boolean | false |
Allow updating/deleting events |
tool_prefixes |
string[] | ["mcp__google__", "google__"] |
Tool-name prefixes |
Working directory
block_working_dir_changes
Blocks shell commands that change the working directory (cd, pushd, git -C) or manage git worktrees. Parses chained and wrapped commands to prevent trivial bypasses.
| Parameter | Type | Default | Description |
|---|---|---|---|
block_cd |
boolean | true |
Block cd/chdir/pushd/popd and git -C |
block_worktree |
boolean | true |
Block git worktree add/move/remove |
allowed_dirs |
string[] | [] |
Directories cd may move into (including subdirectories) |
action |
string | "deny" |
Verdict for gated commands (deny or ask) |
shell_tools |
string[] | ["sys_os_shell"] |
Shell tools to parse |
Risk score
risk_score_policy
Accrues a per-session risk score from tool calls and sensitive data labels. Escalates guarded tools to ASK or DENY once the score exceeds a threshold.
| Parameter | Type | Default | Description |
|---|---|---|---|
threshold |
integer | 50 |
Score at which guarded tools escalate |
tool_points |
object | {} |
Tool name to points mapping, e.g. {"web_search": 10} |
sensitive_labels |
object | {} |
Data-classification label to points, e.g. {"Highly Confidential": 30} |
guarded_tools |
string[] | [] |
Tools gated once score reaches threshold |
escalate_action |
string | "ASK" |
Verdict for guarded tools over threshold |
initial_scores_by_actor |
object | {} |
Actor email to starting score offset |
state_key |
string | "risk_score" |
Session state key for the running score |
Routing
deny_trivial_to_expensive_model
Classifies user messages as TRIVIAL or COMPLEX using the server LLM. Denies trivial tasks from using expensive models. Requires the server to have an llm: config block.
| Parameter | Type | Default | Description |
|---|---|---|---|
expensive_models |
string[] | (required) | Model IDs to gate, e.g. ["databricks-claude-opus-4-6"] |
classification_prompt |
string | (builtin default) | System instructions for the classifier |
# server_config.yaml
llm:
model: databricks-gpt-5-4-mini
policies:
deny_trivial_opus:
type: function
handler: omnigent.policies.builtins.routing.deny_trivial_to_expensive_model
factory_params:
expensive_models:
- databricks-claude-opus-4-6
Writing custom policies
Policy function interface
A policy function receives an event dict and returns a response dict (or None to abstain).
from omnigent.policies.schema import PolicyEvent, PolicyResponse
def my_policy(event: PolicyEvent) -> PolicyResponse | None:
if event["type"] != "tool_call":
return None # abstain on non-tool phases
tool = event["data"]["name"]
if tool == "dangerous_tool":
return {"result": "DENY", "reason": "This tool is blocked."}
return {"result": "ALLOW"}
Factory form
For policies that need configuration, write a factory -- a function that accepts parameters and returns the actual evaluator:
def block_domains(blocked_domains: list[str]) -> callable:
blocked = frozenset(d.lower() for d in blocked_domains)
def evaluate(event: PolicyEvent) -> PolicyResponse | None:
if event["type"] != "tool_call" or event["target"] != "web_fetch":
return None
url = event["data"]["arguments"].get("url", "")
for domain in blocked:
if domain in url.lower():
return {"result": "DENY", "reason": f"Domain {domain} is blocked."}
return {"result": "ALLOW"}
return evaluate
Event and response examples
Event dict passed to the policy callable (example for a tool_call phase):
{
"type": "tool_call",
"target": "sys_os_shell",
"data": {
"name": "sys_os_shell",
"arguments": {"command": "rm -rf /tmp/data"}
},
"context": {
"actor": {"run_as": "alice@example.com", "client_id": "oauth_abc"},
"usage": {
"input_tokens": 1520,
"output_tokens": 340,
"total_tokens": 1860,
"total_cost_usd": 0.012
}
},
"session_state": {"call_count": 5},
"request_data": null
}
Response dict returned by the policy callable:
{
"result": "DENY",
"reason": "Destructive shell command blocked.",
"state_updates": [
{"key": "call_count", "action": "increment", "value": 1}
]
}
result is the only required field. Valid values: "ALLOW", "DENY", "ASK". Return None to abstain.
state_updates supports four actions: "set", "increment", "delete", "append".
Making policies discoverable
To make custom policies appear in the UI, export a POLICY_REGISTRY list from your module:
# myorg/policies.py
POLICY_REGISTRY = [
{
"handler": "myorg.policies.block_domains",
"kind": "factory",
"name": "Block Domains",
"description": "Block web access to specific domains.",
"params_schema": {
"type": "object",
"properties": {
"blocked_domains": {
"type": "array",
"items": {"type": "string"},
"description": "Domains to block"
}
},
"required": ["blocked_domains"]
}
}
]
Then register the module in the server config:
policy_modules:
- myorg.policies
Appendix: Admin policy REST API
After starting the server, admins can manage default policies at runtime through these endpoints:
# Create a server-wide policy
curl -X POST http://localhost:6767/v1/policies \
-H "Content-Type: application/json" \
-d '{
"name": "global_rate_limit",
"type": "python",
"handler": "omnigent.policies.builtins.safety.max_tool_calls_per_session",
"factory_params": {"limit": 200}
}'
| Method | Endpoint | Description |
|---|---|---|
POST |
/v1/policies |
Create a default policy |
GET |
/v1/policies |
List all default policies |
GET |
/v1/policies/{policy_id} |
Get a specific policy |
PATCH |
/v1/policies/{policy_id} |
Update (name, handler, enabled) |
DELETE |
/v1/policies/{policy_id} |
Remove a policy |
GET /v1/policy-registry lists all discoverable policies with parameter schemas -- useful for building admin UIs.
