1. What makes out-of-band approval the source of security in a pairing flow?
The pairing code is encrypted before it is sent to the sender
The sender can request a code, but only an operator on a separate trusted console can approve it
The code is long enough that it cannot be guessed within an hour
The assistant verifies the sender's webhook signature before issuing the code
2. When an unknown contact first messages the bot under a pairing policy, what happens to that first message?
It is forwarded to the model, which decides whether to respond
It is answered normally but flagged for later review
It never reaches the LLM; the assistant replies only with a pairing code and blocks the sender
It is queued until the code expires, then delivered
3. Which hardening measure prevents an attacker from flooding the endpoint with pairing requests?
A cap on pending pairing requests per channel, with extra attempts silently ignored
Requiring the sender to solve a CAPTCHA before a code is issued
Making codes case-sensitive
Encrypting the pending-request store on disk
4. What is the difference between DM pairing and node pairing?
DM pairing is temporary; node pairing is permanent
DM pairing controls which people may message the bot; node pairing controls which physical devices may connect to the gateway
DM pairing uses codes; node pairing uses passwords
DM pairing is for owners; node pairing is for strangers
5. Why is revoking a paired contact described as "fast and local"?
Because the assistant automatically re-verifies every contact each hour
Because the binding is stored data, so you delete one approved-contact entry and the next message is treated as an unknown stranger
Because revocation waits for a short-lived token to expire on its own
Because revocation must be pushed out to a fleet of devices at once
Pairing and Enrollment
Key Points
- Enrollment is a deliberate, up-front process that binds a specific human, through a specific channel identity, to the assistant before any message reaches the model.
- The dominant pattern is a pairing-code flow: an unknown sender gets a short-lived, single-use code and is blocked; the message never reaches the LLM.
- Security rests on out-of-band approval — the attacker can request a code but cannot approve it, because approval requires the operator's trusted console.
- Hardening: rate limiting on pending requests (e.g., max 3/channel), ~1-hour single-use codes, and a self-trust exception so the owner is never locked out.
- Because the binding is just data, revocation and re-pairing are fast, local edits to the approved-contact record.
Any WhatsApp number, Telegram handle, or Slack app is reachable by strangers. If the assistant forwards every inbound message to the model, it takes instructions from the entire internet — the over-trust failure from Chapter 1. Enrollment closes this gap. The real-world analogy is a secure facility: a stranger at the door is not admitted because they ask; they receive a visitor token, and a badged employee must vouch for that token on a separate, trusted path before the door unlocks.
The pairing code is typically an 8-character string built to avoid lookalike characters (no 0/O or 1/l confusion) and expires after roughly one hour. The sender receives it in-channel, but approval happens elsewhere — for example the owner runs openclaw pairing approve whatsapp <CODE> on a trusted device, which writes an allowlist entry.
Two categories of pairing guard different doors:
| Pairing type | Controls | What it binds | Example command |
| DM pairing | Which people/handles on a channel may message the bot | A human's chat identity to the assistant | openclaw pairing approve <channel> <CODE> |
| Node pairing | Which physical devices may connect to the gateway | A device (iOS, Android, macOS, headless host) to the gateway | openclaw pairing approve nodes <CODE> |
A Worked Pairing Flow
Tracing a first-contact sequence for a new Telegram user, Dana: (1) Dana messages the bot and the gate finds no approved contact. (2) The assistant issues a code such as K7M9-P2QX, records a pending request keyed to her Telegram user ID, and replies only with the code — the message never reaches the LLM. (3) The operator runs openclaw pairing approve telegram K7M9-P2QX out-of-band, moving Dana from pending to approved. (4) Her next message resolves to an approved contact and is forwarded, subject to her capability tier. (5) If the operator never approves, the code expires in ~1 hour; if an attacker floods, rate limiting caps pending requests at 3.
Figure 5.1: Out-of-band pairing/enrollment sequence for a first-contact sender
sequenceDiagram
participant Dana as Dana (Telegram)
participant Gate as Access-Control Gate
participant Store as Pending/Approved Store
participant Op as Operator (trusted console)
participant LLM as Agent / LLM
Dana->>Gate: First inbound message
Gate->>Store: Look up (telegram, user_id)
Store-->>Gate: No approved contact
Gate->>Store: Record pending request + code "K7M9-P2QX"
Gate-->>Dana: Reply with code only (message never reaches LLM)
Note over Op: Out-of-band, separate trust domain
Op->>Store: openclaw pairing approve telegram K7M9-P2QX
Store-->>Op: Pending -> Approved (allowlist entry written)
Dana->>Gate: Next message
Gate->>Store: Look up (telegram, user_id)
Store-->>Gate: Approved contact + capability tier
Gate->>LLM: Forward message (subject to tier)
Visual animation — coming soon
Binding, Revocation, and Re-Pairing
The result of pairing is data: an entry linking a channel identity to an approved-contact record. Pending requests and approved contacts are typically stored as separate JSON files under a credentials directory such as ~/.openclaw/credentials/. Because the binding is data, its lifecycle is easy to manage. Revocation — needed when a device is lost, an account is compromised, or a relationship ends — is simply removing the approved-contact entry. Re-pairing is the same enrollment flow run again. The critical property is that revocation is fast and local: you delete one entry and the next message from that identity is an unknown stranger again.
Figure 5.2: Binding lifecycle — pending, approved, revoked, and re-pairing state transitions
stateDiagram-v2
[*] --> Unknown
Unknown --> Pending: First inbound message (code issued)
Pending --> Approved: Operator approves out-of-band
Pending --> Unknown: Code expires (~1h) or rate-limit drop
Approved --> Revoked: Delete approved-contact record
Revoked --> Unknown: Identity severed
Unknown --> Pending: Re-pairing (same flow again)
Approved --> [*]
1. What makes out-of-band approval the source of security in a pairing flow?
The pairing code is encrypted before it is sent to the sender
The sender can request a code, but only an operator on a separate trusted console can approve it
The code is long enough that it cannot be guessed within an hour
The assistant verifies the sender's webhook signature before issuing the code
2. When an unknown contact first messages the bot under a pairing policy, what happens to that first message?
It is forwarded to the model, which decides whether to respond
It is answered normally but flagged for later review
It never reaches the LLM; the assistant replies only with a pairing code and blocks the sender
It is queued until the code expires, then delivered
3. Which hardening measure prevents an attacker from flooding the endpoint with pairing requests?
A cap on pending pairing requests per channel, with extra attempts silently ignored
Requiring the sender to solve a CAPTCHA before a code is issued
Making codes case-sensitive
Encrypting the pending-request store on disk
4. What is the difference between DM pairing and node pairing?
DM pairing is temporary; node pairing is permanent
DM pairing controls which people may message the bot; node pairing controls which physical devices may connect to the gateway
DM pairing uses codes; node pairing uses passwords
DM pairing is for owners; node pairing is for strangers
5. Why is revoking a paired contact described as "fast and local"?
Because the assistant automatically re-verifies every contact each hour
Because the binding is stored data, so you delete one approved-contact entry and the next message is treated as an unknown stranger
Because revocation waits for a short-lived token to expire on its own
Because revocation must be pushed out to a fleet of devices at once
1. Why is a default-deny allowlist considered to "fail safe" while a default-allow denylist "fails open"?
Allowlists are faster to evaluate than denylists
With an allowlist an overlooked case stays blocked; with a denylist an overlooked case gets through
Denylists require encryption while allowlists do not
Allowlists automatically expire, so stale rules never accumulate
2. Why must the access-control gate run before the message reaches the model?
Because the model is slower than the gate and would create a bottleneck
Because terminating unallowed messages at the gate denies a prompt-injection payload its audience
Because the model cannot read channel identifiers
Because webhook signatures can only be verified after the model responds
3. Which DM policy is the recommended secure default?
open — anyone may message the bot
disabled — the channel accepts no DMs at all
pairing — unknown senders get a code and are blocked until manually approved
allowFrom: ["*"] — a wildcard allowlist
4. What do capability tiers add on top of a plain allowlist?
They separate being allowed to talk to the agent from being allowed to ask for anything
They encrypt each message according to the sender's rank
They replace pairing so no out-of-band approval is needed
They let denylisted users through if they are trusted enough
5. Why do rate limiting and abuse controls apply inside the allowlist, not just at the pairing gate?
Because allowlisted users are exempt from all other checks
Because even an approved sender can be abusive, maliciously or because their account was compromised
Because the LLM charges per token regardless of sender
Because pairing codes expire and must be re-issued periodically
Allowlisting Who Can Talk to the Agent
Key Points
- Default-deny ("deny by default, allow by exception") blocks all requests unless an explicit rule permits them — the direct application of least privilege and the backbone of zero trust.
- An allowlist fails safe (an overlooked case stays blocked); a denylist fails open (an overlooked case gets through). It is easier to enumerate a few allowed things than to imagine every attack.
- Enforce it with a gate that runs before the model sees the message, so prompt-injection payloads never get an audience.
- Capability tiers (RBAC) separate allowed to talk from allowed to ask for anything; sensitive actions can add human-in-the-loop approval (e.g., CIBA + RAR).
- Rate limiting, just-in-time access, and periodic reviews keep even approved senders within bounds.
The two postures differ sharply in how they fail:
| Posture | Rule | Failure mode | Analogy |
| Allowlist (default-deny) | Everything is denied; you enumerate the few things that are allowed | Fails safe — an overlooked case stays blocked | A guest list at a private event: not on the list, not in |
| Denylist (default-allow) | Everything is allowed; you enumerate the things that are denied | Fails open — an overlooked case gets through | A bouncer with a photo of known troublemakers: any new face walks in |
Each channel ships with a configurable DM policy, spanning secure to reckless:
| DM policy | Behavior | Recommendation |
pairing | Unknown senders get a code and are blocked until manually approved | Secure default |
allowlist | Only pre-approved identities may message the bot | Safe (no self-enrollment) |
open | Anyone may message (equivalent to allowFrom: ["*"]) | Explicitly not recommended |
disabled | The channel accepts no DMs | Safe (channel off) |
The Access-Control Gate
The gate that enforces default-deny runs before an inbound message reaches the agent. It asks: Is this sender in the allowlist? If this is a first-time DM, has pairing been approved? If either check fails, the message stops there. This ordering connects directly to Chapter 2: prompt injection can only hijack the model if untrusted text reaches it, so terminating unallowed messages at the gate denies the injection payload an audience.
Figure 5.3: Default-deny decision path for an inbound message
flowchart TD
A[Inbound message] --> B{Sender in allowlist?}
B -->|Yes| E{Within rate limit and tier?}
B -->|No| C{First-time DM and DM policy = pairing?}
C -->|Yes| D[Issue pairing code, record pending, block]
C -->|No| F[Deny: drop message]
E -->|Yes| G[Forward to agent / LLM]
E -->|No| F
D --> F
A canonical code implementation of default-deny has three parts: a base controller that verifies an authorization check actually ran on every request; a base policy that denies all actions by default; and model-specific policies that inherit from the base and selectively re-enable only the permitted actions. The design property: every granted permission is intentional rather than accidental — a developer who forgets to write a policy gets a locked door, not an open one.
Per-User and Per-Group Capability Tiers
Being allowed to talk is different from being allowed to ask for anything. Authorization should be role-based (RBAC), determined by task and responsibility rather than per individual:
| Tier | Who | What they may ask | Example |
| Paired-but-untrusted | Newly enrolled contacts | Read-only / informational queries | "What's on the public calendar?" |
| Standard | Trusted collaborators | Routine actions | "Draft a reply to this email" |
| Owner/admin | The operator | Configuration changes, sensitive tools | "Rotate the API key," "run this shell command" |
The channel identity resolves to a principal; the principal's tier decides what they may ask — cleanly separating authentication (who is this?) from authorization (what may they do?). For the most sensitive actions, tiers can be reinforced with human-in-the-loop approval. Auth0 implements this with Client-Initiated Backchannel Authentication (CIBA) plus Rich Authorization Requests (RAR): the agent sends a request, the user is notified on a separate trusted device to approve a concretely described action — e.g., "Approve payment of $50.00 to ExampleCorp" — before tokens are issued. This is the same out-of-band principle from pairing, now applied to individual high-impact actions.
Visual animation — coming soon
Rate Limiting and Abuse Controls for Allowed Senders
Allowlisting stops strangers, but an approved sender can still be abusive — maliciously or because their account was compromised. Rate limiting therefore applies inside the allowlist too: cap requests per user per window, throttle expensive tool calls, and alert on anomalous bursts. Least-privilege operational practices reinforce this over time: separate administrative from standard accounts, use just-in-time (JIT) access — temporary, time-limited elevated credentials that auto-revoke to prevent "privilege creep" — and run regular access reviews to close inactive accounts.
1. Why is a default-deny allowlist considered to "fail safe" while a default-allow denylist "fails open"?
Allowlists are faster to evaluate than denylists
With an allowlist an overlooked case stays blocked; with a denylist an overlooked case gets through
Denylists require encryption while allowlists do not
Allowlists automatically expire, so stale rules never accumulate
2. Why must the access-control gate run before the message reaches the model?
Because the model is slower than the gate and would create a bottleneck
Because terminating unallowed messages at the gate denies a prompt-injection payload its audience
Because the model cannot read channel identifiers
Because webhook signatures can only be verified after the model responds
3. Which DM policy is the recommended secure default?
open — anyone may message the bot
disabled — the channel accepts no DMs at all
pairing — unknown senders get a code and are blocked until manually approved
allowFrom: ["*"] — a wildcard allowlist
4. What do capability tiers add on top of a plain allowlist?
They separate being allowed to talk to the agent from being allowed to ask for anything
They encrypt each message according to the sender's rank
They replace pairing so no out-of-band approval is needed
They let denylisted users through if they are trusted enough
5. Why do rate limiting and abuse controls apply inside the allowlist, not just at the pairing gate?
Because allowlisted users are exempt from all other checks
Because even an approved sender can be abusive, maliciously or because their account was compromised
Because the LLM charges per token regardless of sender
Because pairing codes expire and must be re-issued periodically
1. Why should permissions attach to an internal principal rather than to raw channel handles?
Raw handles are longer and harder to store
Otherwise the same person appears as unrelated strangers across channels, with multiple places to revoke and chances to miss one
Channel handles cannot be used in authorization decisions at all
Principals are required by OAuth 2.0 but handles are not
2. What is the process that links multiple external identity-provider profiles to one internal user?
Account linking
Token introspection
Session pinning
Credential stuffing
3. Why should identity mapping key off a stable, opaque identifier rather than a username or display name?
Opaque identifiers are shorter and save storage
Usernames and display names can change or be reassigned to a different person, while a stable opaque ID does not
Display names are always encrypted and cannot be read
Opaque identifiers are the only values a webhook can carry
4. How does storing the channel type alongside the external ID help prevent identity collision?
It compresses the identifier so collisions are impossible
It disambiguates identifiers so (slack, U04AB12CD) and (telegram, 483920174) can never be confused
It forces every channel to use the same ID format
It lets display names be used safely as the mapping key
5. What is the decisive advantage of per-channel mapping when one of a user's channels is compromised?
All of the user's channels are automatically frozen for safety
You can revoke the single compromised channel entry without severing the human's other links
The compromised channel is migrated to a new provider automatically
The principal is deleted and must be re-created from scratch
Per-Channel Identity Mapping
Key Points
- A single human reaches the assistant through many channel-scoped identifiers (Slack ID, Telegram ID, WhatsApp number, email); each is issued by a third party.
- Map all of them onto one internal principal via account linking, and attach permissions and memory to the principal — not to raw handles.
- Always key off stable, opaque identifiers (an IdP
NameID or immutable user ID), never mutable usernames or display names — like filing under a passport number, not a name.
- Store the channel type alongside the external ID so
(slack, …) and (telegram, …) are always disambiguated, preventing collisions.
- Because each mapping is data, revoking one compromised channel need not revoke the human's other links, and per-principal memory scoping prevents cross-user data leakage.
If you attach permissions to raw handles, the same person shows up as three unrelated strangers across three channels, a permission granted on Slack does not follow them to Telegram, and you have three places to revoke access with three chances to miss one. Mapping every channel identity to a single principal gives you one place to reason about who someone is and what they may do. The standard technique is account linking: store the external channel identifier plus its provider/channel type as attributes of the internal principal, and resolve inbound messages by looking up (channel, external_id) -> principal.
| Channel | External identifier | Internal principal | Tier |
| Slack | U04AB12CD | dana | standard |
| Telegram | 483920174 | dana | standard |
| Email | dana@example.com | dana | standard |
An inbound Telegram message resolves (telegram, 483920174) -> dana, and dana's tier governs the request regardless of which channel it arrived on.
Figure 5.4: Per-channel identity mapping — many channel handles resolve to one principal and tier
flowchart LR
S["Slack: U04AB12CD"] --> P["Principal: dana"]
T["Telegram: 483920174"] --> P
E["Email: dana@example.com"] --> P
P --> R["Tier: standard"]
R --> Z["Authorization decision (what may they ask?)"]
Visual animation — coming soon
Key Off Stable, Opaque Identifiers
The most common mistake is keying off a mutable value. Usernames, display names, and handles can change — and in some systems can be reassigned to a different person after an account is deleted. SAML and OIDC systems use a NameID: "a unique, pseudo-random string that should not change for the user over time," assigned by the IdP. The rule for a bot: pin the mapping to the immutable Slack user ID or Telegram user ID — not the username, display name, or handle. The analogy is a passport number versus a name: two people can share a name and one person can change theirs, but the passport number is a stable, unique key issued by an authority. Where multiple providers feed one system, a normalization layer (e.g., Amazon Cognito with a PreTokenGeneration Lambda mapping IdP groups into user-pool groups) harmonizes identities into a common shape.
Preventing Collision, Confusion, and Scoping Memory
Two dangers arise with multiple channels. Identity collision — two external identities resolving to the same principal, or one identity reused for two humans — is prevented by keying off stable opaque IDs and storing the channel type alongside the external ID, so (slack, U04AB12CD) and (telegram, 483920174) are always disambiguated. Cross-channel confusion — authority leaking between channels — is contained by attaching permissions to the principal while still respecting where a request came from; an owner-tier action from a low-assurance channel (easily spoofed email) may warrant step-up confirmation on a higher-assurance channel.
Because the mapping is data, revocation is a targeted edit: remove a role assignment (Slack models this with admin.roles.addAssignments / removeAssignments / listAssignments), delete the approved-contact record to sever a channel identity, or apply just-in-time grants that auto-expire. The decisive advantage: if Dana's Telegram account is stolen, you delete (telegram, 483920174) and her Slack and email access continue uninterrupted. Finally, scoping applies to memory and context too — each principal has its own conversational memory and data scope, preventing cross-user leakage (a theme revisited in Chapter 10's confused-deputy problem).
1. Why should permissions attach to an internal principal rather than to raw channel handles?
Raw handles are longer and harder to store
Otherwise the same person appears as unrelated strangers across channels, with multiple places to revoke and chances to miss one
Channel handles cannot be used in authorization decisions at all
Principals are required by OAuth 2.0 but handles are not
2. What is the process that links multiple external identity-provider profiles to one internal user?
Account linking
Token introspection
Session pinning
Credential stuffing
3. Why should identity mapping key off a stable, opaque identifier rather than a username or display name?
Opaque identifiers are shorter and save storage
Usernames and display names can change or be reassigned to a different person, while a stable opaque ID does not
Display names are always encrypted and cannot be read
Opaque identifiers are the only values a webhook can carry
4. How does storing the channel type alongside the external ID help prevent identity collision?
It compresses the identifier so collisions are impossible
It disambiguates identifiers so (slack, U04AB12CD) and (telegram, 483920174) can never be confused
It forces every channel to use the same ID format
It lets display names be used safely as the mapping key
5. What is the decisive advantage of per-channel mapping when one of a user's channels is compromised?
All of the user's channels are automatically frozen for safety
You can revoke the single compromised channel entry without severing the human's other links
The compromised channel is migrated to a new provider automatically
The principal is deleted and must be re-created from scratch