Component Development · Part 3 of 3 — Permissions
Defining Component Permissions
This part is mainly for components that bridge a third-party messaging channel. Once other people's messages arrive through an interface you wrote, you have to answer two questions: may the sender call this component's commands (TOOL_POLICY), and which tools may the task triggered by that push use (TRIGGER_TOOL_POLICY).
Prerequisite: the component can receive external messages and hand them to the Agent. Purely local utility components take no inbound messages and rarely need this part.
1. Command level: who may call which command
Declare TOOL_POLICY on the component class. Keys are bare command names (no component prefix); values decide who is allowed.
class NapcatComponent(BaseCell):
# Keys are bare command names, no component prefix
TOOL_POLICY = {
"reset_config": "deny", # nobody
"add_user": "admin", # admins only
"shutdown": {"mode": "blacklist", "users": ["12345"]}, # deny listed users
"send_private": {"mode": "whitelist", "users": ["10001"]}, # listed users only
}
TOOL_POLICY_DEFAULT = "allow" # undeclared commands allowed; set to "deny" to block them allFive values
| Value | Effect |
|---|---|
"allow" | Allowed (default) |
"deny" | Nobody |
"admin" | Admins only |
{"mode": "whitelist", "users": [...]} | Listed users only |
{"mode": "blacklist", "users": [...]} | Everyone except listed users |
TOOL_POLICY_DEFAULT governs undeclared commands: allow by default; switch to deny so only explicitly allowed commands work, which suits sensitive components.
2. How admin is decided
BaseCell already implements it: it reads tool_allowed from platform_context, falling back to is_admin. The QQ channel passes tool_allowed automatically, so most components need no code. Override is_policy_admin to plug in your own admin list.
# Default implementation (built into BaseCell)
# tool_allowed wins, then is_admin, otherwise not an admin
# Override to use your own admin list
def is_policy_admin(self, platform_context=None) -> bool:
user_id = str((platform_context or {}).get("user_id") or "")
return user_id in {"10001", "10002"} # your admin QQ numbersAny exception inside this check is treated as not-an-admin, so never raise here — return False yourself.
3. Full example: admins manage cities, everyone queries weather
add_city / remove_city require an admin; query is undeclared and therefore allowed.
class WeatherBot(BaseCell):
TOOL_POLICY = {
"add_city": "admin", # admin only
"remove_city": "admin", # admin only
# query is undeclared -> allowed for everyone
}
def is_policy_admin(self, platform_context=None) -> bool:
user_id = str((platform_context or {}).get("user_id") or "")
return user_id in {"10001", "10002"}
def _cmd_query(self, city: str = "", **kwargs) -> Dict[str, Any]:
return {"success": True, "weather": f"{city} sunny 25C"}
def _cmd_add_city(self, city: str = "", **kwargs) -> Dict[str, Any]:
...A user asking for Beijing weather runs normally; asking to add Shanghai makes the agent call add_city, which is rejected, and the agent tells the user they lack permission.
# The call to add_city is blocked; the tool returns:
{
"success": False,
"error": "命令 add_city 仅管理员可用",
"_source": "component:weather_bot"
}
# The agent reports the lack of permission instead of retrying4. Task level: which tools a triggered task may use
TOOL_POLICY only governs this component's own commands. To restrict what tasks triggered through this component may use (shell, sub_agent, ...), declare TRIGGER_TOOL_POLICY. It accepts tool names and tool.command keys for command-level precision.
class NapcatComponent(BaseCell):
# Messages relayed through this component may not use these tools
TRIGGER_TOOL_POLICY = {
"mode": "blacklist",
"tools": ["shell", "sub_agent", "qq_files.download"],
}Effect: a QQ user message triggers the agent through this component; shell is blocked, qq_files.download is blocked, other qq_files commands are unaffected. The trigger request's source must equal the component's cell_name for the declaration to apply.
class MonitorComponent(BaseCell):
# Alert-triggered tasks may use only these two tools
TRIGGER_TOOL_POLICY = {
"mode": "whitelist",
"tools": ["monitor", "napcat.send_msg"],
}
def _on_alert(self, alert):
"""Alert callback: triggers the agent with the restriction applied"""
httpx.post(f"{self._base_url}/api/component/event", json={
"session_id": "monitor:alerts",
"message": f"CPU over threshold: {alert}",
"source": self.cell_name, # matched against the declaration above
"platform_context": {
"platform": "monitor",
"user_id": alert.user_id,
},
})5. Passing permissions per trigger request
No code declaration needed: put the policy in platform_context of /api/component/event.
| Form | Effect |
|---|---|
"tool_allowed": false | Enable the system default blacklist (about 20 entries: shell, file, component, sub_agent, ...) — right for relaying ordinary user messages |
tool_policy blacklist | Custom blacklist, merged with the default one (add-only) |
tool_policy whitelist | Only the listed tools are allowed, everything else is denied (default blacklist not involved) |
POST /api/component/event
{
"session_id": "qq:12345:group",
"message": "handle this for me",
"source": "napcat",
"platform_context": {
"platform": "qq",
"user_id": "12345",
"tool_allowed": false
}
}Relay example: ordinary users get qq_files only; admins carry no policy field and are unrestricted.
def _trigger_agent(self, message: str, user_id: str):
is_admin = user_id in self._admins
ctx = {"platform": "qq", "user_id": user_id, "message_type": "group"}
if not is_admin:
ctx["tool_policy"] = {"mode": "whitelist", "tools": ["qq_files", "napcat.send_msg"]}
# admins carry no policy field -> unrestricted
httpx.post(f"{self._base_url}/api/component/event", json={
"session_id": f"qq:{user_id}",
"message": message,
"source": self.cell_name,
"platform_context": ctx,
})Priority: an explicit tool_policy in the request wins, overriding the component's declaration. Components without TRIGGER_TOOL_POLICY behave exactly as before.
Decision table
| Goal | Where to put it |
|---|---|
| Block a command for everyone | TOOL_POLICY = {"xxx": "deny"} |
| Make a command admin-only | "xxx": "admin" |
| Restrict a command to listed users | "xxx": {"mode": "whitelist", "users": [...]} |
| Block other tools for tasks triggered here | TRIGGER_TOOL_POLICY blacklist |
| Allow only a few tools for tasks triggered here | TRIGGER_TOOL_POLICY whitelist |
| Set permissions per trigger request | platform_context + tool_allowed / tool_policy |
Pitfalls
- Key formats differ: TOOL_POLICY keys are bare command names (add_user); TRIGGER_TOOL_POLICY keys are tool names or tool.command (qq_files.download). A wrong key does not error — it silently does nothing.
- Whitelist bypasses the default blacklist: listing shell allows shell. Use whitelists only for trusted callers.
- Blacklist is add-only: blacklist mode merges with the system default, so to allow a default-blocked tool you must switch to whitelist.
- Command-level matching needs the command argument: a key like qq_files.download only matches when command is passed explicitly; otherwise matching falls back to the bare tool name.
- WebUI calls carry no user_id: whitelist commands are therefore blocked in the WebUI too, while blacklist mode allows them.
Blocking is a runtime rejection, not a removal from the tool list: forbidden tools remain visible to the LLM and fail on call. To stop the model from trying at all, constrain it via the prompt, not the policy table.
