Skip to content

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-denies ask_user() (the runtime sees is_autonomous=True and short-circuits the prompt) and attaches a max-step hook that emits Finish once 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-denies ask_user() when true.
  • display_label — short status-bar string when is_autonomous is true ("auto", "strict", …). Defaults to empty.

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 carry tool_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.

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.

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.

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-denies ask_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(name: str) -> type[SessionPolicy] | None

Look up a registered policy class by name.

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() -> list[str]

Return sorted list of policy names currently in the registry.

resolve_policy(spec: _PolicySpec) -> SessionPolicy

Resolve a policy spec from the API into a policy instance.

spec may be:

  • None or "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.