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 )
Enter fullscreen mode Exit fullscreen mode
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:
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);
Enter fullscreen mode Exit fullscreen mode
Same thing in Go, which along with Rust is the primary reference:
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)
Enter fullscreen mode Exit fullscreen mode
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.
- Authorization that travels with the request This is the piece I care most about, and it is the specific gap I built N-AALP to close.
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.
- Approvals that cannot be reused or pointed at different arguments Two failure modes have bitten every human-in-the-loop agent system I have looked at. A user approves an action and something mutates the arguments between approval and execution. Or one approval gets spent twice.
An N-AALP Approval object names, under signature, the content id of the exact argument object it approves:
{ approves: <content-id-of-args>, approver: signer,
grant: effect, nonce: bstr, not_after: uint }
Enter fullscreen mode Exit fullscreen mode
Because arguments are named by their content hash, changing any argument changes the id and the approval no longer matches. You get ApprovalMismatch. You cannot approve “transfer 50″andhaveitexecute”transfer5000,” not because a validator caught it, but because the approval is arithmetically about different bytes.
Single use is a hash-chained ledger with an atomic compare-and-set on the approval’s content id. First append wins. A second append for the same id is rejected with AlreadyConsumed.
And an approval that is required but not yet granted produces a distinct signed ApprovalHeld object. Not a silent success. Not a denial that looks like a failure. A third state you can actually act on, which is what a human-in-the-loop queue needs.
- An audit trail an outsider can check without trusting you An ordering authority appends a signed receipt for each object it accepts: { prev: , obj: , seq, at }. Reorder, omit or substitute anything and you break a prev link or duplicate a seq, and a verifier sees it. The authority never modifies the object to order it, so the original signature stays valid; ordering is an outer layer.
Underneath that is something I think is the more interesting primitive. Every object can name its causes by content id in field 8. That is a signed partial order: the edge “A caused B” is proven by B’s signature over A’s content id. No authority needs to be present to check it. You can hand someone a bag of objects and they can reconstruct and verify the causal graph offline. Cycles and causes-after-effects are rejected with CausalViolation.
An independent auditor can also detect equivocation from the signed receipts alone: one authority issuing two receipts at the same sequence number naming different objects. That is a fork, provable, from the receipts.
Being straight about the limit, because the design document is: the chain reveals equivocation and reveals omission of events you know about. It cannot force an authority to hand over events it chooses to withhold. That residual is a trust property of the authority and no wire format removes it.
- Wrapping MCP and A2A without giving up governance You are not rewriting your stack, and neither am I. N-AALP carries foreign agent protocols octet for octet inside a signed object, by carriage class rather than by per-protocol adapter. Six classes cover it: JSONRPC (which is where MCP and A2A core land), HTTP, MSG, STREAM, DOC, and OPAQUE as a universal catch-all for anything not yet defined.
The carried bytes must not be re-serialized, canonicalized, summarized or rewritten. N-AALP metadata goes around the foreign message, never inside it. So a carried MCP call round-trips byte-identical while gaining an identity, an effect, an approval binding and an audit position it did not have.
One rule in there is worth pulling out, because it is the kind of thing that becomes a breach report. A foreign protocol’s identity never becomes an N-AALP authorization identity. A carried MCP request claiming to be “from” some principal is authorized by the N-AALP signer who wrapped it, full stop. Confused-deputy attacks through a bridge are the obvious way these systems get owned, and the containment is normative rather than advisory.
Adding a protocol is a registry row, not new envelope machinery, and there is an experimental id range you can use immediately with no registration.
- Streams you can sign once instead of per chunk Signing every chunk of a stream with ML-DSA is not viable. An ML-DSA signature is large: 3309 bytes for ML-DSA-65 and 4627 bytes for ML-DSA-87 (FIPS 204). Per chunk, that is absurd.
So a stream is three signed objects and a body of unsigned chunks. StreamOpen establishes the stream’s identity, its effect, and its approval binding if it causes an effect. The chunks themselves are not individually signed, because the transport’s encryption already authenticates each chunk to the peer. Then StreamCommit carries a rolling SHA-384 over the complete ordered stream, which makes the entire content non-repudiable with one signature instead of N. Optional signed checkpoints let a verifier confirm a prefix without waiting for the end.
If the recomputed digest disagrees you get StreamDigestMismatch, and an effect that is not authorized refuses the stream at StreamOpen, before a single chunk moves.
How it sits on N-PAMP
N-AALP is the application layer. N-PAMP is the transport underneath it. They are separate drafts and separate repositories on purpose.
application agent
| emits / consumes N-AALP objects
+--------------------------------------------------+
| N-AALP object layer |
| envelope, identity, effect, approval, audit, |
| delivery, streaming, carriage | | |
+--------------------------------------------------+
| transport binding
+-----------+-----------+-------------+------------+
| N-PAMP | QUIC | WebSocket | HTTP |
+-----------+-----------+-------------+------------+
Enter fullscreen mode Exit fullscreen mode
The split is a clean division of guarantees. Integrity, identity, non-repudiation, effect and audit are object-level and present on all four transports. Confidentiality, forward secrecy and connection authentication are transport-provided and conditional. The object layer never reads a guarantee from the transport that it needs for its own correctness.
N-PAMP is the reference transport because it completes the picture: post-quantum authenticated encryption on every frame, a mutually authenticated post-quantum handshake, and twenty multiplexed channels whose ids are the same twenty channel ids N-AALP uses. Over N-PAMP, one object is one frame body on its semantic channel, and foreign carriage rides the Bridge channel 0x000D, reusing the byte-exact carriage N-PAMP already provides rather than duplicating it.
There is one rule at that seam I would point at specifically. An object marked sensitive must not be emitted in cleartext over a non-confidential transport. The binding refuses to send it and returns ConfidentialTransportRequired. That turns “N-AALP over plain HTTP leaks your payloads” from a footgun into a refusal.
I should also say: I am not the only person working on this problem at the IETF. There are drafts on agentic HTTP conventions, on an agent transport protocol, on JWT-based agentic identity and intent binding. Several approach it through tokens or through the transport. N-AALP’s bet is specifically that the signed object is the right unit, because it is the only thing that still exists after the connection closes.
Getting the code
The spec, all ten reference implementations, and the conformance suite are public under Apache-2.0 in one repository: github.com/bubblefish-tech/naalp_protocol. Go and Rust are the primary references and produce byte-identical output; the snippets above are from those implementations.
Install the SDK for your language:
npm install naalp # TypeScript / JavaScript, Node >= 22
pip install naalp # Python >= 3.9
go get github.com/bubblefish-tech/naalp_protocol/impl/go
Enter fullscreen mode Exit fullscreen mode
Or clone the whole thing – spec, SDKs, oracles, harness and corpus:
git clone https://github.com/bubblefish-tech/naalp_protocol
cd naalp_protocol/impl/go && GOWORK=off go build ./...
Enter fullscreen mode Exit fullscreen mode
All ten ports (Go, Rust, Python, TypeScript, C#, Swift, Java, Kotlin, PHP, Ruby) implement the same spine and are graded against the same 239-case conformance corpus: deterministic CBOR codec, content id, COSE signing input, signer id, the effect lattice, approval and audit records, delivery, streaming, carriage, channels, and federation.
Two other public anchors:
The N-AALP Internet-Draft, draft-bubblefish-naalp-00, on the IETF Datatracker: datatracker.ietf.org/doc/draft-bubblefish-naalp/
The byte-level wire authority, spec/naalp-draft-00.cddl. CDDL is a schema language for CBOR. Prose and CDDL cannot disagree; where they appear to, the CDDL governs.
The N-PAMP substrate that N-AALP rides on is also public with code: github.com/bubblefish-tech/npamp_protocol, docs at bubblefish-tech.github.io/npamp_protocol/docs/.
Enter fullscreen mode Exit fullscreen mode
Two honest notes about the reference crypto. The Python port uses dilithium-py, which is correct but not constant-time – fine for interop and reference work, not for production key handling. Swap in a constant-time FIPS 204 provider and the object bytes stay identical. PHP and Swift have no deterministic ML-DSA seed-keygen path in their ecosystems, so they grade every non-crypto operation plus Ed25519 and honestly report the ML-DSA leg as skipped rather than claiming a pass.
Proving your implementation actually conforms
This is the part I would want to see if someone showed me a new protocol, so it is the part I built hardest.
Two implementations agreeing proves nothing if the test vectors came from one of them. So the conformance corpus is assembled by independent Python oracles, and every expected value traces to an outside standard, never to an N-AALP implementation: RFC 8949 for canonical CBOR, FIPS 180-4 for SHA-384, RFC 9052 for the COSE signing input, FIPS 204 and NIST ACVP for ML-DSA, RFC 8032 for Ed25519, and from-scratch byte constructors for the rest. A bug shared across every implementation cannot quietly pass, because the answers do not come from the implementations.
The one thing no external test vector covers is the full deterministic ML-DSA COSE_Sign1. That one is graded by cross-language consensus: seven language ports must agree byte for byte.
You grade your own implementation two ways. Loop the corpus through your code directly, where valid cases must match the expected bytes and invalid cases must be rejected. Or write a small adapter and let the runner drive it as a subprocess over a length-prefixed JSON contract:
./harness/runner/naalp-conform run --testee "node harness/adapters/typescript/adapter.mjs"
# RESULT: PASS (239 graded, 0 unimplemented/skipped)
Enter fullscreen mode Exit fullscreen mode
The adapter holds no test logic, only translation, so it can be written in any language. It exits non-zero on any failure, which means it drops into CI without glue. The negative cases, the ones an implementation must reject, are where real conformance bugs surface, and they are graded too.
What N-AALP does not do
It is draft-00. Pre-adoption, Independent Submission, single maintainer, no working-group consensus. The wire format can change in a later revision.
It provides no transport, no connection management and no RPC. Objects are transport-independent by design; carrying them is your client's job or N-PAMP's.
It provides no confidentiality. Signing is not encryption. An N-AALP object is signed, not secret, and confidentiality is the transport's job. That is why the sensitive-payload refusal exists.
It cannot make an ordering authority honest about events it never publishes.
created is the signer's own claim and is explicitly not trustworthy for ordering. A signer can lie about its clock. The real temporal fact is position in the receipt chain.
The effect label is an authorization input, but an optional safety label is still only an accountable claim by its signer, not a guarantee the content is safe.
It does not stop an agent from doing something stupid that it was legitimately authorized to do.
Enter fullscreen mode Exit fullscreen mode
Where I would like the argument
The claims that should get attacked hardest, in the order I would attack them:
Is a closed four-value effect vocabulary actually enough to authorize against, or does it collapse the moment a real capability model meets it?
Does content-addressed single-use approval hold up under concurrency in a way that survives a distributed ledger, or does the compare-and-set become the bottleneck?
Is the object really the right unit, or does per-object post-quantum signing cost more than the guarantee is worth at agent-swarm message rates?
Enter fullscreen mode Exit fullscreen mode
Number three is the one I am least certain about and the one I would most like real numbers on.
Read the draft, read the CDDL, and tell me where it breaks. If you have built human-in-the-loop approval for agents and hit the argument-mutation or double-spend problem, I would like to hear how you solved it, because that is the failure mode that started this.
N-AALP and N-PAMP are developed by BubbleFish Technologies. Code and original content are Apache-2.0; the Internet-Drafts are additionally under the IETF Trust’s BCP 78. Anyone may implement either protocol royalty-free.
답글 남기기