Sign the message, not the tunnel: Introducing N-AALP for AI agents A developer introduced N-AALP, an application-layer object protocol that makes the message itself carry identity, authorization, approval, and audit, verifiable offline on any transport. The protocol closes the gap where agent security is inherited from the connection, ensuring that proofs survive replay, forwarding, and logging. Agent security today is inherited from the connection. N-AALP makes the message itself carry identity, authorization, approval and audit, verifiable offline, on any transport. Your agent just deleted a production table. The audit log says the request was approved. Now prove it. Not "show me the log line" - prove it, to someone who does not trust your log, your gateway, or your database. Which key approved it? Were those the exact arguments that were approved, or did something rewrite them after approval? Was that approval already used once? If your answer to any of those starts with "well, our gateway checks that," then the proof lives in your infrastructure, not in the message. Replay the message somewhere else and the proof is gone. This is the gap I have been working on. It has a name worth stating plainly: agent security today is inherited from the connection. TLS tells you the tunnel was private. mTLS tells you which service opened it. A bearer token tells you someone had a credential. None of that survives the message being written to a queue, forwarded by a relay, logged, replayed, or handed to a second agent. The moment a message leaves the connection it arrived on, it is just bytes with no provable origin. N-AALP is my attempt to close that. It is an application-layer object protocol where the message, not the connection, is the unit of security and governance. Full disclosure before you read further: I wrote it. I am the sole editor and maintainer, it is draft-bubblefish-naalp-00, an Independent Submission, and it claims no IETF working-group consensus. I would rather you read the spec and tell me where I am wrong than take my word for anything below. There is a section at the end listing what it does not do. The one-object idea Every N-AALP message is one signed object. Not a request type, not an envelope-plus-payload, not a header format with a body convention. One structure, one signature, one identity model, one authorization model, one audit model, and every channel and every transport reuses them without variation. Two plain definitions, because the rest depends on them: CBOR is a binary format for structured data, like JSON but smaller and binary. Deterministic CBOR means the same logical data always produces exactly the same bytes: keys sorted a fixed way, numbers encoded the shortest possible way, no ambiguity. That matters because a signature is over bytes. If two implementations encode the same object differently, a signature made by one will not verify on the other. COSE is the standard way to sign CBOR, the way JWS signs JSON. COSE Sign1 is its single-signer form. Here is the actual signed structure, from the wire authority in spec/naalp-draft-00.cddl: naalp-object = { 1 : bstr, ; id - content id of this object 2 : uint, ; kind - what kind of object this is 3 : channel-id, ; channel - which of the 20 channels 4 : uint, ; tier - capability tier 0 = baseline 5 : bstr, ; signer - self-certifying signer id 6 : uint, ; created - signer's claimed time advisory 7 : effect, ; effect - what this is allowed to do 8 : bstr , ; causes - content ids of causing objects 9 : profile, ; profile - crypto profile 10 : any, ; body - kind-specific body ? 11 : { uint = any }, ; ext - unknown keys ignored ? 12 : { uint = any }, ; cext - unknown keys reject the object } effect = & read only: 0, idempotent write: 1, non idempotent write: 2, destructive: 3 The whole map is the signed payload. Change any field and the signature breaks. Two fields are doing unusual work. id is a content id: the SHA-384 hash of the object's own body with field 1 removed, wrapped as a multihash so the hash algorithm is self-describing. So the id is not a random UUID someone assigns. It is a function of the content. Anyone can recompute it and check it. That single choice is what makes the rest work, and I will come back to it twice. signer is derived from the public key, so the identity is a function of the key itself. There is no directory to look up, no certificate authority to ask, no online check. You verify an object with what is in your hand. The consequence: an N-AALP object verifies offline, and it verifies identically whether it arrived over N-PAMP, QUIC, a WebSocket, plain HTTP, or on a USB stick. Signing and verifying one TypeScript, from the reference implementation: js import { cose, identity, envelope } from 'naalp'; import { U, T, M } from 'naalp/cbor'; const seed = new Uint8Array 32 ; // a real 32-byte key seed in production const alg = cose.ALG MLDSA65; const pk = cose.mldsaKeygen 'ML-DSA-65', seed ; const sid = identity.signerId alg, pk ; const body = new M new U 1 , new T 'hello' ; const obj = new envelope.Object { kind: 1, channel: 4, // 0x0004 = Governance signer: new TextEncoder .encode sid , created: 1785000000000n, effect: 2, // non idempotent write profile: cose.PROFILE PUBLIC, body, } ; const signed = envelope.sign obj, alg, seed ; const got = envelope.verify cose.PROFILE PUBLIC, alg, pk, c, k = c === 4n && k === 1n, signed ; Same thing in Go, which along with Rust is the primary reference: js var seed mldsa65.SeedSize byte pk, sk := mldsa65.NewKeyFromSeed &seed obj := &envelope.Object{ Kind: 0, // Hello Channel: 0, // 0x0000 = Control Tier: 0, Signer: pk.Bytes , Created: 1785000000000, Effect: 0, // read only Profile: uint64 cose.ProfilePublic , Body: cbor.Tstr "hello" , } signed, err := envelope.Sign obj, cose.MLDSA65Signer{SK: sk} got, err := envelope.Verify cose.ProfilePublic, cose.MLDSA65Verifier{PK: pk}, channels.KindValidator, nil, signed Note the last real argument to verify. You pass a validator for which channel and kind you are willing to accept. Verification is not just "is the signature good," it is "is this the kind of object I agreed to process." Everything fails closed with a named error: ContentIdMismatch, HeaderBodyMismatch, UnknownCriticalExt, NonCanonical, UnknownKind. The signature algorithm is ML-DSA, the post-quantum signature standard NIST published as FIPS 204, run in its deterministic mode. There is an optional Ed25519 hybrid leg if you want a classical signature alongside it. The reason to care is not quantum computers arriving next Tuesday. It is that receipts, approvals and audit chains are records you keep for years. A signature you need to still be sound in 2040 should not be one that a future machine can forge retroactively. What this lets you build I want to be careful here, because I have seen the "previously impossible" framing on protocol posts and it is almost always false. Nearly all of this was possible before if you were willing to hand-assemble it per service and re-review it forever. What changes is that these become properties of the message that hold everywhere it goes, instead of behaviors of one gateway you have to trust and re-implement. Most systems that label agent actions treat the label as a hint about intent. My own transport protocol, N-PAMP, does exactly that: it carries a safety label and says outright that the label describes intent and does not replace authorization. That is honest, and it leaves a hole. A hint that nothing checks is decoration. In N-AALP the effect field is an authorization input. There are exactly four values, closed, no extension. Each one states what it authorizes and what it denies. read only authorizes observation and denies any write. destructive sits at the top and authorizes irreversible change. Before anything executes, the endpoint checks the object's effect against the capability that was actually granted, and an object whose effect exceeds its capability is denied with EffectNotAuthorized. Then the part that matters most: an effect value it does not recognize is treated as destructive. Absence on a state-mutating request is treated as destructive. It fails closed, upward, toward "refuse," never toward "probably fine." What you build: a policy check that works on a message that arrived from anywhere, including one replayed from a queue three days later, without asking the transport or a gateway what it thinks happened. An N-AALP Approval object names, under signature, the content id of the exact argument object it approves: { approves: