Nanobot Adapter
The `NanobotAdapter` enforces rulesets on tool calls made through
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_registryDirect 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 = governedLoading 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 trackingThe 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 emittedAudit 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_idgroups tool calls into a session. Access it viagoverned.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'sToolRegistryrule). Non-string results from the inner registry are converted viastr(). -
Full interception: Unlike hook-based adapters, the enforced registry wraps the entire execution flow. Postcondition
redactandblockeffects are applied before the result is returned. This means the adapter can redact content before the LLM sees it.
Last updated on