How to Configure Policies
Policies are cross-cutting settings that affect multiple lifecycle phases simultaneously. Unlike providers — which each own a single phase — a policy coordinates behavior across tools, instructions, and post-processing.
Policies live at the top level of agent_setups.json, not nested under any provider. For the full JSON schema, see the Agent Configuration Reference.
List Policies
za agents policies list
| Policy | Description |
|---|---|
| Citations | Controls how the agent references search results in responses |
| Model | Selects a named LLM configuration for the request, within the setup's allowed names |
| Tool rules | Decides, before each model call, which tools the agent sees and which ask the user first |
Tool rules
Before each model call the agent asks its tool rules about every tool it has. A rule can hide a tool (the model does not see it and a call to it fails), make it ask the user first, waive asking, or load it on demand. Rules only narrow: any rule that hides a tool hides it, and a rule that asks first for a tool cannot be waived by another. A default ask (confirmation_default) is weaker: it holds only where no rule decides, so a user's Always allow waives it. A tool no rule has an opinion on is decided by the model from its prompts.

The agent's own tool settings come first, set by each provider as it lists its tools; rules then decide on top of them:
| Settings | Where they live | What they decide |
|---|---|---|
| Tools | sandbox_only_tools, sandbox_only_sources, direct_only_tools, direct_only_sources, defer_tools in tools_provider_configuration; defer_tools in each source's block | Who may call a tool: the model directly (direct_only_*) or Python in the REPL (sandbox_only_*); and loading a source's tools on demand, see Tool search |
| MCP | confirmation_default, defer_tools in mcp_tools_provider_configuration; defer_tools in each mount | A default ask for MCP tools, and loading them on demand. confirmation_default only applies where no rule decides, so a user's Always allow waives it |
On the Zeta Alpha platform, asking first for a specific tools source, MCP server or tool is a setting rule, the tenant's or the agent's, see Access rules. Setting rules come from the platform's access_rules rule source, which the agents SDK does not ship.
| Rule source | Where its settings live | What it decides |
|---|---|---|
skill_tools | the skills' allowed-tools; switched on with skill_tools_rule_source_configuration | A tool a skill names stays hidden until the skill is read |
On the Zeta Alpha platform three more rule sources apply:
| Rule source | What it decides |
|---|---|
access_rules | Who gets each tools source, MCP server or tool, whether it asks first and whether it loads on demand: the tenant's rules, then the agent's access_rules_rule_source_configuration, see Access rules |
confidentiality_labels | Once the chat retrieved a document whose label disables tools that may write, those tools are hidden. The tenant's confidentiality settings decide (features.confidentiality_settings, restrictions per label); an agent's confidentiality_labels_rule_source_configuration applies only when the tenant sets no such rule. |
user_choices | The user's own switches: MCP servers and tools in Connected apps (Always allow, Ask first, Off) and the sources switched off for the conversation. |
A user's Always allow therefore cannot waive a setting rule with "values": {"confirmation": "always"} (the agent's or the tenant's).
Tools the agent calls from Python with run_python follow the same rules: a hidden tool is not available, and a tool that asks first is declined, since Python cannot ask the user, unless the user allowed it for this chat. A rule's check_call and check_result decide those calls too, see Write your own rule source.
Access rules
The access_rules rule source runs on the Zeta Alpha platform only; an agent built with the SDK alone has no access or setting rules. An agent's own rules live in access_rules_rule_source_configuration, in the format of the tenant's rules: access rules (rules) and setting rules (settings), each naming principals and resources such as model:<name>, tools:<source>/<tool> or mcp:<server>. An agent's access rules can only forbid; its setting rules override the tenant's unless the tenant locked them. The block's enabled must be true for the agent's rules to run. An agent that always asks before sending mail:
{
"access_rules_rule_source_configuration": {
"enabled": true,
"settings": [
{
"name": "Ask before sending mail",
"resources": ["mcp:outlook/send_email"],
"values": { "confirmation": "always" }
}
]
}
}
A rule on a tool beats a rule on its source or server, so mcp:outlook set to always and mcp:outlook/list_mail set to never asks for every tool but list_mail. The format, the resource names and how tenant and agent rules combine are in Controlling who can use what.
"access_rules_rule_source_configuration": {
"rules": [
{ "name": "Only writers draft mail", "effect": "forbid", "principals": ["public"], "except_principals": ["user_role_id:writers"], "resources": ["mcp:ms365/create-draft-email"] },
{ "name": "GPT big for leads only", "effect": "forbid", "principals": ["public"], "except_principals": ["user_role_id:leads"], "resources": ["model:gpt-big"] }
],
"settings": [
{ "name": "Ask before drafting", "principals": ["public"], "resources": ["mcp:ms365/create-draft-email"], "values": { "confirmation": "always" } }
]
}
Rules on models also decide which models the agent offers a caller: a caller who loses the agent's default model runs on its next remaining one, and a caller left with none does not see the agent. Tenant admins are exempt from rules on models; rules on tools apply to them too.
Switching rule sources
Switch rule sources on or off per agent with tool_policy_configuration, like any source. Every rule source is off by default: an agent switches one on with "enabled": true in its <source>_rule_source_configuration block, for example "access_rules_rule_source_configuration": {"enabled": true, ...}. The agent editor in Platform Admin sets it when you add the agent's rules.
"tool_policy_configuration": { "exclude_sources": ["user_choices"] }
Rule source configuration blocks, access_rules_rule_source_configuration among them, and tool_policy_configuration come from the agent setup only: a request's bot_params cannot fill them.
Write your own rule source
A rule source is a ToolPolicySource (zav.agents_sdk.adapters.tool_policy):
source_name: the nametool_policy_configurationswitches it by, and the prefix of its<source_name>_rule_source_configurationblock.specificity: run order, lower first; each source works on what the ones before it left. The platform's tenant and agent rules run at10, the user's choices at20.enabled: set from the block'senabled, which defaults toFalse.async def apply(self, tools, transcript): returns the tools kept. Drop a tool to hide it. Change itsoptionsto ask first withask_first(options, source_name, required=...):required=Trueasks and no later source can waive it,required=Falseasks only where no source decided yet.waive_asking(options, source_name)lifts any ask except a required one.transcriptisNoneoutside a conversation (an MCP client listing the tools).async def check_call(self, call, transcript): optional; decides one call, with its arguments, before it runs. ReturnNoneto run it,CallDecision.decline(message, reason)to decline it (the model getsmessageas the tool result; the user seesreason, if given, when they open the call), orCallDecision.ask()to ask the user first. A decline from any rule wins over an ask. A rule's ask holds for that call only: the user can allow it once, not for the chat. In a delegated agent the ask followssub_agent_approvals, so withrunthe call runs without asking. Inputs the user edits on the card go throughcheck_callagain; a decline then stands.transcriptis the conversation up to a call the model made, the calls before it in the same model reply included, andNonefor a call made outside a conversation (Python in the REPL, an MCP client). For example, decline a call the current turn already made with the same arguments, or ask before mail goes outside the company.async def check_result(self, call, result, transcript): optional; decides what the model, the user and Python code get of a call's result.resultis the tool's own return value, a dict or a string for most tools. ReturnNoneto keep it,ResultDecision.replace(value, reason)to replace it, for example with the confidential parts redacted, orResultDecision.block(message, reason)to withhold it: the model getsmessageand the user sees the call as blocked. The user sees a replaced result marked as edited by a rule.reasonis optional and only the user sees it: writemessagefor the model ("tell the user you cannot share this") andreasonfor the user ("Board minutes are confidential."). When several rules replace a result, the user sees their reasons together. A replacement is shown and sent like the tool's own value, so keep the tool's shape: code that reads the result, and the UI that formats it, expect it. Each rule sees the result as the rules before it left it, and a block ends the chain. A tool that failed returns its error unchecked.
Calls made outside a conversation go through both, with nobody to ask. From Python in the REPL, what a rule declines, asks about or blocks raises PermissionError in the code, which can catch it. An MCP client calling the agent's tools served over MCP gets the rule's message as an error result.
Build it in an AgentDependencyFactory whose create returns it, and register the factory with AgentDependencyRegistry.register. The tool policy collects every registered rule source, and tool_policy_configuration.include_sources / exclude_sources switch it like any other:
from zav.agents_sdk import AgentDependencyFactory, AgentDependencyRegistry
from zav.agents_sdk.adapters.tool_policy import ToolPolicySource, ask_first
from zav.pydantic_compat import BaseModel, Field
class AskBeforeDeletingRuleSourceConfiguration(BaseModel):
enabled: bool = Field(False, description="Ask before any delete tool runs.")
class AskBeforeDeleting(ToolPolicySource):
source_name = "ask_before_deleting"
specificity = 15
def __init__(self, enabled: bool):
self.enabled = enabled
async def apply(self, tools, transcript):
return [
tool.copy(update={"options": ask_first(tool.options, self.source_name, required=True)})
if "delete" in tool.name
else tool
for tool in tools
]
class AskBeforeDeletingFactory(AgentDependencyFactory):
@classmethod
def create(
cls,
ask_before_deleting_rule_source_configuration: AskBeforeDeletingRuleSourceConfiguration = (
AskBeforeDeletingRuleSourceConfiguration()
),
) -> AskBeforeDeleting:
return AskBeforeDeleting(enabled=ask_before_deleting_rule_source_configuration.enabled)
AgentDependencyRegistry.register(AskBeforeDeletingFactory)
The agent switches it on with "ask_before_deleting_rule_source_configuration": {"enabled": true}.
Model
The model policy selects which named LLM configuration a request runs on. It is the only channel through which model choice travels at runtime, and it carries a name, never a configuration.
"llm_selection_configuration": { "llm_configuration_name": "fast" }
Selection rules:
- The agent's default is its own
llm_configuration_name. A requested name is honored only when it is the default or listed in the setup'sallowed_llm_configuration_names; otherwise the default applies. On the Zeta Alpha platform, the tenant's and the agent's access rules on models narrow both per user first. - Pinned in the agent's configuration, the block cannot be moved by callers. Left out, callers may supply it per request through
bot_params, and a parent agent may pass it to a sub-agent the same way; each agent validates against its own allowed names. - Allowed names also turn on the model picker on the chat page: users see the default and the allowed names that have an LLM configuration, and can switch between turns. A conversation reopens on the model of its latest turn. A pinned model hides the picker.
- For each model the picker shows its maker, its model id (for Azure, the deployment name), where it is hosted (provider, plus the region for AWS), and its reasoning effort on OpenAI and Azure OpenAI. Every member of the tenant can read these details; endpoints and credentials are never exposed. A configuration with a custom API base shows no hosting.
- Every LLM consumer in the request follows the selection: the agent's own client and every LLM-using tool.
- The
taskanddelegatetools expose anllm_configuration_nameparameter to the agent when the configurator opts in (allow_llm_selectiononsub_agent_tool_source_configuration,agent_delegation_provider_configuration, or per delegable agent).
za agents policies show model
za agents policies configure model
Citations
Citation policy controls how the agent references search results in its responses. It spans three phases:
| Phase | What happens |
|---|---|
| 🔧 Tools | IndexToolsSource registers each search hit in the CitationStore |
| 📋 Prompt | CitationInstructionSource tells the LLM how to format references |
| 📝 Post-process | CitationProcessor resolves citation markers into structured evidence |
All three components share the same CitationConfiguration instance through DI — changing the strategy changes behavior across all phases at once.
za agents policies show citations # show citation fields, current values, defaults
za agents policies configure citations # interactively set citation strategy
Available Strategies
| Strategy | How it works | When to use |
|---|---|---|
inline_url | LLM cites as [[N]](url "extract") | Production default — web UIs that render markdown |
short_id | LLM uses 8-char hex IDs; processor resolves to URLs | When URLs are too long for context |
deferred | LLM writes naturally; processor matches text to results | When you want clean prose with citations added after |
sup_numeric | LLM uses superscript [^N] notation | Academic-style footnotes |
How It Works
The CitationStore is a request-scoped singleton — the same instance is shared by all components within a single request. During tool execution, every search hit is registered with its metadata, short ID, and source URL. After the LLM generates its response, the CitationProcessor reads from the same store to resolve inline markers into structured evidence objects with source URLs, page ranges, and relevance scores.
The citation instruction source reads the configured strategy and injects the appropriate formatting rules into the system prompt, so the LLM knows which citation format to use.
For architecture details, see Cross-Cutting Concerns.