Control-Trust Provenance
Gateway commands are gated by content provenance, not just identity — untrusted producers cannot drive /stop, /new, /compress even from an owner identity
Gateway control commands are gated by content provenance: a message only counts as a command when it is a real interactive human turn, so an untrusted producer can never drive /stop, /new, or /compress even under a privileged identity.
Quick Start
- Interactive human turn — command runs
A real user typing into a live chat keeps the default (
allow_control=True), so a slash-prefixed message is classified as a command.python from praisonaiagents import BotMessage msg = BotMessage(content="/compress") msg.is_command # -> True - Untrusted producer — command is neutralised
A webhook, relay, or wake producer marks its message
allow_control=False, so the same text is handled as plain text instead of a privileged command.python from praisonaiagents import BotMessage msg = BotMessage(content="/compress", allow_control=False) msg.is_command # -> False
How It Works
A producer sets allow_control=False on non-interactive content; is_command short-circuits to False before the gateway ever classifies the message, so it is dispatched as plain text.
| Producer | allow_control | Rationale |
|---|---|---|
| Interactive human turn (Telegram/Slack/Discord/Signal/WhatsApp/local DM) | True (default) | Real user typed into a live chat |
| Generic webhook channel body | False | Body originates outside the human turn |
Relayed / mirrored bot-authored content (sender.is_bot == True) | False | Not a user’s typed input |
| Proactive / wake / scheduler inject | False | Synthetic, not typed by the user |
The Two-Gate Model
Command classification passes through two independent gates — content-trust fires first, identity ACL second.
Identity alone is not enough: untrusted content is often attributed to an allowed identity — a webhook bound to the owner, a relayed or proactive event on the bot’s own session. Identity ACL passes, but content-trust does not, so the command never executes.
What This Prevents
Content provenance is now a first-class part of the contract, so a /-prefix in an untrusted body no longer drives privileged session control.
from praisonaiagents import BotMessage, BotUser
OWNER_ID = "owner-123"
# BEFORE — content prefix alone drove control commands
# A webhook body of "/compress" attributed to the owner passed identity ACL
# and compacted the session.
# AFTER — provenance is first-class on the contract
msg = BotMessage(
content="/compress",
sender=BotUser(user_id=OWNER_ID, is_bot=True),
allow_control=False, # webhook/relay/mirror/proactive producer marks it untrusted
)
msg.is_command # -> False (handled as plain text)
# Interactive user turn — unchanged
BotMessage(content="/compress", sender=BotUser(user_id=OWNER_ID)).is_command # -> TrueFor Adapter Authors
Set allow_control=False on non-interactive producers
When you write an adapter or a webhook/relay/proactive producer, set allow_control=False on the BotMessage unless the content is a real, interactive human turn on that channel. The default (True) keeps today’s behaviour for interactive turns; the primitive is safe by narrowing it for untrusted sources.
Round-trip carries the field
to_dict() and from_dict() carry allow_control. from_dict defaults it to True when the key is absent, so older serialised payloads still deserialize as trusted.
Layer identity ACL on top
Content-trust is the first gate. CommandAccessPolicy.can_run(user_id, cmd) still runs afterward for messages that pass content-trust, so identity gating (admin_users, user_allowed_commands) is unchanged.
Configuration Reference
| Field | Type | Default | Description |
|---|---|---|---|
BotMessage.allow_control | bool | True | True for interactive human turns only. Producers set False for untrusted / non-interactive content. When False, is_command returns False regardless of / prefix or MessageType.COMMAND. |
Backward Compatibility
The default is True, so today’s interactive traffic is unchanged. from_dict defaults the field to True when absent, so pre-PR serialised messages still deserialize as trusted. There is no config flag, CLI flag, or YAML change — this is a contract primitive, not a user-facing feature knob.
Related
Prompt-plane counterpart — wraps untrusted payload content as data
Bot Command Access ControlIdentity-based ACL that layers on top of content-trust
Webhook ChannelThe primary untrusted producer
Gateway Inbound HooksHTTP-triggered agent runs with the same trust posture
Bot Chat CommandsWhat the gated commands actually do
Inbound Message GateAdmission after is_command classifies