[Framework] Five Layers of No: How OGP’s Doorman Actually Works
Every inbound message gets five chances to be rejected. Here’s why that’s a feature.
There’s a running joke in distributed systems: “The most important word in security is ‘no.’” OGP took this literally. Every inbound federated message hits five independent policy layers before it ever reaches your agent. Each layer can reject the message for a different reason. The result is a defense-in-depth model that looks paranoid until you see what it’s defending against.
This is the Doorman. And if you’re building anything that lets external agents talk to your internal agent, you should steal this design.
The Problem: Your Agent Is a Trusting Beast
LLM-based agents are, by default, helpful. Ask them something, they answer. Give them a message, they reason over it. This is great for user experience and terrible for security.
When federation enters the picture, the attack surface explodes. A remote gateway can send your agent:
Instructions disguised as questions
Requests that consume tokens, API quotas, or compute
Messages designed to extract information your agent has access to
Flood traffic that drowns legitimate messages
Replay attacks that resend old messages to trigger duplicate actions
The agent will handle all of these helpfully unless something stops it. That something is the Doorman.
Layer 1: Authentication
Question: Did this message actually come from who it claims to come from?
Every OGP message carries an Ed25519 signature covering the canonical JSON serialization of all fields except the signature itself. The Doorman’s first step is cryptographic verification:
Parse the
fromGatewayIdfrom the message envelopeLook up the stored public key for that peer in the local peer registry
Verify the Ed25519 signature against the message bytes using the stored public key
Verify the message
timestampis within ±5 minutes of current time (replay protection + clock-skew tolerance)
If any step fails, the message is rejected with a signed error response. The peer knows their message didn’t get through and why.
Replay protection is intentionally simple: the timestamp window is the only line of defense, not a nonce-seen cache. A captured-and-replayed message becomes invalid the moment its timestamp falls outside ±5 minutes of the receiver’s clock, which is short enough that practical replay attacks have to land in a tiny window. Nonces still appear in the envelope, but they’re used for reply correlation, the daemon stashes a reply against the sender’s nonce so the original CLI process can pick it up, not for replay tracking. Adding a seen-nonce cache is a future option if a deployment wants belt-and-suspenders, but the timestamp window alone is what the protocol relies on today.
Why Ed25519 and not something else? Ed25519 is fast, has small signatures (64 bytes), and doesn’t require randomness during signing, which matters when your daemon is generating signatures under load. We use Node.js’s built-in crypto.sign() with Ed25519 keys, not a third-party library, because native bindings to OpenSSL/LibreSSL are faster and better maintained than pure-JS alternatives.
The key insight: Authentication doesn’t trust the network. HTTPS provides transport encryption, but the Doorman verifies end-to-end message authenticity independent of TLS. If your nginx misconfigures a proxy header, if a load balancer strips a token, if a man-in-the-middle compromises the TLS session, the signature still has to verify against a key you’ve already approved. Transport can fail. Application-layer authentication must not.
Layer 2: Peer Trust
Question: Do I even talk to this gateway?
Authentication proves the message was signed by the key. It doesn’t prove you want to talk to the owner of that key.
The Doorman maintains a peer registry, every gateway you’ve federated with, their public key, their current gateway URL, their alias, and their approval status. Before any message proceeds past authentication, the Doorman checks:
Is
fromGatewayIdin the peer registry?Is the peer status
approved? (Notpending, notrejected, notremoved)
If either check fails, the message is rejected. Even a cryptographically valid message from an unknown or unapproved peer gets a hard 403.
This is the bilateral trust handshake paying off. You can’t spam someone just by generating an Ed25519 keypair. You have to get through the human-approved pairing process first. And the human on the receiving side has to explicitly approve you. That approval is durable, it survives daemon restarts, URL changes, and key rotations, until explicitly revoked.
Tombstones matter. When a peer is removed, we don’t delete their record. We mark it as removed with a timestamp, and the federation state machine transitions the peer to a dedicated tombstoned lifecycle state. This prevents a subtle attack: if Alice removes Bob, then Bob sends a new federation request from a fresh key, Alice might accidentally re-approve him thinking he’s someone new. The tombstone preserves the “I already decided not to trust this person” signal, even across key changes. Re-federation from a tombstoned peer creates a fresh init-state pending record; it doesn’t silently reuse the old one.
Layer 3: Scope Enforcement
Question: Even if I trust this peer, did they ask for something I said they could ask for?
This is where most authorization systems stop at “can this user access this endpoint?” OGP goes finer-grained: can this peer invoke this specific intent type?
During federation approval, both gateways negotiate a scope bundle, the set of intent types the peer is allowed to send. The default bundle covers common operations:
message— human-to-human relayagent-comms— agent-to-agent conversationproject.join— request admission to a projectproject.contribute— add to a shared projectproject.query— query project stateproject.status— check project health
Operators can customize this bundle at approval time (ogp federation approve <peer-id> --intents message,agent-comms) or update it later (ogp federation grant <peer-id> --intents <list>).
The Doorman’s third check:
Look up the scopes granted to this peer
Check if the message’s
typeis in the allowed setIf the type is project-scoped (
project.*), verify the peer is a member of that specific project
If the peer sends a calendar-read intent but was only granted message and agent-comms — 403 Forbidden. If they query a project they haven’t joined, 403 Forbidden. If they send a custom intent type they never negotiated — 403 Forbidden.
The three-layer scope model:
Layer 1: Gateway capabilities — what the gateway can support (
/.well-known/ogpadvertises this)Layer 2: Peer negotiation — what the peer will be granted (set at approval time)
Layer 3: Runtime enforcement — what the peer is allowed right now (checked on every message)
Capabilities can be broader than grants. Grants can be broader than runtime enforcement (if a scope is revoked). The Doorman always enforces the most restrictive applicable layer.
Layer 4: Rate Limiting
Question: Is this peer being polite?
Even a fully trusted, fully scoped peer can misbehave by accident or malice. A bug in their agent that loops and sends messages. A compromised gateway that gets hijacked. A well-meaning automation that runs too frequently.
The Doorman implements per-peer, per-intent sliding-window rate limiting:
Default: 100 requests per 3600 seconds per peer per intent
Configurable: --rate <requests>/<seconds> at approve or grant time
Algorithm: Sliding window — keep a list of request timestamps,
drop anything older than the window, count what remains
Response on breach: 429 Too Many Requests with Retry-After header
The rate limiter is stateful but lightweight. It maintains an in-memory Map<string, { timestamps: number[]; windowStart: number }> keyed by {peerId}:{intent}. On every request, the timestamps array gets filtered to drop entries older than the window; if what’s left is at the limit, the request is rejected with 429 and a calculated Retry-After based on when the oldest in-window request will age out. On daemon restart, limits reset, a deliberate tradeoff for simplicity. For production deployments that need persistent rate-limit state, the store could be backed by Redis or similar, but the default is designed for personal gateways where “restart and reset” is acceptable.
Layer 5: Agent-Comms Policy
Question: Even if the message passes all security checks, how should my agent handle it?
This is the delegated authority layer discussed in the previous article. The Doorman doesn’t just enforce security, it enforces behavioral policy. After a message passes authentication, trust, scope, and rate checks, the Doorman consults the agent-comms policy to determine:
Should this message be delivered to the agent at all? (
off)Should it be summarized before delivery? (
summary)Should the agent handle it autonomously? (
escalate)Should the human see the full raw message? (
full)
The Doorman makes this decision before the message ever reaches the agent. If the policy says off, the agent literally never sees it. This is important: if the agent never sees the message, it can’t be prompt-injected, socially engineered, or otherwise manipulated by it.
The 6-Step Validation Algorithm
Putting it all together, the Doorman’s checkAccess() method follows six steps:
1. Peer lookup — find the peer record for fromGatewayId
2. Approval status check — verify status is 'approved'
3. Scope bundle determination — load granted scopes for this peer
(or fall back to v0.1 defaults for legacy peers)
4. Intent grant lookup — check if message.type is in granted scopes
5. Scope coverage check — verify the request's topic (if any) is allowed
by the matched scope grant
6. Rate limit check — verify the peer hasn't exceeded their budget
Project-membership checks (e.g., “is this peer actually a member of the project they’re querying?”) happen one layer up, in the intent handlers themselves, after checkAccess() returns. That separation matters: the Doorman is responsible for whether you can speak this intent at all; the handler is responsible for whether the specific resource you’re asking about exists and includes you. Different questions, different answers, different return codes (403 scope-violation vs. 403 not-a-member).
Any step can return a structured rejection:
Step 1-2 fail →
403 unknown-peeror403 not-approvedStep 3-5 fail →
403 scope-violationwith the offending intent typeStep 6 fails →
429 rate-limitedwithRetry-After: N
All rejections are cryptographically signed, so the sending gateway can verify the rejection is authentic and surface actionable feedback to its human.
Why Six Steps?
Each step catches a different class of threat:
Steps 1-2 (Peer trust) protect against:
Unknown actors sending forged or unsolicited messages
Replay attacks using old requests
Removed peers attempting to reconnect with fresh credentials
Steps 3-5 (Scope enforcement) protect against:
Overreach by approved peers exceeding their granted permissions
Project membership leakage across federation boundaries
Confused deputy attacks where a trusted peer is compromised and used as a proxy
Step 6 (Rate limiting) protects against:
Accidental loops in peer agents generating runaway traffic
Compromised trusted peers being weaponized
DDoS from otherwise legitimate, approved sources
A confused deputy attack is the subtle one. Imagine Alice is federated with Bob for message intents. Bob’s agent is compromised. It sends a project.query to Alice’s gateway, claiming to be querying on behalf of Carol. Without scope enforcement, Alice’s agent might process this, it trusts Bob, after all. But Bob was never granted project.query scope. The Doorman rejects it at step 4, before the agent ever sees the request or has a chance to be tricked into serving as a deputy for an unauthorized operation.
Comparison to Alternatives
mTLS + RBAC: Mutual TLS authenticates the connection. RBAC checks role permissions. Both are coarse-grained and assume a shared identity provider. OGP replaces the identity provider with Ed25519 keypairs, and replaces RBAC roles with per-peer intent scopes. The result is finer-grained, decentralized, and doesn’t require a shared CA.
OAuth 2.0 / OIDC: These require a trusted authorization server. OGP intentionally avoids any third-party issuer. Peers authenticate each other directly using keypairs they control. This is slower to bootstrap (you need the bilateral handshake) but eliminates a central point of compromise.
Capability-based security: OGP’s scope grants are essentially capabilities, “here is the set of things you are allowed to do.” But unlike pure capability systems, OGP capabilities are revocable and auditable. The peer registry shows exactly who has what.
The Honest Limitation
The Doorman is not formally verified. We don’t have a TLA+ model or a Coq proof that checkAccess() is correct. What we have is:
A 198-test suite (across 25 test files) covering signing canonicalization, agent-comms default-deny, scope grants, federation approval/preflight, peer tombstones, project membership, well-known peer-id authentication, and reply signature verification, every layer the Doorman enforces has direct test coverage
A three-node mesh test validating rejection paths
Empirical testing of confused deputy scenarios
Code review by multiple contributors
For a personal agent gateway, this is solid. For a financial system processing billions, you’d want formal methods. OGP is designed for the personal-to-small-team deployment tier, where “well-tested and carefully reviewed” is the right tradeoff against “mathematically proven.” The protocol is extensible, a future deployment could swap in a formally verified Doorman without changing the wire format.
Bottom Line
The Doorman is OGP’s most important component and the one most people overlook. They see the handshake, the signatures, the project layer, and they miss the thing that actually protects the agent from the network.
If you’re building federated agent infrastructure, you need something like the Doorman. Not because your peers are malicious, most aren’t. But because your agent is trusting, your boundaries are personal, and “no” is the most important word in security.
Five layers of no means five independent chances to catch a mistake before it becomes a breach. In a world where AI agents are gaining more access to more systems, that paranoia isn’t excessive. It’s the minimum.
How to Try It
OGP is open source and installable today:
npm install -g @dp-pcs/ogp@latest
ogp setup
ogp whoami # confirm identity & keypair before starting
ogp start --background
ogp status # shows your gateway URL Source, spec, and docs all live at github.com/dp-pcs/ogp.
If you install it and it breaks, file an issue. That’s how this gets better.
David Proctor is VP of AI at Trilogy. The Doorman ships with every OGP installation. Read the source in src/daemon/doorman.ts or install it: npm install -g @dp-pcs/ogp


