Skip to main content

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
PolicyDescription
CitationsControls how the agent references search results in responses
ModelSelects a named LLM configuration for the request, within the setup's allowed names
Tool rulesDecides, 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.

Tool rules and tool search in the agent editor's Policies

The agent's own tool settings come first, set by each provider as it lists its tools; rules then decide on top of them:

SettingsWhere they liveWhat they decide
Toolssandbox_only_tools, sandbox_only_sources, direct_only_tools, direct_only_sources, defer_tools in tools_provider_configuration; defer_tools in each source's blockWho 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
MCPconfirmation_default, defer_tools in mcp_tools_provider_configuration; defer_tools in each mountA 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 sourceWhere its settings liveWhat it decides
skill_toolsthe skills' allowed-tools; switched on with skill_tools_rule_source_configurationA tool a skill names stays hidden until the skill is read

On the Zeta Alpha platform three more rule sources apply:

Rule sourceWhat it decides
access_rulesWho 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_labelsOnce 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_choicesThe 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 name tool_policy_configuration switches it by, and the prefix of its <source_name>_rule_source_configuration block.
  • specificity: run order, lower first; each source works on what the ones before it left. The platform's tenant and agent rules run at 10, the user's choices at 20.
  • enabled: set from the block's enabled, which defaults to False.
  • async def apply(self, tools, transcript): returns the tools kept. Drop a tool to hide it. Change its options to ask first with ask_first(options, source_name, required=...): required=True asks and no later source can waive it, required=False asks only where no source decided yet. waive_asking(options, source_name) lifts any ask except a required one. transcript is None outside 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. Return None to run it, CallDecision.decline(message, reason) to decline it (the model gets message as the tool result; the user sees reason, if given, when they open the call), or CallDecision.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 follows sub_agent_approvals, so with run the call runs without asking. Inputs the user edits on the card go through check_call again; a decline then stands. transcript is the conversation up to a call the model made, the calls before it in the same model reply included, and None for 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. result is the tool's own return value, a dict or a string for most tools. Return None to keep it, ResultDecision.replace(value, reason) to replace it, for example with the confidential parts redacted, or ResultDecision.block(message, reason) to withhold it: the model gets message and the user sees the call as blocked. The user sees a replaced result marked as edited by a rule. reason is optional and only the user sees it: write message for the model ("tell the user you cannot share this") and reason for 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's allowed_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 task and delegate tools expose an llm_configuration_name parameter to the agent when the configurator opts in (allow_llm_selection on sub_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:

PhaseWhat happens
🔧 ToolsIndexToolsSource registers each search hit in the CitationStore
📋 PromptCitationInstructionSource tells the LLM how to format references
📝 Post-processCitationProcessor 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​

StrategyHow it worksWhen to use
inline_urlLLM cites as [[N]](url "extract")Production default — web UIs that render markdown
short_idLLM uses 8-char hex IDs; processor resolves to URLsWhen URLs are too long for context
deferredLLM writes naturally; processor matches text to resultsWhen you want clean prose with citations added after
sup_numericLLM uses superscript [^N] notationAcademic-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.