Getting Started with NL Protocol
What You'll Build
In this guide, you'll build a minimal NL Protocol implementation that achieves Basic conformance (Levels 1-3). This covers the three foundational security layers:
Cryptographic identity for every agent interacting with secrets.
Agents request actions, not secrets. Scoped grants govern access.
Secrets exist only inside isolated subprocesses. Never in the agent's context.
Prerequisites
Before you start, make sure you have:
- ✓ Understanding of HTTP APIs and JSON — the wire protocol uses standard JSON messages over HTTP, stdio, or WebSocket.
- ✓ A programming language that supports subprocesses and environment variables — Python, Node.js, Go, Rust, or any language with process spawning capabilities.
- ✓ Basic understanding of cryptographic hashing (SHA-256) — used for audit trail integrity. You don't need to implement crypto from scratch; standard libraries work fine.
Core Architecture
The NL Protocol defines two primary components: the AI Agent (your application) and the NL Provider (the system you'll build). The agent sends action requests containing opaque handles. The provider resolves secrets, executes commands in isolation, and returns only results.
┌─────────────┐ action_request ┌──────────────┐
│ AI Agent │ ────────────────── │ NL Provider │
│ (your app) │ │ (you build) │
│ │ ◄────────────────── │ │
└─────────────┘ action_response │ ┌──────────┐ │
│ │ Isolation │ │
│ │ Boundary │ │
│ └──────────┘ │
└──────────────┘
The key insight: the agent never sees secret values. It works with handles like {{nl:api-key}}, and the NL Provider resolves them inside an isolation boundary that the agent cannot access.
Step 1: Agent Identity (Level 1)
Every agent interacting with your system needs an Agent Identity Document (AID). This is the foundation of the entire protocol — without identity, there is no access control.
Create a minimal AID for your agent:
// Minimal AID — the agent's passport { "agent_uri": "nl://mycompany/my-agent/1.0", "display_name": "My Agent", "scope": ["secrets/api-key", "secrets/db-password"], "trust_level": "L1", "created_at": "2026-01-15T00:00:00Z", "expires_at": "2027-01-15T00:00:00Z" }
nl:// URI scheme. Format: nl://org/name/version. Step 2: Scope Grants (Level 2)
A scope grant authorizes a specific agent to use a specific secret for specific actions, under specific conditions. This is the per-task, per-secret access control that makes NL Protocol different from traditional secret management.
// Grant: allow my-agent to exec with api-key { "agent_uri": "nl://mycompany/my-agent/1.0", "secret": "secrets/api-key", "actions": ["exec"], "conditions": { "valid_until": "2026-06-01T00:00:00Z", "max_uses": 1000 } }
When an action request arrives, the NL Provider evaluates access through a strict chain:
Every check must pass. If the agent's AID doesn't include the requested secret in its scope, access is denied even if a scope grant exists. If the scope grant has expired or exhausted its max_uses, access is denied.
Step 3: Action Execution (Level 2 + 3)
This is where the core security guarantee is enforced. When an agent sends an action request, the NL Provider processes it through a strict pipeline:
- 1 Receive action_request JSON from the agent
- 2 Verify agent identity (AID lookup)
- 3 Check scope grant covers the requested secret and action type
- 4 Resolve
{{nl:secret-name}}placeholders with actual secret values - 5 Execute command in an isolated subprocess (no shell expansion!)
- 6 Capture stdout, stderr, and exit_code from the subprocess
- 7 Scan output for leaked secret values and replace with
[NL-REDACTED:name] - 8 Return action_response to the agent (result only, never secrets)
Here's the core execution logic in pseudocode:
function execute_action(request): // 1. Identity verification agent = lookup_aid(request.agent_id) if agent is null: return error("unknown agent") // 2. Scope grant check grant = find_grant(agent.uri, request.secret, request.action_type) if grant is null or grant.expired or grant.uses_exhausted: return error("access denied") // 3. Resolve placeholders inside isolation boundary resolved_cmd = request.template for each handle in extract_handles(resolved_cmd): secret_value = secret_store.get(handle.name) resolved_cmd = resolved_cmd.replace(handle.placeholder, secret_value) // 4. Execute in isolated subprocess — NO shell expansion process = spawn_subprocess( cmd: resolved_cmd, shell: false, // Critical: never use shell=true env: {}, // Clean environment timeout: 30s ) stdout, stderr, exit_code = process.wait() // 5. Sanitize output — remove any leaked secret values stdout = sanitize_output(stdout, secrets_used) stderr = sanitize_output(stderr, secrets_used) // 6. Return result (never the secret) return { status: "success", exit_code: exit_code, stdout: stdout, stderr: stderr }
Always spawn subprocesses without a shell (shell=false). Shell expansion can be exploited to exfiltrate secrets via environment variable interpolation, command substitution, or globbing.
Step 4: Output Sanitization (Level 2)
After execution, scan ALL output for secret values before returning anything to the agent. This is the last line of defense — even if a command somehow echoes a secret, the agent won't see it.
Check for multiple encodings of each secret value:
Direct string comparison of the raw secret value against output.
Check for base64(secret_value) in the output.
Check for percent-encoded version (%XX sequences).
Check for hexadecimal representation of the secret bytes.
Replace any match with a redaction marker that identifies the secret name but not its value:
// Before sanitization (raw subprocess output) "Connected to postgresql://admin:[email protected]/mydb" // After sanitization (what the agent receives) "Connected to postgresql://admin:[NL-REDACTED:db-password]@db.example.com/mydb"
What's Next?
Congratulations — you've built a system with Basic conformance (Levels 1-3)! Your implementation now ensures that agents never see secret values, access is scoped and conditional, and execution happens in isolation.
To reach Standard conformance (Levels 1-5), add these two additional layers:
Before resolving secrets, analyze the command template for dangerous patterns. Deny rules can block commands that attempt to exfiltrate data (e.g., piping to curl, writing to network sockets, or encoding output).
Create a SHA-256 hash-chained record for every action. Each record includes agent identity, action type, secret handle (not value), timestamp, and the hash of the previous record. This produces a tamper-evident log.