Back to Implement Developer Guide

Getting Started with NL Protocol

~15 min read | Basic Conformance (Levels 1-3)
1

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:

Level 1
Agent Identity

Cryptographic identity for every agent interacting with secrets.

Level 2
Action-Based Access

Agents request actions, not secrets. Scoped grants govern access.

Level 3
Execution Isolation

Secrets exist only inside isolated subprocesses. Never in the agent's context.

2

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.
3

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.

Architecture Overview
┌─────────────┐    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.

4

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:

Agent Identity Document (AID)
// 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"
}
1
agent_uri — The unique identifier for this agent. Uses the nl:// URI scheme. Format: nl://org/name/version.
2
scope — Defines the maximum set of secrets this agent is allowed to access. This is the upper bound; actual access is further restricted by scope grants.
3
trust_level — Determines the agent's capabilities. L0 = untrusted, L1 = basic verified, L2 = attested, L3 = hardware-attested. Start with L1.
5

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.

Scope Grant
// 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:

1 Verify agent identity 2 Check AID scope 3 Check scope grant 4 Check conditions

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.

6

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. 1 Receive action_request JSON from the agent
  2. 2 Verify agent identity (AID lookup)
  3. 3 Check scope grant covers the requested secret and action type
  4. 4 Resolve {{nl:secret-name}} placeholders with actual secret values
  5. 5 Execute command in an isolated subprocess (no shell expansion!)
  6. 6 Capture stdout, stderr, and exit_code from the subprocess
  7. 7 Scan output for leaked secret values and replace with [NL-REDACTED:name]
  8. 8 Return action_response to the agent (result only, never secrets)

Here's the core execution logic in pseudocode:

Action Execution — 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
  }
!
Critical: No shell expansion

Always spawn subprocesses without a shell (shell=false). Shell expansion can be exploited to exfiltrate secrets via environment variable interpolation, command substitution, or globbing.

7

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:

Plaintext match

Direct string comparison of the raw secret value against output.

Base64-encoded

Check for base64(secret_value) in the output.

URL-encoded

Check for percent-encoded version (%XX sequences).

Hex-encoded

Check for hexadecimal representation of the secret bytes.

Replace any match with a redaction marker that identifies the secret name but not its value:

Sanitization Example
// 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"
8

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:

L4 Pre-Execution Defense

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).

L5 Audit Trail

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.