简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

Scope

Note


This feature requires ErisPulse 2.8.0+.

Scope answers four questions: which modules are available, whether events from certain sources should be received, what text a module processes, and what actions a module can perform externally.
Control is entirely user-driven: at the upper level of module / adapter / processor / outbound call registration (configured via ErisPulse.scope or runtime sdk.scope), declarations are made, and the event pipeline automatically reads and executes them at entry, processor filtering, and outbound gate.

Dimension What is controlled Rejection behavior Configuration path
① Module Which modules are available (platform / Bot / session three levels) Filtered modules do not trigger or reply (intercept broadcast scope.blocked event; matched commands are still claimed and blocked) scope.platforms / bots / sessions
② Identity Whether to receive events (adapter / Bot / session / user four levels) Completely discard at entry (intercept broadcast scope.blocked event) scope.identity.*
③ Outbound Which outbound calls a module can initiate (messages / API / requests, method-level white/blacklists) Fail response (retcode=34601) scope.actions

Related systems: Commands are special message event processors, with their user allow/deny lists (ACL) and implementation parameter overrides handled by the command system itself (ErisPulse.event.command).
See Event Handling Introduction and Configuration Guide.

{!--< tips >!--}

  1. Import the singleton via from ErisPulse.Core import scope (same object as sdk.scope)
  2. Check permissions: scope.is_allowed(...) / scope.is_identity_allowed(...) /
    scope.is_action_allowed(...) correspond to the three gates ①②③
  3. Read/write: dimension-specific parameter methods (IDE can auto-complete) —
    scope.set_module(...) / scope.set_identity(...) / scope.set_action(...);
    Additionally, there are dictionary-style fallback methods scope.get(path) / scope.set(path, v) / scope.delete(path)
  4. Event handler text condition overrides are covered in
    Event Handling Introduction · Event Overriding;
    command ACL / parameter overrides are covered in Event Handling Introduction {!--< /tips >!--}

Matching Entry Syntax (Unified Across the System)

All "name lists" in scope (module names, identity keys, outbound entries) share the same matching syntax (ErisPulse.Core.text_match):

Syntax Example Description
Exact name "Chat" Full value comparison, case-insensitive
Glob "Tool*"、"spam_*" * for any string / ? for single character / [seq] for character set, case-insensitive
Regular expression "re:^Danger.*" Declared with re: prefix, matches using regex search, default case-insensitive

Global Fallback: default_allow

default_allow is the single global fallback switch (default true), affecting two decision dimensions:

Setting it to false enables "implicit deny" strict mode: whitelisting management, any not explicitly allowed is denied.

Exception: The outbound dimension is not affected by default_allow—it is an independent tightening switch, defaulting to full allow, restricting only with explicit rules (framework-level owner-empty calls are always allowed). This strict global mode won't accidentally cut off all module message replies. Command ACL has a separate ErisPulse.event.command.default_allow fallback, independent of this.

Configuration File

[ErisPulse.scope]
default_allow = true        # Global fallback (false = implicit deny strict mode)
cache_size = 1024           # LRU cache size

# ── ① Module dimension (priority: session > Bot > platform) ──
[ErisPulse.scope.platforms.onebot11]
modules = ["Chat", "Tool*"]   # Whitelist: exact name / glob / re: regex
blocked = ["re:^Danger"]
[ErisPulse.scope.bots.onebot11."123456"]
modules = ["Chat"]
merge = true                  # Append on top of platform-level binding (default is overall override)
[ErisPulse.scope.sessions.onebot11."789012345"]
modules = ["Chat"]

# ── ② Identity dimension (priority: user > session > Bot > adapter) ──
[ErisPulse.scope.identity.adapters.onebot11]
deny = true                   # Discard all events from this adapter
[ErisPulse.scope.identity.bots.onebot11."123456"]
deny = true
[ErisPulse.scope.identity.sessions.onebot11."g_blocked"]
deny = true
[ErisPulse.scope.identity.users.onebot11]
allow = ["u_admin"]           # User keys support glob / re: regex
deny = ["u_bad", "spam_*"]

# ── ③ Outbound dimension (default all allowed, only explicitly tighten) ──
[ErisPulse.scope.actions.MyModule]
send = { deny = true }                                    # Disable all sending
api = { allow = ["get_*"] }                               # Only allow standard query APIs
request = { deny = true }                                 # Disable request handling

① Module Level

Answer the question: "In a certain context, which modules are available?" By default, all modules are open; filtering starts only after configuration is bound. No changes are required for modules or adapters.

flowchart TD
    A["Event arrives at a module's handler/command"] --> B{"scope.is_allowed<br/>(platform, bot, module, session)"}
    B --> C{"Resolution chain: session level > bot level > platform level<br/> (when merge = true, merge each level)"}
    C -->|"Matched"| D["blocked matched → deny<br/>modules non-empty → only whitelist allowed<br/>both empty → default_allow"]
    C -->|"Not matched"| E["default_allow (default true = allow)"]
    D -->|"Deny"| Z["No reply<br/> (blocks broadcast `scope.blocked` event; matched commands are still claimed and blocked)"]

Binding Inheritance (merge)

By default, the semantics of complete override are clear and predictable; when you need to add to the parent binding, set merge = true in the sub-level:

[ErisPulse.scope.platforms.onebot11]
modules = ["Chat", "Tool"]      # Platform level: allow Chat, Tool

[ErisPulse.scope.bots.onebot11."123456"]
modules = ["Music"]
merge = true                    # The actual effective modules for this Bot = ["Chat", "Tool", "Music"]

② Identity Dimension (Event Admission)

Answers "whose events are received or not." Events rejected at the distribution entry are completely discarded—they do not enter middleware or any processor (including framework-level), visible only in TRACE-level logs (core.scope.identity_denied).

[ErisPulse.scope.identity.adapters.onebot11]
deny = true
[ErisPulse.scope.identity.users.onebot11]
allow = ["u_admin"]   # Even if adapter-level is denied, events from u_admin are still allowed

③ Outbound Dimension (Limiting Module Initiated Outbound Calls)

Restricts the outbound actions initiated by a module: message sending / standard API actions / request operations. The three action types correspond to the underlying DSL: Event.reply and Send (send), Api / call_api (api), Request's accept/reject (request). Outbound calls initiated by a module during event handler execution carry the module owner, and are uniformly judged by this dimension.

Rule Forms (Inline Table)

Each action's rule is an inline table: { allow = [...], deny = true|[...] }. Only one rule per action is allowed (TOML keys cannot be repeated, choose either full deny or fine-grained):

[ErisPulse.scope.actions.MyModule]
send = { deny = true }                                  # Disable all sending (Event.reply / Send DSL)
# Or fine-grained method-level: send = { allow = ["Text", "Image*"], deny = ["File"] }
api = { allow = ["get_*"] }                             # Only allow standard query APIs
# Or action-level blacklist: api = { deny = ["set_*", "leave_*"] }
request = { deny = true }                               # Disable request handling accept/reject

Decision Semantics

Default is full allow—unconfigured, or owner is empty (internal framework calls) are all allowed. After configuration, the following order is used for judgment:

  1. deny = true → deny
  2. deny list matches the call name → deny
  3. allow list is non-empty and the call name is not matched (or the call has no name) → deny
  4. Otherwise allow

Denied calls do not initiate any network requests, directly returning the standard failure response (retcode = 34601, see api-response §5.3).

# Runtime API
sdk.scope.set_action("MyModule", "send", deny=True)              # Disable all message sending
sdk.scope.set_action("MyModule", "send", allow=["Text"])         # Allow only text sending
sdk.scope.is_action_allowed("MyModule", "send", name="Image")    # False
sdk.scope.is_action_allowed("MyModule", "api", name="get_user_info")  # Judged by rules
sdk.scope.delete_action("MyModule", "send")                      # Restore allow
sdk.scope.get_action("MyModule", "send")                         # Current rule for this action

Runtime API

The runtime API for scope is layered into three parts: decision (three questions), dimensional read/write (each dimension has set/get/delete parameterized methods, fully type-annotated, IDE-completable), and dictionary-style fallback (dot-path access to any section).

from ErisPulse import sdk

scope = sdk.scope

Decision (Three Questions)

scope.is_allowed("onebot11", "123456", "Chat")                 # ① Module dimension
scope.is_allowed("onebot11", "123456", "Chat", "789012345")    # With session-level
scope.is_allowed("onebot11", "123456", None)                   # Framework-level resource -> True

scope.is_identity_allowed("onebot11", "123456", "group_9", "u1")   # ② Identity dimension

scope.is_action_allowed("MyModule", "send")                    # ④ Outbound dimension
scope.is_action_allowed("MyModule", "send", name="Image")      # Fine-grained method-level

① Module Dimension

# Binding (hierarchy determined by parameters: session_id > bot_id > platform-level)
scope.set_module("onebot11", bot_id="123456", modules=["Chat", "Tool*"])
scope.set_module("onebot11", blocked=["re:^Danger"])                       # Platform-level
scope.set_module("onebot11", bot_id="123456", session_id="g9", modules=["Chat"])  # Session-level
scope.set_module("onebot11", bot_id="123456", modules=["Music"], merge=True)      # Union with existing entries
scope.set_module("onebot11", bot_id="123456", modules=["Chat"], persist=False)    # Runtime only

# Read / Delete
scope.get_module("onebot11", bot_id="123456")   # {"modules": ["Chat"], "blocked": []}
scope.delete_module("onebot11", bot_id="123456")

merge=True is write-time union (merges entries with existing bindings at this level); merge = true configuration at parse time across levels is described in the previous section on Binding Inheritance—these are independent mechanisms.

Runtime binding (persist=False) semantics: Runtime bindings are saved in a separate overlay layer, not overwritten by any subsequent configuration writes or hot reloads of the configuration file (the configuration tree is rebuilt and automatically reapplied in the order of writes, including runtime deletions). They are not persisted, lost on process restart; when a module is unloaded, runtime bindings written by that module are cleared by fallback. Subsequent persist=True writes (user-persistent semantics) to the same path will replace runtime rules.

② Identity Dimension

# Binding policies (hierarchy determined by parameters: user > session > Bot > adapter; allow / deny is binary)
scope.set_identity("onebot11", user_id="u_bad", deny=True)
scope.set_identity("onebot11", user_id="spam_*", deny=True)    # Key supports glob / re: regex
scope.set_identity("onebot11", bot_id="123456", session_id="g9", allow=True)

# Read / Delete
scope.get_identity("onebot11", user_id="u_bad")   # {"deny": True}
scope.delete_identity("onebot11", user_id="u_bad")

③ Outbound Dimension

# Set restriction rules (allow: str|list; deny: bool|str|list; whole rule replacement semantics)
scope.set_action("MyModule", "send", deny=True)                    # Disable all sending
scope.set_action("MyModule", "send", allow=["Text"])               # Allow only text sending
scope.set_action("MyModule", "api", deny=["set_*", "leave_*"])     # Disable management APIs

# Read / Delete
scope.get_action("MyModule", "send")       # {"allow": ["Text"]} original rule
scope.delete_action("MyModule", "send")    # Remove single action
scope.delete_action("MyModule")            # Remove all action restrictions for this module

General

scope.get("platforms")   # Dictionary-style fallback: dot-path read any section
scope.topology()         # Full configuration tree (for Dashboard)
scope.stats()
# {"module_calls": .., "module_filtered": .., "identity_checks": .., "identity_denied": ..,
#  "action_checks": .., "action_denied": .., "cache_hits": .., "cache_misses": ..}
scope.reset_stats()
scope.clear()           # Clear all configurations (memory-only)

Advanced: Dictionary-style Dot-Path Fallback

Dimensional methods cover daily scenarios; when you need direct access to any node (or future added dimensions), use the dictionary-style API—get / set / delete accepts dot-path (deep dict merge, immediate read after write), and provides scope[path] / scope[path] = v / del scope[path] / path in scope protocols:

scope.set("bots.onebot11.123456", {"modules": ["Chat"], "blocked": []})
scope.set("identity.users.onebot11.u_bad", {"deny": True})
scope.get("actions.MyModule.send")

scope["platforms.onebot11"]        # Read (raises KeyError if not exists)
scope["platforms.onebot11"] = {...}  # Write
del scope["platforms.onebot11"]      # Delete
"actions.MyModule" in scope          # Existence check

Interception Observability: scope.blocked Event

When scope interception (module filtering / identity rejection) occurs, the scope.blocked lifecycle event is broadcast, allowing subscription to and statistics on "who was blocked and at which layer" and presenting this information in the Dashboard. Interception does not reply by default, but it is no longer an opaque black box.

Field Description
dimension "module" (module dimension filtering) / "identity" (identity admission rejection)
module The name of the filtered module (only for module dimension)
platform / bot_id / session_id / user_id The source context where the interception occurred
from ErisPulse.Core.lifecycle import lifecycle

@lifecycle.on("scope.blocked")
def on_blocked(data):
    print(f"Intercepted: {data['dimension']} {data.get('module') or data.get('user_id')}")

Cache and Hot Reload

Configuration Format Validation

Configuration format is validated per section during loading / hot reload: sections with type errors (e.g., platforms written as a string), invalid outbound rules (e.g., allow written as a number), unknown action names, or unknown top-level keys (e.g., alow with a typo) will output a WARNING and ignore the corresponding section / entry, while other valid configurations remain effective—mistakes no longer silently fail.

Frequently Asked Questions and Precautions

1. Configuration Hierarchy and Overriding

2. Module/Command Not Responding

First suspect the scope rather than the module itself:

from ErisPulse import sdk

print(sdk.scope.is_allowed(event.get_platform(), bot_id, "MyModule", session_id))
print(sdk.scope.is_identity_allowed(event.get_platform(), bot_id, session_id, user_id))
print(sdk.scope.stats())   # module_filtered / identity_denied > 0 indicates there are blocking records

Filtered requests default to no reply (at both module and identity levels, to avoid exposing rules), but will broadcast the scope.blocked lifecycle event and continuously accumulate statistics; ACL-rejected commands will explicitly reply with "Insufficient permissions".

3. Troubleshooting Outbound Actions Rejected

from ErisPulse import sdk

print(sdk.scope.get("actions.MyModule"))
print(sdk.scope.stats())   # action_denied > 0 indicates there are blocked calls

Blocking is explicit: rejected calls return a standard failure response with retcode = 34601 (no network request is initiated).

4. Session Identifier Isolation Across Platforms

The combination of (platform, session_id) is the unique identifier. scope.sessions.onebot11."789" only applies to onebot11 and does not affect the session with 789 on telegram. The same applies to user keys at the identity level.

Topology API

ModuleManager.get_topology() and AdapterManager.get_topology() provide data on module/adapter ownership relationships. sdk.get_topology() offers a one-click aggregation (including scope scope):

from ErisPulse import sdk

topology = sdk.get_topology()
# {
#   "modules": {                                   # Module → owned resources
#     "Chat": {
#       "loaded": True, "enabled": True,
#       "commands": ["chat", "translate"],
#       "handlers": {"message": 2, "notice": 1},
#       "routes": {"http": ["/Chat/api"], "ws": [], "sse": []},
#       "lifecycle_hooks": 3,
#     }
#   },
#   "adapters": {                                  # Adapter → Bot → scope
#     "onebot11": {
#       "status": "started", "enabled": True,
#       "bots": {"123456": {"status": "online", "scope": {...}}},
#       "scope": {"modules": [...], "blocked": [...]},
#     }
#   },
#   "scope": {                                     # Scope (module / identity / outbound action)
#     "platforms": {...}, "bots": {...}, "sessions": {...},
#     "identity": {"adapters": {...}, "bots": {...}, "sessions": {...}, "users": {...}},
#     "actions": {...},
#   },
# }