Edictum
Python Adapters

Nanobot Adapter

The `NanobotAdapter` enforces rulesets on tool calls made through

AI Assistance

Right page if: you are adding Edictum to a nanobot agent and need GovernedToolRegistry as a drop-in replacement for ToolRegistry. Wrong page if: you are using a different multi-agent framework -- see https://docs.edictum.ai/docs/adapters/overview. Gotcha: use principal_from_message() to map InboundMessage to a Principal for multi-channel agents (Telegram, Discord, Slack). Use for_subagent() to propagate rulesets to child agents -- it shares rulesets but tracks session limits independently.

The NanobotAdapter enforces rulesets on tool calls made through nanobot agents. It provides a GovernedToolRegistry that is a drop-in replacement for nanobot's ToolRegistry, wrapping every tool execution with runtime checks.

Getting Started

Install

pip install edictum[yaml]

No additional framework dependencies are needed. The adapter uses duck typing and does not import from nanobot at module level.

Create adapter

from edictum import Edictum, Principal
from edictum.adapters.nanobot import NanobotAdapter

guard = Edictum.from_yaml("rules.yaml")
adapter = NanobotAdapter(
    guard=guard,
    session_id="session-001",
    principal=Principal(user_id="telegram:user123", role="user"),
)

Wrap registry

# Wrap the agent's tool registry
governed_registry = adapter.wrap_registry(agent.tool_registry)

# Replace the agent's registry with the enforced one
agent.tool_registry = governed_registry

Direct GovernedToolRegistry

For more control, create GovernedToolRegistry directly:

from edictum import Edictum, Principal
from edictum.adapters.nanobot import GovernedToolRegistry

guard = Edictum.from_yaml("rules.yaml")
governed = GovernedToolRegistry(
    inner=agent.tool_registry,
    guard=guard,
    session_id="session-001",
    principal=Principal(user_id="telegram:user123", role="user"),
)
agent.tool_registry = governed

Loading Rulesets

Rulesets can be loaded from a YAML file, the built-in nanobot template, or defined in Python:

# From a YAML ruleset
guard = Edictum.from_yaml("rules.yaml")

# From the built-in nanobot template
guard = Edictum.from_template("nanobot-agent")

# From Python rulesets
from edictum import deny_sensitive_reads
guard = Edictum(rules=[deny_sensitive_reads()])

The nanobot-agent template includes approval rulesets for shell commands, sub-agent spawning, cron jobs, and MCP tools, plus path-based write/edit restrictions and session limits. See Templates for full details.

Block Behavior

Nanobot's ToolRegistry.execute() returns strings. The adapter returns block messages as strings so the LLM can see the block reason and adjust:

result = await governed.execute("write_file", {"path": "/etc/passwd", "content": "..."})
# result == "[DENIED] Cannot write outside workspace: /etc/passwd"

In enforce mode, blocked calls return "[DENIED] {reason}" and the tool never executes. In observe mode, the tool executes normally and a CALL_WOULD_DENY audit event is emitted.

Principal from InboundMessage

Map nanobot's InboundMessage to an Edictum Principal:

from edictum.adapters.nanobot import NanobotAdapter

# message is a nanobot InboundMessage
principal = NanobotAdapter.principal_from_message(message)
# Principal(
#     user_id="telegram:user123",
#     role="user",
#     claims={"channel": "telegram", "channel_id": "chat456"},
# )

Use this with a principal_resolver for per-message principal resolution:

def resolve_principal(tool_name: str, tool_input: dict) -> Principal:
    # Look up the current message's sender
    return NanobotAdapter.principal_from_message(current_message)

governed = GovernedToolRegistry(
    inner=registry,
    guard=guard,
    principal_resolver=resolve_principal,
)

Sub-agent Rulesets

When SubagentManager creates child agents, propagate rulesets with for_subagent():

child_registry = governed.for_subagent(session_id="child-001")
# child_registry shares the same guard and rulesets
# but has its own session for independent limit tracking

The child registry inherits the parent's principal and principal_resolver.

Approval Workflows

Rulesets with action: ask trigger the approval flow. If the guard has an approval_backend, the adapter requests approval and waits for a decision:

from edictum import Edictum
from edictum.approval import LocalApprovalBackend

guard = Edictum.from_template(
    "nanobot-agent",
    approval_backend=LocalApprovalBackend(),
)

If no approval backend is configured, approval-required calls return "[DENIED] Approval required but no approval backend configured".

Observe Mode

Deploy rulesets without enforcement to see what would be blocked:

guard = Edictum.from_yaml("rules.yaml", mode="observe")
governed = GovernedToolRegistry(inner=registry, guard=guard)

result = await governed.execute("exec", {"command": "rm -rf /"})
# Tool executes normally; CALL_WOULD_DENY audit event emitted

Audit and Observability

Every tool call produces structured audit events:

from edictum import Edictum
from edictum.audit import FileAuditSink, RedactionPolicy

redaction = RedactionPolicy()
sink = FileAuditSink("audit.jsonl", redaction=redaction)

guard = Edictum.from_yaml(
    "rules.yaml",
    audit_sink=sink,
    redaction=redaction,
)
governed = GovernedToolRegistry(inner=registry, guard=guard)

Allowed calls emit CALL_ALLOWED + CALL_EXECUTED. Blocked calls emit CALL_DENIED. Observed blocks emit CALL_WOULD_DENY.

Session Tracking

The adapter tracks per-session state automatically:

  • session_id groups tool calls into a session. Access it via governed.session_id.
  • Attempt count increments before every rule evaluation.
  • Execution count increments only when a tool actually runs.
  • Call index is a monotonic counter within the registry instance.

Known Limitations

  • String results only: GovernedToolRegistry.execute() always returns a string (matching nanobot's ToolRegistry rule). Non-string results from the inner registry are converted via str().

  • Full interception: Unlike hook-based adapters, the enforced registry wraps the entire execution flow. Postcondition redact and block effects are applied before the result is returned. This means the adapter can redact content before the LLM sees it.

Last updated on

On this page