Opaque Handles: How Agents Use Secrets Without Seeing Them
The most fundamental concept in the Never-Leak Protocol. Opaque handles decouple AI agents from the secret values they need, making it architecturally impossible for secrets to leak through the agent's context window.
The Problem
Today, AI agents that need to call APIs or access databases receive secret values directly in their context window. An orchestrator might inject an API key into the agent's system prompt, or the agent itself might read a secret from a file or environment variable.
This is fundamentally unsafe. Any data in an LLM's context can be memorized, leaked via prompt injection, or accidentally included in outputs. The model has no concept of "confidential" versus "public" data -- everything in the context window is equally accessible.
# The agent sees the raw secret value # Any prompt injection can now exfiltrate it agent.context = "Use API key: sk-1234567890abcdef to call the API" # The secret is now in the LLM's context window # It can be memorized, leaked, or extracted
The NL Protocol Solution
Instead of secrets, agents work with opaque handles: references like {{nl:api-key}} that carry no information about the secret's value. The handle is just a name -- it tells you what the secret is for, but nothing about what it contains.
The agent includes these handles in action templates -- structured requests that describe what needs to be done. The NL System receives the template, resolves the handles inside an isolation boundary, executes the action, and returns only the result to the agent.
{
"type": "exec",
"template": "curl -H 'Authorization: Bearer {{nl:api-key}}' https://api.example.com",
"purpose": "Fetch user profile"
}
The agent never sees sk-1234567890abcdef. It only sees the result of the curl command. The secret exists only inside the isolated execution boundary, for the duration of a single action.
How It Works
The opaque handle resolution process involves six steps. At no point does the agent have access to the secret value.
The agent builds a request containing {{nl:secret-name}} handles wherever secret values are needed. The template also includes a purpose string explaining why the action is necessary.
The system checks the agent's identity document and confirms it has a valid scope grant for each referenced secret. If any check fails, the request is rejected before any secret is accessed.
The handle {{nl:api-key}} is resolved to the actual secret value inside the system's secure memory. This value never leaves the isolation boundary.
The template is populated with real values and executed in an isolated subprocess. The subprocess has no connection back to the agent -- it runs independently with a clean environment.
Before returning results to the agent, the output is scanned for any occurrence of secret values. If a secret appears in stdout or stderr (for example, a verbose error message that echoes credentials), it is redacted.
Only the sanitized output (stdout, stderr, exit code) is returned. The agent can reason about the result, but has no path to the underlying secret values.
Agent NL System Secure Storage | | | | action_request | | | {{nl:api-key}} | | | ----------------------> | | | | resolve handle | | | ----------------------> | | | sk-1234... | | | <---------------------- | | | | | [execute in isolation] | | [scan & sanitize output] | | | | | action_response | | | (clean result only) | | | <---------------------- | |
Why This Matters
Even if the agent is compromised through prompt injection, it cannot leak what it does not have. The attacker controls the agent's behavior, but the secret values are simply not available in the context.
The handle {{nl:api-key}} tells an attacker nothing. It is just a reference name. The attacker cannot derive, guess, or reconstruct the secret value from the handle alone.
Secret rotation is invisible to the agent. The handle {{nl:api-key}} stays the same; only the underlying value changes. No agent code needs to be updated, no prompts need to be rewritten.