Normandy.Agents.ConfigTemplate
(normandy v2.2.0)
View Source
Splits a %BaseAgentConfig{} into a serializable, secret-free template
(persisted via SessionStore.save_config_template/3) and reconstructs a full
config on any node from template + node-local supplement + credential token.
Excluded from the template (resolved node-locally instead): client (built from
the token), toolset and before/after_hooks (from the supplement), and
mcp_servers (from the supplement; the template records only that it was set, and
rebuild/3 raises if the supplement lacks it).
current_user_input and resolved_context_window are transient and not kept.
Everything else travels in the template: model and sampling settings, tool
limits, prompt, schemas, memory cap, initial_memory, the retry and
circuit_breaker options (the breaker itself is the node's shared breaker for that key,
BaseAgentConfig.circuit_breaker_key/1), JSON retry settings, guardrails, tool_context,
behaviour module refs and hook entries (Normandy.Behaviours.Hooks; their opts must
be plain data and hold no secrets). These must be plain data: from_config/2 raises
on a function or pid (e.g. a :retry_if fun). tool_context must hold no secrets:
the template is stored in plaintext.
Templates saved by Normandy 1.0
rebuild/3 reads templates written by Normandy 1.0.0 and by 2.0. A 1.0 template lacks
the fields 2.0 added (they rebuild with their defaults) and differs in three ways, which
rebuild/3 translates:
max_tokens: nil(the agent set none) rebuilds withBaseAgentConfig.default_max_tokens/0;behaviours_refscarriessession_storeandsession_registry, slots 1.0 never read; they are dropped;- a
Normandy.Behaviours.Compactor.WindowManagerref pins the window with:max_tokens, which 2.0 calls:context_window. A custom compactor's opts are left as they are.
Any other field, a behaviours_refs slot or a node_local_fields entry that neither
version writes, and a missing required field, raise ArgumentError naming it. Templates
written by unreleased 2.0 builds are not translated.
Trust boundary
The session store is as trusted as your code. Whoever can write it (a shared Redis, a Postgres role another service uses) decides, on the next rehydrate on any node:
- the policy, budget, compactor and model catalog modules and their opts, the
guardrails and the hooks (removing them, or
PolicyEngine.AllowAll); - the credential provider and its opts (
Turn.Servercalls it to get a token); tool_context, so another tenant's memory namespace;template_id, so which node-local supplement, and so which toolset, is used;- the saved server options (
on_approval_timeout, the timeouts); - the turn state, including the input of a call parked for approval, and the conversation history.
Keep the store single-tenant, or give Normandy's role its own database or ACL'd key
prefix. Postgres and Redis decode stored bytes with binary_to_term(_, [:safe]), which
still builds local functions.
Normandy checks what it can without keys. check_loaded!/1 runs on every template
Turn.Server loads, before anything in it is used, and in rebuild/3: it refuses a
function, pid, reference or port anywhere in the template, a behaviour ref whose module
does not implement its behaviour (policy a Normandy.Behaviours.PolicyEngine,
budget a BudgetTracker, credential a CredentialProvider, compactor a
Compactor, model_catalog a ModelCatalog) and a guard module that does not
implement Normandy.Guardrails.Guard. A supplement's :pinned fixes security settings
on the node: with pinned: [policy: ref, guardrails: g, credential: ref] a template
whose policy, guardrails or credential differs raises, before the credential
provider is called. :pinned takes :policy, :budget, :credential, :compactor,
:model_catalog, :hooks and :guardrails; tool_context, template_id, turn
state and history cannot be pinned. Templates are not signed (a known limit).
Summary
Functions
Checks a template read from a session store before anything in it is used (see "Trust
boundary"). Turn.Server calls it on every template it loads, and rebuild/3 calls it
first.
Builds the plain-data template of config, to store under template_id.
Builds the template of config as from_config/2 does, with resume_policy set to
:lazy or :eager.
Rebuilds a BaseAgentConfig from a template and a supplement.
Functions
@spec check_loaded!(map()) :: :ok
Checks a template read from a session store before anything in it is used (see "Trust
boundary"). Turn.Server calls it on every template it loads, and rebuild/3 calls it
first.
Raises ArgumentError naming the field for a function, pid, reference or port anywhere
in the template, a behaviours_refs slot that is not {module, keyword} or whose module
is not loaded on this node or does not implement the slot's behaviour, and a guard in
guardrails whose module does not implement Normandy.Guardrails.Guard. A missing or
unknown field or slot is left to rebuild/3, which names it. Values are never shown.
@spec from_config(Normandy.Agents.BaseAgentConfig.t(), String.t()) :: map()
Builds the plain-data template of config, to store under template_id.
The template holds the fields the moduledoc lists and a resume_policy of :lazy; use
from_config/3 for :eager. Fields that cannot be stored (the client, the toolset,
mcp_servers) are left out, and node_local_fields records that mcp_servers was set.
Raises ArgumentError when one of initial_memory, retry, circuit_breaker,
enable_json_retry, json_retry_max_attempts, guardrails, tool_context or
max_tool_result_chars, or the opts of a hook entry, holds a function, pid, reference or
port, or when a hook entry is malformed.
@spec from_config(Normandy.Agents.BaseAgentConfig.t(), String.t(), :lazy | :eager) :: map()
Builds the template of config as from_config/2 does, with resume_policy set to
:lazy or :eager.
:eager makes a Turn.Server that loads the template resume an in-flight turn as soon as
it starts; :lazy waits for the next call. Raises as from_config/2 does.
@spec rebuild(map(), Normandy.Behaviours.AgentTemplate.supplement(), String.t()) :: Normandy.Agents.BaseAgentConfig.t()
Rebuilds a BaseAgentConfig from a template and a supplement.
Translates a template saved by Normandy 1.0 (see the moduledoc). Raises
ArgumentError naming the field for one it cannot translate, for a max_tokens that is
neither nil (1.0) nor a positive integer, for a compaction setup that can't size its
window or whose compactor opts are malformed, and for a malformed hook entry in
the template (shape, :on_error and plain-data opts; whether the hook module exists on
this node is checked later, by Turn.Server). Raises ArgumentError for a supplement
that still uses the 1.x key :tool_registry, whose :toolset is not a
%Normandy.Tools.Toolset{} or nil, or whose toolset holds a struct whose module does not
implement Normandy.Tool.