NL Protocol Specification v1.0 -- Level 2: Action-Based Access
Status: 1.0-draft Version: 1.0.0 Date: 2026-02-08 Level: 2 (Core Innovation) Conformance: Required for all tiers (Basic, Standard, Advanced)
Note: This document is a SPECIFICATION. It defines required behaviors, data formats, and protocols — not specific products or CLI commands. For implementations of this specification, see IMPLEMENTATIONS.md.
1. Purpose
This is the core innovation of the NL Protocol. Level 2 defines the fundamental paradigm shift:
Agents request ACTIONS, not SECRETS. The secret value NEVER enters the agent's context.
In traditional secret management, an agent retrieves a secret value and uses it in subsequent operations. This is fundamentally unsafe for AI agents because any data in the agent's context window (LLM memory) can be memorized, replicated in output, or exfiltrated via prompt injection.
The NL Protocol eliminates this risk entirely. Agents construct action
templates containing opaque handles ({{nl:...}}). The NL-compliant
system resolves these handles to real secret values, executes the action
in an isolated environment (Level 3), and returns only the result to the
agent. The agent never sees, touches, or has access to the secret value.
This specification defines:
- The placeholder syntax for opaque secret references
- Action types and their semantics
- Action request and response formats
- Scope Grants: time-bounded, conditional, per-secret permissions
- Output sanitization requirements
- Result-only propagation in multi-agent chains
2. The Paradigm Shift
2.1 Traditional Model (UNSAFE for AI Agents)
1. Agent: "Give me the value of API_KEY"
2. Secret Manager: "sk-1234567890abcdef"
3. Agent: *has the secret in its context window FOREVER*
4. Agent: *uses the secret in a command*
PROBLEM: At step 2, the secret enters the LLM's context.
From that moment, it can be:
- Memorized by the model
- Replicated in output to the user
- Exfiltrated via prompt injection attack
- Leaked through conversation history
- Included in training data (for some providers)
2.2 NL Protocol Model (SECURE)
1. Agent: "Execute: curl -H 'Auth: Bearer {{nl:API_KEY}}' https://api.example.com"
2. NL System: *resolves {{nl:API_KEY}} -> "sk-1234567890abcdef"*
3. NL System: *injects into isolated subprocess as env var*
4. Subprocess: *executes curl with the real secret*
5. NL System: *captures stdout/stderr*
6. NL System: *scans output for leaked secrets, redacts if found*
7. NL System: *wipes secret from memory*
8. Agent: *receives: HTTP 200 {"data": ...}*
RESULT: The agent NEVER had access to "sk-1234567890abcdef".
It only ever saw the handle "{{nl:API_KEY}}" and the result.
3. Requirements Summary
| ID | Requirement | Priority | Description |
|---|---|---|---|
| NL-2.1 | Action Request | MUST | Agents MUST submit actions with opaque handles, not requests for secret values. |
| NL-2.2 | Placeholder Syntax | MUST | The standard placeholder syntax {{nl:reference}} MUST be supported. |
| NL-2.3 | Reference Resolution | MUST | Placeholders MUST be resolved OUTSIDE the agent's context, inside the NL-compliant system. |
| NL-2.4 | No Secret Return | MUST | The NL-compliant system MUST NEVER return a secret value directly to an agent in any response. |
| NL-2.5 | Action Types | MUST | At minimum, the exec, template, inject_stdin, and inject_tempfile action types MUST be supported. |
| NL-2.6 | Result Sanitization | MUST | Action output (stdout/stderr) MUST be scanned for leaked secrets before returning to the agent. |
| NL-2.7 | Scope Validation | MUST | Every placeholder MUST be validated against the agent's Scope Grant before resolution. |
| NL-2.8 | Action Response | MUST | Action responses MUST include status, result, list of secrets used (names only, NEVER values), and audit reference. |
| NL-2.9 | Scope Grants | MUST | Implementations MUST support time-bounded, conditional, per-secret permission grants. |
| NL-2.10 | SDK Proxy | SHOULD | Implementations SHOULD support the sdk_proxy action type for cloud vendor API proxying. |
| NL-2.11 | Delegation | SHOULD | Implementations SHOULD support the delegate action type for multi-agent delegation. |
| NL-2.12 | Dry Run | SHOULD | Agents SHOULD be able to request a dry run that validates permissions without executing. |
| NL-2.13 | Batch Actions | MAY | Implementations MAY support multiple actions in a single request. |
| NL-2.14 | Cross-Provider References | MAY | Implementations MAY support cross-provider placeholder syntax. |
4. Placeholder Syntax
4.1 Grammar
The NL Protocol defines a standard placeholder syntax for referencing secrets within action templates. The placeholder is an opaque handle: it identifies a secret but carries no information about the secret's value.
placeholder = "{{nl:" reference "}}"
reference = simple-ref
/ categorized-ref
/ scoped-ref
/ qualified-ref
/ provider-ref
simple-ref = name
categorized-ref = category "/" name
scoped-ref = project "/" environment "/" name
qualified-ref = project "/" environment "/" category "/" name
provider-ref = provider "://" path
name = 1*(ALPHA / DIGIT / "_" / "-" / ".")
category = 1*(ALPHA / DIGIT / "_" / "-")
project = 1*(ALPHA / DIGIT / "_" / "-")
environment = 1*(ALPHA / DIGIT / "_" / "-")
provider = 1*(ALPHA / DIGIT / "_" / "-")
path = 1*(ALPHA / DIGIT / "_" / "-" / "/" / ".")
4.2 Reference Formats
| Format | Syntax | Resolution Strategy | Example |
|---|---|---|---|
| Simple | {{nl:NAME}} |
Search all accessible scopes for a secret named NAME. If ambiguous (multiple matches), the system MUST return an error. |
{{nl:API_KEY}} |
| Categorized | {{nl:CATEGORY/NAME}} |
Search within the specified category across accessible projects/environments. | {{nl:database/DB_PASSWORD}} |
| Scoped | {{nl:PROJECT/ENVIRONMENT/NAME}} |
Look up the secret in the exact project and environment. | {{nl:myapp/production/STRIPE_KEY}} |
| Fully Qualified | {{nl:PROJECT/ENVIRONMENT/CATEGORY/NAME}} |
Exact match: project, environment, category, and name. No ambiguity possible. | {{nl:myapp/production/payments/STRIPE_KEY}} |
| Cross-Provider | {{nl:PROVIDER://PATH}} |
Delegate resolution to an external secret provider. The NL-compliant system acts as a bridge. | {{nl:aws-sm://us-east-1/prod/db-pass}} |
4.3 Resolution Rules
Simple references MUST be resolved by searching all projects and environments accessible to the agent (as defined by the agent's AID scope and applicable Scope Grants). If the search yields zero results, the system MUST return an error with code
SECRET_NOT_FOUND. If the search yields more than one result, the system MUST return an error with codeAMBIGUOUS_REFERENCElisting the possible qualified paths.Categorized references narrow the search to a specific category. The same zero/multiple match rules apply.
Scoped and fully qualified references are exact lookups. They either match exactly one secret or return
SECRET_NOT_FOUND.Cross-provider references require the NL-compliant system to have a configured bridge to the specified provider. If the provider is not configured, the system MUST return
PROVIDER_NOT_CONFIGURED.
4.4 Backward Compatibility
Implementations MAY support alias prefixes for backward compatibility:
{{vault:...}}as an alias for{{nl:...}}
The alias MUST resolve identically to the canonical {{nl:...}} format.
Implementations SHOULD log a deprecation warning when aliases are used.
4.5 Escaping
If the literal string {{nl: appears in content that is NOT a
placeholder, it MUST be escaped as {{{{nl: (double the opening braces).
Implementations MUST NOT attempt to resolve escaped placeholders.
4.6 Examples
# Simple reference
curl -H "Authorization: Bearer {{nl:GITHUB_TOKEN}}" https://api.github.com/user
# Categorized reference
psql "postgresql://admin:{{nl:database/DB_PASSWORD}}@localhost/mydb"
# Scoped reference
aws s3 ls --region us-east-1 # with {{nl:braincol/production/AWS_ACCESS_KEY}}
# Fully qualified reference
docker login -u deploy -p {{nl:braincol/production/registry/DOCKER_TOKEN}} ghcr.io
# Cross-provider reference (AWS Secrets Manager)
curl -H "X-Api-Key: {{nl:aws-sm://us-east-1/prod/api-keys/payment-gateway}}" \
https://payments.example.com/charge
# Multiple placeholders in one template
curl -u "{{nl:api/USERNAME}}:{{nl:api/PASSWORD}}" https://api.example.com/data
5. Action Types
5.1 Overview
Actions are typed operations that the NL-compliant system executes on behalf of the agent. Each action type defines how secrets are injected and how results are returned.
| Action Type | Description | Secret Injection Method | Conformance |
|---|---|---|---|
exec |
Execute a shell command with secret injection | Environment variables in subprocess | MUST |
template |
Render a template file with secrets | File written with restricted permissions | MUST |
inject_stdin |
Pipe a secret via stdin to a command | stdin pipe (no command-line arguments) | MUST |
inject_tempfile |
Create a temporary file containing a secret | Temporary file with 0o400 permissions | MUST |
sdk_proxy |
Proxy an SDK/API call with secret credentials | Internal SDK invocation (no subprocess) | SHOULD |
delegate |
Delegate an action to another agent with scoped token | Delegation token (Level 7) | SHOULD |
5.2 Action Type: exec
The agent submits a command template containing placeholders. The NL-compliant system resolves the placeholders, injects the secret values as environment variables in an isolated subprocess, and executes the command.
How it works:
- The system parses the command template and extracts all
{{nl:...}}placeholders. - Each placeholder is resolved to a secret value (after scope validation).
- The system creates a mapping:
NL_SECRET_0= first secret value,NL_SECRET_1= second, etc. - The command template is rewritten to reference these env vars instead of the placeholders.
- The command is executed in an isolated subprocess with the env vars set (see Level 3).
- stdout and stderr are captured and sanitized.
- Secret values are wiped from memory.
Example:
Agent submits:
{
"action": {
"type": "exec",
"template": "curl -H 'Authorization: Bearer {{nl:api/GITHUB_TOKEN}}' https://api.github.com/user"
}
}
System internally executes:
NL_SECRET_0="ghp_abc123..." \
curl -H "Authorization: Bearer $NL_SECRET_0" https://api.github.com/user
Agent receives:
{
"result": {
"stdout": "{\"login\":\"acme-bot\",\"id\":12345}",
"stderr": "",
"exit_code": 0
}
}
The agent never sees ghp_abc123....
5.3 Action Type: template
The agent submits a template file path (or inline template content) containing placeholders. The system resolves all placeholders and writes the result to a file with restricted permissions.
Security properties:
- The output file MUST be created with permissions
0o600(read/write owner only) or more restrictive. - The output file path MUST be in a secure temporary directory.
- The agent receives confirmation of the file path and the number of resolved placeholders, NOT the file contents.
Example:
Agent submits:
{
"action": {
"type": "template",
"template_content": "DB_HOST=localhost\nDB_USER=admin\nDB_PASS={{nl:database/DB_PASSWORD}}\nDB_NAME=myapp\n",
"output_path": "/tmp/nl-secure/app.env"
}
}
Agent receives:
{
"result": {
"output_path": "/tmp/nl-secure/app.env",
"resolved_count": 1,
"permissions": "0600"
}
}
5.4 Action Type: inject_stdin
The agent submits a command that expects a secret via stdin. The system
resolves the secret and pipes it to the command's stdin. This avoids
placing secrets in command-line arguments (which are visible in /proc
on Linux).
Example:
Agent submits:
{
"action": {
"type": "inject_stdin",
"command": "docker login -u deploy --password-stdin ghcr.io",
"secret_ref": "{{nl:registry/DOCKER_TOKEN}}"
}
}
System internally executes:
echo "$NL_SECRET_0" | docker login -u deploy --password-stdin ghcr.io
Agent receives:
{
"result": {
"stdout": "Login Succeeded",
"stderr": "",
"exit_code": 0
}
}
5.5 Action Type: inject_tempfile
The agent needs a secret as a file (certificates, SSH keys, service account JSON). The system creates a temporary file containing the secret value, executes the command with a reference to that file, and then securely deletes the file.
Security properties:
- File permissions:
0o400(read-only, owner only). - File location: implementation-defined secure temporary directory.
- Lifecycle: created immediately before execution, securely deleted immediately after (see Level 3 for secure deletion requirements).
- Maximum lifetime: configurable (default: 60 seconds).
Example:
Agent submits:
{
"action": {
"type": "inject_tempfile",
"command": "ssh -i {{nl:SSH_KEY_FILE}} -o StrictHostKeyChecking=yes deploy@prod.example.com 'systemctl restart app'",
"file_refs": {
"SSH_KEY_FILE": "{{nl:ssh/id_rsa_deploy}}"
}
}
}
System internally:
- Writes the SSH private key to
/tmp/nl-secure/tmpXXXXXXwith0o400permissions. - Rewrites the command:
ssh -i /tmp/nl-secure/tmpXXXXXX ... - Executes the command.
- Overwrites the file with random bytes, then deletes it.
Agent receives:
{
"result": {
"stdout": "",
"stderr": "",
"exit_code": 0
}
}
5.6 Action Type: sdk_proxy
The sdk_proxy action type enables cloud vendors and API providers to
implement the NL Protocol natively. Instead of executing a shell command,
the NL-compliant system makes an SDK or API call directly, using the
secret as an authentication credential.
This is the mechanism by which providers like AWS, Stripe, and GitHub can offer NL-compliant access to their services without requiring agents to use CLI wrappers.
Example:
Agent submits:
{
"action": {
"type": "sdk_proxy",
"provider": "aws",
"service": "s3",
"operation": "list_objects_v2",
"parameters": {
"Bucket": "my-data-bucket",
"Prefix": "reports/"
},
"credentials_ref": "{{nl:braincol/production/aws/AWS_CREDENTIALS}}"
}
}
System internally:
- Resolves the credentials.
- Creates an AWS SDK client with the resolved credentials.
- Calls
s3.list_objects_v2(Bucket="my-data-bucket", Prefix="reports/"). - Returns the result (without credentials).
Agent receives:
{
"result": {
"data": {
"Contents": [
{"Key": "reports/2026-01.csv", "Size": 1234},
{"Key": "reports/2026-02.csv", "Size": 5678}
]
}
}
}
Conformance: sdk_proxy is RECOMMENDED (SHOULD) but not REQUIRED.
Implementations that do not support sdk_proxy MUST support the same
use cases via exec with CLI commands.
5.7 Action Type: delegate
The delegate action type allows an agent to request that another agent
perform an action, with scoped permissions. The delegating agent issues
a Delegation Token (see Level 7) that grants the delegate a subset of
its own permissions.
Example:
Agent submits:
{
"action": {
"type": "delegate",
"delegate_to": "nl://acme.corp/deploy-bot/2.1.0",
"delegated_action": {
"type": "exec",
"template": "kubectl apply -f deployment.yaml --token={{nl:k8s/DEPLOY_TOKEN}}"
},
"delegation_scope": {
"secrets": ["k8s/DEPLOY_TOKEN"],
"max_uses": 1,
"ttl_seconds": 300
}
}
}
System:
- Verifies the delegating agent has permission for the requested secrets.
- Verifies the delegation scope is a strict subset of the delegating agent's scope.
- Issues a Delegation Token.
- Forwards the action to the delegate agent.
- Returns the result to the delegating agent.
The delegating agent NEVER sees DEPLOY_TOKEN's value. Neither does
the delegate agent -- it flows through the isolation boundary just as
in a normal exec action.
Conformance: delegate is RECOMMENDED (SHOULD) for implementations
targeting multi-agent workflows. REQUIRED for Advanced conformance.
6. Action Request Format
6.1 Schema
Every action request MUST conform to the following JSON structure:
{
"$schema": "https://nlprotocol.org/schemas/v1.0/action-request.json",
"nl_version": "1.0",
"request_id": "req_550e8400-e29b-41d4-a716-446655440000",
"agent": {
"agent_uri": "nl://anthropic.com/claude-code/1.5.2",
"instance_id": "550e8400-e29b-41d4-a716-446655440000",
"attestation": "eyJhbGciOiJFUzI1NiIs..."
},
"action": {
"type": "exec",
"template": "curl -H 'Authorization: Bearer {{nl:api/GITHUB_TOKEN}}' https://api.github.com/user",
"context": {
"project": "braincol",
"environment": "development"
},
"purpose": "Verify GitHub API access for CI setup",
"timeout_ms": 30000,
"dry_run": false
}
}
6.2 Field Definitions
6.2.1 Top-Level Fields
| Field | Type | Required | Description |
|---|---|---|---|
nl_version |
string | MUST | Protocol version. MUST be "1.0". |
request_id |
string | MUST | Unique identifier for this request. UUID v4 RECOMMENDED. Used for idempotency and audit correlation. |
agent |
object | MUST | Agent identity information. See below. |
action |
object | MUST | The action to perform. See below. |
6.2.2 Agent Object
| Field | Type | Required | Description |
|---|---|---|---|
agent_uri |
string | MUST | The agent's URI (Level 1). |
instance_id |
string | MUST | The agent's instance ID (Level 1). |
attestation |
string | SHOULD (Basic), MUST (Standard+) | The attestation JWT (Level 1). |
6.2.3 Action Object
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | MUST | One of: "exec", "template", "inject_stdin", "inject_tempfile", "sdk_proxy", "delegate". |
template |
string | Conditional | The command or content template with {{nl:...}} placeholders. REQUIRED for exec. |
template_content |
string | Conditional | Inline template content. REQUIRED for template if template_path is not provided. |
template_path |
string | Conditional | Path to a template file. Alternative to template_content for the template action type. |
output_path |
string | Conditional | Output file path for template actions. |
command |
string | Conditional | Command to execute. REQUIRED for inject_stdin and inject_tempfile. |
secret_ref |
string | Conditional | Single secret reference. REQUIRED for inject_stdin. |
file_refs |
object | Conditional | Map of placeholder name to secret reference. REQUIRED for inject_tempfile. |
provider |
string | Conditional | Cloud/API provider identifier. REQUIRED for sdk_proxy. |
service |
string | Conditional | Provider service name. REQUIRED for sdk_proxy. |
operation |
string | Conditional | Provider operation name. REQUIRED for sdk_proxy. |
parameters |
object | Conditional | Operation parameters. For sdk_proxy. |
credentials_ref |
string | Conditional | Credentials reference. REQUIRED for sdk_proxy. |
delegate_to |
string | Conditional | Target agent URI. REQUIRED for delegate. |
delegated_action |
object | Conditional | The action to delegate. REQUIRED for delegate. |
delegation_scope |
object | Conditional | Scope for the delegation. REQUIRED for delegate. |
context |
object | SHOULD | Contextual information for resolution. See below. |
purpose |
string | SHOULD | Human-readable description of why this action is needed. Recorded in audit trail. |
timeout_ms |
integer | SHOULD | Maximum execution time in milliseconds. Default: 30000 (30 seconds). Maximum: 600000 (10 minutes). |
dry_run |
boolean | MAY | If true, validate permissions and resolve references without executing. Default: false. |
6.2.4 Context Object
| Field | Type | Description |
|---|---|---|
project |
string | The project context for reference resolution. If provided, simple references are resolved within this project first. |
environment |
string | The environment context. Combined with project for scoped resolution. |
7. Action Response Format
7.1 Schema
Every action response MUST conform to the following JSON structure:
{
"$schema": "https://nlprotocol.org/schemas/v1.0/action-response.json",
"nl_version": "1.0",
"request_id": "req_550e8400-e29b-41d4-a716-446655440000",
"action_id": "act_660f9511-f30c-52e5-b827-557766551111",
"status": "success",
"result": {
"stdout": "{\"login\":\"acme-bot\",\"id\":12345}",
"stderr": "",
"exit_code": 0
},
"secrets_used": [
"api/GITHUB_TOKEN"
],
"redacted": false,
"redacted_count": 0,
"audit_ref": "aud_770a0622-a41d-63f6-c938-668877662222",
"timing": {
"received_at": "2026-02-08T14:30:00.000Z",
"resolved_at": "2026-02-08T14:30:00.050Z",
"executed_at": "2026-02-08T14:30:00.055Z",
"completed_at": "2026-02-08T14:30:00.250Z",
"total_ms": 250
}
}
7.2 Field Definitions
| Field | Type | Required | Description |
|---|---|---|---|
nl_version |
string | MUST | Protocol version. |
request_id |
string | MUST | Echo of the request ID for correlation. |
action_id |
string | MUST | Unique identifier for this action execution. Generated by the system. |
status |
string | MUST | One of: "success", "denied", "error", "timeout", "dry_run_ok". |
result |
object | Conditional | Execution result. Present when status is "success". |
result.stdout |
string | Conditional | Standard output from the executed command. |
result.stderr |
string | Conditional | Standard error from the executed command. |
result.exit_code |
integer | Conditional | Process exit code. 0 typically indicates success. |
result.data |
object | Conditional | Structured result data (for sdk_proxy actions). |
result.output_path |
string | Conditional | Output file path (for template actions). |
result.resolved_count |
integer | Conditional | Number of placeholders resolved (for template actions). |
secrets_used |
string[] | MUST | List of secret reference names that were resolved. MUST contain names only, NEVER values. |
redacted |
boolean | MUST | true if the output was modified by the sanitizer to remove leaked secrets. |
redacted_count |
integer | SHOULD | Number of redactions performed. |
audit_ref |
string | MUST | Reference to the audit trail entry for this action. |
timing |
object | SHOULD | Timing information for observability. |
error |
object | Conditional | Error details. Present when status is "denied" or "error". |
7.3 Status Codes
| Status | Meaning | Result Present? |
|---|---|---|
success |
Action executed successfully. | YES |
denied |
Agent lacks permission for the requested action or secrets. | NO (error present) |
error |
Action execution failed (e.g., command returned non-zero, secret not found). | Partial (may include stderr) |
timeout |
Action exceeded the timeout limit. | Partial (may include partial output) |
dry_run_ok |
Dry run: permissions validated, references resolved, but action not executed. | NO |
7.4 Error Response
When status is "denied" or "error":
{
"status": "denied",
"error": {
"code": "SCOPE_VIOLATION",
"message": "Agent does not have permission to access secret 'production/DB_PASSWORD'",
"details": {
"secret_ref": "production/DB_PASSWORD",
"agent_scope": {
"environments": ["development", "staging"]
},
"required_scope": {
"environments": ["production"]
}
},
"suggestion": "Request a Scope Grant for the 'production' environment from your administrator."
},
"secrets_used": [],
"redacted": false,
"audit_ref": "aud_770a0622-..."
}
Error responses MUST NEVER include secret values. Even in error scenarios, the NL Protocol's zero-exposure guarantee applies.
7.5 Critical Invariant
The following invariant MUST hold for every action response:
The
resultobject (includingstdout,stderr, anddata) MUST NOT contain any secret value that was resolved during action execution. If a secret value is detected in the output, it MUST be redacted before the response is returned to the agent, andredactedMUST be set totrue.
This invariant is enforced by output sanitization (Section 9).
8. Scope Grants
8.1 Overview
Scope Grants are permission objects that bind an agent identity to a set of allowed actions, secret patterns, and conditions. They are the authorization mechanism of the NL Protocol.
A Scope Grant answers the question: "Is this agent allowed to perform this action type on these secrets, right now, under these conditions?"
8.2 Schema
{
"$schema": "https://nlprotocol.org/schemas/v1.0/scope-grant.json",
"grant_id": "grant_550e8400-e29b-41d4-a716-446655440000",
"nl_version": "1.0",
"agent_uri": "nl://anthropic.com/claude-code/1.5.2",
"instance_id": "550e8400-e29b-41d4-a716-446655440000",
"organization_id": "org_acme_corp_2024",
"granted_by": {
"type": "human",
"identifier": "andres@acme.corp",
"granted_at": "2026-02-08T10:00:00Z"
},
"permissions": [
{
"action_types": ["exec", "inject_stdin"],
"secrets": ["api/*", "database/DB_PASSWORD"],
"conditions": {
"valid_from": "2026-02-08T10:00:00Z",
"valid_until": "2026-02-08T18:00:00Z",
"max_uses": 100,
"require_human_approval": false,
"min_trust_level": "L1",
"allowed_environments": ["development", "staging"],
"allowed_contexts": {
"repository": "github.com/acme-corp/backend"
}
}
},
{
"action_types": ["exec"],
"secrets": ["production/*"],
"conditions": {
"valid_from": "2026-02-08T10:00:00Z",
"valid_until": "2026-02-08T18:00:00Z",
"max_uses": 5,
"require_human_approval": true,
"min_trust_level": "L2",
"allowed_environments": ["production"]
}
}
],
"revocable": true,
"revoked": false
}
8.3 Field Definitions
8.3.1 Top-Level Fields
| Field | Type | Required | Description |
|---|---|---|---|
grant_id |
string | MUST | Unique identifier for this grant. |
nl_version |
string | MUST | Protocol version. |
agent_uri |
string | MUST | The Agent URI this grant applies to. |
instance_id |
string | SHOULD | Specific instance. If omitted, the grant applies to all instances of the agent. |
organization_id |
string | MUST | The organization that issued the grant. |
granted_by |
object | MUST | Who created the grant. |
permissions |
array | MUST | List of permission objects. |
revocable |
boolean | MUST | Whether the grant can be revoked. Default: true. |
revoked |
boolean | MUST | Whether the grant has been revoked. Default: false. |
8.3.2 Permission Object
| Field | Type | Required | Description |
|---|---|---|---|
action_types |
string[] | MUST | Which action types are permitted: "exec", "template", "inject_stdin", "inject_tempfile", "sdk_proxy", "delegate", or "*" for all. |
secrets |
string[] | MUST | Glob patterns for allowed secret references. "*" matches all secrets (use with extreme caution). "api/*" matches all secrets in the api category. |
conditions |
object | MUST | Conditions that must be met for the permission to be active. |
8.3.3 Conditions Object
| Field | Type | Required | Description |
|---|---|---|---|
valid_from |
string (ISO 8601) | MUST | Start of validity window. The permission is not active before this time. |
valid_until |
string (ISO 8601) | MUST | End of validity window. The permission expires after this time. |
max_uses |
integer | SHOULD | Maximum number of times this permission can be used. After reaching the limit, the permission is exhausted. 0 = unlimited. |
require_human_approval |
boolean | SHOULD | If true, each action under this permission requires explicit human approval before execution. Default: false. |
min_trust_level |
string | MAY | Minimum trust level required. One of: "L0", "L1", "L2", "L3". |
allowed_environments |
string[] | MAY | Restrict to specific environments. |
allowed_contexts |
object | MAY | Additional context-based restrictions (e.g., restrict to a specific repository or IDE). |
allowed_ip_ranges |
string[] | MAY | CIDR ranges from which the action is permitted. |
max_concurrent |
integer | MAY | Maximum concurrent actions under this permission. |
8.4 Scope Evaluation Algorithm
When an action request is received, the NL-compliant system MUST evaluate Scope Grants using the following algorithm:
function evaluateScope(agent, action):
grants = findActiveGrants(agent.agent_uri, agent.instance_id)
for each placeholder in action.template:
secretRef = extractSecretRef(placeholder)
permitted = false
for each grant in grants:
if grant.revoked:
continue
for each permission in grant.permissions:
if NOT matchesActionType(permission.action_types, action.type):
continue
if NOT matchesSecretPattern(permission.secrets, secretRef):
continue
if NOT evaluateConditions(permission.conditions, agent, action):
continue
permitted = true
break
if permitted:
break
if NOT permitted:
return DENIED(secretRef, "No matching Scope Grant")
return ALLOWED
8.5 Grant Lifecycle
Creation: A Scope Grant is created by a human administrator or by the system (for delegation tokens). Creation MUST be recorded in the audit trail.
Active: The grant is active when
valid_from <= now() <= valid_untilandrevoked == falseanduses < max_uses.Expired: The grant has passed its
valid_untiltimestamp. Expired grants MUST NOT authorize any actions.Exhausted: The grant has reached its
max_useslimit. Exhausted grants MUST NOT authorize any actions.Revoked: An administrator has explicitly revoked the grant. Revocation MUST be recorded in the audit trail. Revocation is immediate: in-flight actions MAY complete, but new actions MUST be denied.
8.6 Example: Time-Bounded, Conditional Grant
This grant allows Claude Code to access development API keys for the current workday, with a limit of 100 uses:
{
"grant_id": "grant_daily_dev_2026-02-08",
"agent_uri": "nl://anthropic.com/claude-code/1.5.2",
"organization_id": "org_acme_corp_2024",
"granted_by": {
"type": "human",
"identifier": "andres@acme.corp",
"granted_at": "2026-02-08T09:00:00Z"
},
"permissions": [{
"action_types": ["exec", "template", "inject_stdin", "inject_tempfile"],
"secrets": ["api/*", "database/DB_*"],
"conditions": {
"valid_from": "2026-02-08T09:00:00Z",
"valid_until": "2026-02-08T18:00:00Z",
"max_uses": 100,
"require_human_approval": false,
"min_trust_level": "L1",
"allowed_environments": ["development", "staging"],
"allowed_contexts": {
"repository": "github.com/acme-corp/backend"
}
}
}],
"revocable": true,
"revoked": false
}
8.7 Secret Rotation and Revocation Propagation
When a secret is rotated (value changed) or permanently revoked, the NL-compliant system MUST handle in-flight and future actions consistently.
8.7.1 Requirements
| ID | Requirement | Priority | Description |
|---|---|---|---|
| NL-2.15 | Rotation Event | MUST | When a secret is rotated, the system MUST record a secret_rotated event in the audit trail with the secret name (NEVER the old or new value), the timestamp, and the identity of the principal who initiated the rotation. |
| NL-2.16 | In-Flight Actions | SHOULD | Actions that are currently executing (inside the isolation boundary) when a rotation occurs SHOULD be allowed to complete. The old value was already injected and is confined to the subprocess. |
| NL-2.17 | Pending Actions | MUST | Actions that have been submitted but not yet started executing MUST resolve the secret to the NEW value. There MUST NOT be a window where a pending action resolves to a stale value after rotation is committed. |
| NL-2.18 | Delegation Token Invalidation | SHOULD | When a secret is rotated, Delegation Tokens (Level 7) that reference the rotated secret by name SHOULD remain valid — the delegate will resolve the new value on next use. Implementations SHOULD NOT invalidate delegation tokens on secret rotation unless the rotation is due to a confirmed compromise. |
| NL-2.19 | Compromise-Driven Revocation | MUST | When a secret is revoked due to a confirmed compromise (as opposed to routine rotation), the system MUST reject all pending and future actions referencing that secret until a new value is provisioned. In-flight actions SHOULD be terminated if the execution environment supports it. |
| NL-2.20 | Agent Notification | MAY | Implementations MAY notify agents when a secret they have used recently has been rotated. The notification MUST contain only the secret name and the event type, NEVER the old or new value. |
8.7.2 Rotation Flow
Principal NL-Compliant System Agents
| | |
| 1. Rotate Secret(name) | |
| ---------------------------> | |
| | |
| 2. Store new value |
| 3. Mark old value as superseded |
| 4. Write audit: secret_rotated |
| | |
| 5. In-flight actions: CONTINUE |
| (old value in isolation boundary) |
| 6. Pending actions: resolve NEW value |
| | |
| | 7. [OPTIONAL] Notification |
| | {name, event: "rotated"} |
| | --------------------------> |
| | |
9. Output Sanitization
9.1 Purpose
Even with process isolation (Level 3), a command's stdout or stderr might accidentally contain a secret value. For example:
- An error message that includes the connection string with the password.
- A debug log that prints environment variables.
- A curl verbose output that shows authentication headers.
Output sanitization is the last line of defense: it scans the action's output for any secret values that were used in the action and redacts them before the output reaches the agent.
9.2 Requirements
| ID | Requirement | Priority |
|---|---|---|
| NL-2.6.1 | The system MUST scan stdout and stderr for every secret value used in the action. | MUST |
| NL-2.6.2 | If a secret value is found in the output, it MUST be replaced with [REDACTED:secret_name]. |
MUST |
| NL-2.6.3 | The system MUST also check for common encodings of the secret: Base64, URL-encoded, hex-encoded. | MUST |
| NL-2.6.4 | If an encoded form is found, it MUST be replaced with [REDACTED:secret_name:encoding]. |
MUST |
| NL-2.6.5 | Secrets shorter than 4 characters SHOULD be excluded from output scanning (high false-positive rate). | SHOULD |
| NL-2.6.6 | The scanning SHOULD be performed in constant time relative to secret length to avoid timing side channels. | SHOULD |
| NL-2.6.7 | A redaction event MUST be recorded in the audit trail as a security incident. | MUST |
| NL-2.6.8 | The response MUST set redacted: true and redacted_count to the number of redactions. |
MUST |
9.3 Scanning Algorithm
function sanitizeOutput(output, secretsUsed):
redacted = false
redactedCount = 0
for each (name, value) in secretsUsed:
if length(value) < 4:
continue
# Check plaintext
if output.contains(value):
output = output.replace(value, "[REDACTED:" + name + "]")
redacted = true
redactedCount += 1
# Check Base64
b64Value = base64Encode(value)
if output.contains(b64Value):
output = output.replace(b64Value, "[REDACTED:" + name + ":base64]")
redacted = true
redactedCount += 1
# Check URL encoding
urlValue = urlEncode(value)
if output.contains(urlValue):
output = output.replace(urlValue, "[REDACTED:" + name + ":url]")
redacted = true
redactedCount += 1
# Check hex encoding
hexValue = hexEncode(value)
if output.contains(hexValue):
output = output.replace(hexValue, "[REDACTED:" + name + ":hex]")
redacted = true
redactedCount += 1
return (output, redacted, redactedCount)
9.4 Example
Command: curl -v -H "Authorization: Bearer {{nl:api/TOKEN}}" https://api.example.com
Verbose curl output (before sanitization):
> GET / HTTP/2
> Host: api.example.com
> Authorization: Bearer sk-1234567890abcdef
> User-Agent: curl/8.0
< HTTP/2 200
{"status":"ok"}
After sanitization:
> GET / HTTP/2
> Host: api.example.com
> Authorization: Bearer [REDACTED:api/TOKEN]
> User-Agent: curl/8.0
< HTTP/2 200
{"status":"ok"}
The agent receives the sanitized output. The redaction is logged as a security event.
10. Result-Only Propagation
10.1 Principle
In multi-agent chains (orchestrator -> sub-agent -> sub-sub-agent), only results flow between agents, never secrets.
Orchestrator Sub-Agent A Sub-Agent B
| | |
| Delegate(action) | |
| ------------------> | |
| | |
| | Delegate(action) |
| | ------------------> |
| | |
| | | Execute in
| | | isolation
| | | (secrets here)
| | |
| | Result (no secrets) |
| | <------------------ |
| | |
| Result (no secrets) | |
| <------------------ | |
| | |
Secrets used by Sub-Agent B are INVISIBLE to Sub-Agent A
and the Orchestrator. Only the action result propagates.
10.2 Requirements
When an agent receives a result from a delegated action, the result MUST have already been sanitized (Section 9).
An agent MUST NOT forward secret references (
{{nl:...}}) from one action request to another agent's context. Secret references are resolved by the NL-compliant system, not by agents.In a delegation chain, each agent's secrets are isolated from every other agent. The delegation token grants permission to use secrets, not visibility of secret values.
Audit trails (Level 5) MUST record the full delegation chain for forensic purposes, but MUST NOT record secret values at any link in the chain.
11. Dry Run Mode
11.1 Purpose
Agents SHOULD be able to request a "dry run" that validates permissions and resolves references without actually executing the action. This allows agents to check whether they have the necessary permissions before attempting an action.
11.2 Behavior
When dry_run: true:
- The system validates the agent's identity (Level 1).
- The system evaluates all placeholders against Scope Grants.
- The system verifies that all referenced secrets exist.
- The system does NOT resolve secrets to their values.
- The system does NOT execute any command.
- The system returns
status: "dry_run_ok"if all validations pass.
11.3 Example
Request:
{
"action": {
"type": "exec",
"template": "curl -H 'Auth: {{nl:production/API_KEY}}' https://api.example.com",
"dry_run": true
}
}
Response (permissions insufficient):
{
"status": "denied",
"error": {
"code": "SCOPE_VIOLATION",
"message": "No active Scope Grant covers secret 'production/API_KEY' for action type 'exec'",
"suggestion": "Request a Scope Grant for 'production/*' from your administrator."
}
}
Response (permissions sufficient):
{
"status": "dry_run_ok",
"secrets_validated": ["production/API_KEY"],
"grant_refs": ["grant_daily_prod_2026-02-08"],
"audit_ref": "aud_dry_run_..."
}
12. Security Considerations
12.1 Placeholder Injection
If an agent can control the command template, it could attempt to construct a placeholder that references a secret it should not access. This is mitigated by Scope Grant validation (Section 8): every placeholder is validated against the agent's active grants before resolution.
12.2 Template Injection
An agent could attempt to inject malicious content into a template that,
when resolved, causes the execution environment to leak secrets. For
example: echo {{nl:SECRET}} > /tmp/leaked. This is mitigated by:
- Output sanitization (Section 9): the output is scanned.
- Pre-execution defense (Level 4): dangerous patterns are blocked.
- Process isolation (Level 3): the subprocess cannot communicate with the agent directly.
12.3 Timing Side Channels
The time taken to resolve a secret and execute an action could reveal information about the secret (e.g., its length). Implementations SHOULD normalize response times where practical. Output sanitization SHOULD use constant-time comparison where possible.
12.4 Denial of Service
An agent could submit a large number of action requests to exhaust system resources. Implementations SHOULD enforce rate limits per agent and per Scope Grant.
12.5 Cross-Provider Trust
When using cross-provider references ({{nl:aws-sm://...}}), the
NL-compliant system becomes a bridge to an external provider. The
security of the resolved secret depends on the external provider's
security posture. Implementations SHOULD document which external
providers are trusted and how credentials for those providers are
managed.
13. Conformance Checklist
13.1 Basic Conformance
For Basic conformance, an implementation MUST:
- Support the
{{nl:...}}placeholder syntax (Section 4). - Support all four required action types:
exec,template,inject_stdin,inject_tempfile(Section 5). - Implement the Action Request format (Section 6).
- Implement the Action Response format (Section 7).
- Support Scope Grants with time bounds and usage limits (Section 8).
- Implement output sanitization for plaintext and common encodings (Section 9).
- Never return secret values to the agent in any response.
- Record all actions in the audit trail (Level 5, if implemented).
13.2 Standard Conformance
In addition to Basic, Standard conformance MUST:
- Support all condition types in Scope Grants (Section 8.3.3).
- Implement the Scope Grant lifecycle (Section 8.5).
- Support dry run mode (Section 11).
- Implement all four encoding checks in sanitization (plaintext, base64, URL, hex).
13.3 Advanced Conformance
In addition to Standard, Advanced conformance MUST:
- Support
sdk_proxyaction type (Section 5.6). - Support
delegateaction type (Section 5.7). - Enforce result-only propagation in delegation chains (Section 10).
- Support cross-provider references (Section 4.2).
14. References
- RFC 2119 -- Requirement Levels
- 00-overview.md -- NL Protocol Overview
- 01-agent-identity.md -- Level 1: Agent Identity
- 03-execution-isolation.md -- Level 3: Execution Isolation
Copyright 2026 Braincol. This specification is licensed under CC BY 4.0.