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.
Scope can be configured two ways:
- Structured scope via
scope(full :class:ScopeConfig) or thepresetshortcut (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 additionalrubrictext 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
Section titled “needs_permission_bridge”needs_permission_bridge: boolGuard needs the bridge when scope has ASK capabilities.
HeadlessSessionPolicy
Section titled “HeadlessSessionPolicy”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.
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).
needs_permission_bridge
Section titled “needs_permission_bridge”needs_permission_bridge: boolWhether 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
Section titled “required_facets”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
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.