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.

Diagram

True — human turn

False — untrusted

📨 Inbound message

🔐 allow_control?

✅ is_command → True

🛑 is_command → False, plain text

Quick Start

  1. 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
  2. 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.

Diagram
GatewayBotMessage🌐 Webhook / Relay / WakeGatewayBotMessage🌐 Webhook / Relay / Wakeallow_control=Falseis_command?False (short-circuit)Dispatch as plain text
Producerallow_controlRationale
Interactive human turn (Telegram/Slack/Discord/Signal/WhatsApp/local DM)True (default)Real user typed into a live chat
Generic webhook channel bodyFalseBody originates outside the human turn
Relayed / mirrored bot-authored content (sender.is_bot == True)FalseNot a user’s typed input
Proactive / wake / scheduler injectFalseSynthetic, not typed by the user

The Two-Gate Model

Command classification passes through two independent gates — content-trust fires first, identity ACL second.

Diagram

allow_control=False

allow_control=True

can_run False

can_run True

📨 Inbound message

🔐 Gate 1: content-trust

🛑 Not a command

🪪 Gate 2: identity ACL

🛑 Command denied

✅ Command runs

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.

python
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   # -> True

For 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

FieldTypeDefaultDescription
BotMessage.allow_controlboolTrueTrue 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