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.

Scope can be configured two ways:

  • Structured scope via scope (full :class:ScopeConfig) or the preset shortcut (recon_only, standard_pentest, red_team). The scope model defines capability categories (reconnaissance, exploitation, credential access, etc.) and target boundaries (networks, domains, services, cloud resources, identities). The resolved scope is rendered to a natural-language rubric for the judge. Any additional rubric text is appended after the scope rubric.
  • Freeform rubric via rubric — a plain-text string or YAML path layered on top of the safety-floor default.

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).

Transcript strategies:

  • rubric_only — no transcript, cheapest.
  • intent_only — system + user-authored messages only.
  • intent_plus_calls (default) — adds tool-call sequence, no results.
  • intent_plus_outputs_summary — adds LLM-summarized tool results.
  • full — entire trajectory including assistant prose.

Example::

# TUI — preset shortcut:
# /policy guard judge_model=anthropic/claude-haiku-4-5 preset=standard_pentest
# TUI — freeform rubric:
# /policy guard judge_model=anthropic/claude-haiku-4-5 rubric="In-scope: api.example.com"
# API — full scope config:
POST /api/sessions/{id}/policy
{
"name": "guard",
"judge_model": "anthropic/claude-haiku-4-5",
"scope": {
"preset": "standard_pentest",
"boundaries": {
"in_scope": [{"cidr": "10.0.1.0/24", "label": "DMZ"}],
"out_of_scope": [{"host": "10.0.1.5", "label": "monitoring"}]
},
"capabilities": {
"lateral_movement": {"policy": "deny"},
"credential_access": {"password_spraying": "allow"}
}
}
}
hooks: list[Hook]

Inherited step-budget hooks plus the judge gate.

needs_permission_bridge: bool

Guard needs the bridge when scope has ASK capabilities.

Autonomous mode with an optional step budget and no human in the loop.

The runtime reads is_autonomous=True and resolves ask_user() to deny instantly without touching any transport. When set, max_steps is enforced by a GenerationStep hook that emits Finish(reason="max_steps=N reached") once the turn has run max_steps model turns. Tool fan-out and duplicate lifecycle events do not consume additional budget. Explicit None keeps the session autonomous without adding a policy step ceiling. 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).

needs_permission_bridge: bool

Whether the runtime should attach a PermissionBridge to the agent.

Defaults to True for interactive (non-autonomous) policies. Subclasses may override to request the bridge even in autonomous mode (e.g. guard policies with ASK capabilities).

required_facets() -> set[PolicyFacet]

Policy facets this policy needs the engine to honor.

Mirrors an engine’s :meth:describe_enforcement; the runtime reconciles the two (see dreadnode.policies.reconciliation and CAP-EGOV-*). The base requires autonomy handling and — when a human is in the loop — tool-approval HITL. Subclasses extend.

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.