dreadnode.policies
API reference for the dreadnode.policies module.
Per-session behavioral policies — agent-control hooks bound to a session.
A :class:SessionPolicy is a Pydantic-modelled class with hook methods,
mirroring the :class:~dreadnode.agents.tools.Toolset pattern: subclass,
declare config as fields, decorate methods with @hook(EventType),
and the runtime collects them via :meth:SessionPolicy.get_hooks at
turn start.
Two shipped implementations:
- :class:
InteractiveSessionPolicy— today’s TUI behavior. No continuation hooks;ask_user()flows through the runtime’s per-turn handler which publishes to both transports and awaits. - :class:
HeadlessSessionPolicy— autonomous mode. Auto-deniesask_user()(the runtime seesis_autonomous=Trueand short-circuits the prompt) and attaches a max-step hook that emitsFinishonce a configurable cap is hit.
Policies are resolved by name via :func:resolve_policy so clients can
request a mode with a simple string or \{"name": ..., **params\} dict
without importing Python classes across process boundaries.
Class-level metadata fields the runtime and TUI read for status UI:
name— registry key. Required.is_autonomous— whether the session has no human in the loop. The TUI tags labels and gates background-task notifications by this. The runtime auto-deniesask_user()when true.display_label— short status-bar string whenis_autonomousis true ("auto","strict", …). Defaults to empty.
GuardSessionPolicy
Section titled “GuardSessionPolicy”Headless mode + LLM-judged tool-call gating.
The runtime auto-denies ask_user() (inherited
is_autonomous=True), enforces a per-turn step budget (inherited
max_steps), and runs every tool call past a
:class:ProcessJudge for allow/deny.
The judge sees a slice of the live trajectory selected by
transcript_strategy. The default intent_plus_calls shows the
user task plus the prior tool-call sequence (no responses) — the same
cut Anthropic’s auto-mode uses for its own per-call gating. The other
options trade prompt size and injection surface against how much
context the judge has to reason with:
rubric_only— no transcript. Judge sees only the proposed call against the rubric. Cheapest, lowest signal.intent_only— system + user-authored messages. The original smallest cut, useful when the rubric encodes everything you care about and you don’t want intermediate state to drift the judge.intent_plus_calls(default) — adds assistant messages that carrytool_calls. No tool result content reaches the judge, so attacker-controllable output can’t carry a prompt injection back at the judge.intent_plus_outputs— adds tool-result messages instead of tool calls. The judge sees responses without knowing which call produced them — useful for testing whether the responses are net positive given the injection surface they introduce.full— the entire trajectory. Maximum context, maximum surface.
The captured intent is also trimmed to fit the judge model’s context
window: the system message and the original user task always survive,
older tool-call/result messages drop first when the rendered transcript
would exceed the budget. The trim emits a process_judge.intent_trimmed
metric with dropped_messages and strategy attributes.
Example::
# Mid-session swap from the TUI:# /policy guard judge_model=anthropic/claude-haiku-4-5# /policy guard judge_model=anthropic/claude-haiku-4-5 transcript_strategy=full
# Or from the API:POST /api/sessions/{id}/policy{ "name": "guard", "judge_model": "anthropic/claude-haiku-4-5", "rubric": "In-scope: api.example.com only", "transcript_strategy": "intent_plus_calls", "max_steps": 20}hooks: list[Hook]Inherited step-budget hooks plus the judge gate.
HeadlessSessionPolicy
Section titled “HeadlessSessionPolicy”Autonomous mode — bounded execution, no human in the loop.
The runtime reads is_autonomous=True and resolves
ask_user() to deny instantly without touching any
transport. max_steps is enforced by an AgentStep hook that
emits Finish(reason="max_steps=N reached") once the turn has
run max_steps react cycles. The reset on AgentStart makes
the counter per-turn rather than per-session, so a long chat with
multiple turns each gets the full budget.
InteractiveSessionPolicy
Section titled “InteractiveSessionPolicy”Default policy — no continuation hooks, no special prompt handling.
The runtime’s per-turn human-prompt handler does the publish/await
dance directly when is_autonomous is false. This policy holds
no state and contributes no hooks; it exists so the
"interactive" registry key resolves to a real type.
SessionPolicy
Section titled “SessionPolicy”Session-scoped agent-event hooks.
Subclass and decorate methods with @hook(EventType). The
runtime calls :meth:get_hooks at turn start to collect bound
Hook instances, walking the MRO so inherited hooks are
included and per-class overrides win.
Class-level metadata fields:
name— registry key.is_autonomous— runtime auto-deniesask_user()when true.display_label— short label rendered by the TUI in autonomous sessions.
Per-policy configuration goes in normal Pydantic fields (e.g.
HeadlessSessionPolicy.max_steps). extra="forbid" makes
typos in resolve_policy payloads fail loudly. Hook is in
ignored_types so the metaclass leaves @hook-decorated
methods alone instead of trying to interpret them as fields —
same trick :class:~dreadnode.agents.tools.Toolset uses for
ToolMethod (which sidesteps it by inheriting from
property).
hooks: list[Hook]All hooks declared on this policy, bound to self.
Walks the MRO and returns every attribute that is a Hook
descriptor, bound via :meth:Hook.__get__. Inherited hooks
are included; subclass attributes of the same name shadow
superclass ones (first occurrence in MRO order wins,
mirroring :meth:~dreadnode.agents.tools.Toolset.get_tools).
get_policy_class
Section titled “get_policy_class”get_policy_class(name: str) -> type[SessionPolicy] | NoneLook up a registered policy class by name.
register_policy
Section titled “register_policy”register_policy( cls: type[SessionPolicy], *, name: str | None = None, replace: bool = False,) -> type[SessionPolicy]Register a policy class into the global registry.
The registry key defaults to cls.name; pass name to
override. Re-registering an existing name is a no-op unless
replace=True. Returns the class unchanged so this function
can be used as a decorator.
Capabilities ship policies by placing files under policies/;
the capability loader picks them up and routes them through this
function.
registered_policy_names
Section titled “registered_policy_names”registered_policy_names() -> list[str]Return sorted list of policy names currently in the registry.
resolve_policy
Section titled “resolve_policy”resolve_policy(spec: _PolicySpec) -> SessionPolicyResolve a policy spec from the API into a policy instance.
spec may be:
Noneor"interactive"→ default interactive policy- a string matching a registered name → policy with default params
- a dict
\{"name": ..., **params\}→ policy with keyword params
Unknown names raise ValueError so mis-typed policy names in a
request payload fail loudly at session-create time instead of
silently falling back to interactive.