@nxtlinq/authorization-gateway provides a protocol-neutral authorization boundary and a
built-in adapter for the
Agent Client Protocol (ACP). The ACP gateway runs
between an ACP Client/IDE and a downstream ACP Agent, verifies the project's
signed Nxtlinq attestation when a session starts, and applies that signed
authorization ceiling to sensitive ACP requests.
ACP Client / IDE
│ newline-delimited JSON-RPC over stdio
▼
nxtlinq-authorization-gateway
├─ @nxtlinq/attest verification
├─ session → signed policy binding
├─ protocol-neutral authorization core
├─ ACP permission / filesystem / terminal adapters
└─ redacted decision receipts
│
▼
Downstream ACP Agent
The dependency is intentionally one-way:
@nxtlinq/authorization-gateway ──depends on──> @nxtlinq/attest
Attest remains usable by itself and contains no ACP methods, transports, sessions, or Gateway receipts.
The shared authorization decision comes from @nxtlinq/attest; this package is
an ACP adapter around it. An MCP gateway or non-protocol runtime can translate
its own operation into the same canonical action without depending on this
Gateway.
- Role Guide — what Host integrators, deployment operators, project authorization owners, Agent providers, end users, and Nxtlinq each need to do.
- ACP Host Integration Guide — production wrapper contract for Buzz and other ACP Hosts, including configuration, decision semantics, responsibility boundaries, and conformance criteria.
- Buzz Integration Examples — deterministic
protocol fixture plus a clean
attest initto integrated-Buzz walkthrough.
Start with the Role Guide to identify your responsibilities. Partners implementing a production ACP integration should then read the Host Integration Guide. The Buzz guide is a test harness, not a deployment contract.
All integrations target the versioned nxtlinq.authorization/v1 contract,
not Buzz or ACP internals:
{
"version": "nxtlinq.authorization/v1",
"context": { "sessionId": "host-session-123" },
"action": {
"type": "filesystem:read",
"resource": "/absolute/project/README.md"
}
}The package exports the recommended AuthorizationGateway facade plus the
lower-level AuthorizationAdapter, AuthorizationBoundary, and
createAuthorizationRequestV1. A Host adapter translates its native request,
while the boundary alone owns session freshness, the signed-policy decision,
and the receipt. Putting the real handler inside
AuthorizationBoundary.authorizeAndExecute() guarantees that a denied request
does not invoke it.
Existing ACP Agents can be wrapped without becoming Nxtlinq-specific:
nxtlinq-authorization-gateway \
--adapter acp \
--project /absolute/path/to/project \
--trust-store /operator/nxtlinq/trust.json \
--receipt-dir /operator/nxtlinq/receipts \
-- existing-acp-agent <args>Hosts can use the exported facade for in-process integration, or wrap an ACP runtime with the CLI without importing the Node.js API.
- ACP v1 newline-delimited JSON-RPC over stdio.
- Bidirectional request, response, notification, and request-ID correlation.
session/new,session/load,session/resume, andsession/forkattestation binding.session/closepolicy cleanup.- External trusted-signer store, revocation state, expiry, audience, signature,
and baseline artifact verification through
@nxtlinq/attest. fs/read_text_file→filesystem:read.fs/write_text_file→filesystem:write.terminal/create→terminal:execute.session/request_permissionreads a canonical action from the standard ACP extension fieldtoolCall._meta.nxtlinq.action.- A signed-policy deny dynamically selects the request's
reject_onceoption and never forwards it to the Client. An allow is forwarded so the Client or user may still narrow the decision. - Relative glob
include/excludepath constraints. - Exact shell-free command plus argument allowlists.
- Explicit terminal environment-variable name allowlists; values are never included in receipts.
- MCP server-name filtering during session setup.
- Observe and enforce modes.
- Structured JSON-RPC denial responses.
- Redacted decision receipts with optional independent Ed25519 signatures and hash chaining.
- Per-message size limit, stream backpressure, minimal child environment, and signal forwarding.
ACP v1 defines filesystem paths and terminal cwd as absolute paths. The
Proxy canonicalizes these paths and rejects root escapes. See the
official ACP v1 schema.
The project manifest is created and signed by @nxtlinq/attest. It must include
the Gateway audience and can include structured capabilities:
{
"name": "my-agent",
"version": "1.0.0",
"signerKeyId": "project-owner-2026",
"scope": ["demo:structured-capabilities"],
"aud": ["nxtlinq-authorization-gateway"],
"capabilities": [
{
"type": "filesystem:read",
"include": ["src/**", "package.json"],
"exclude": [".env", "nxtlinq/**"]
},
{
"type": "filesystem:write",
"include": ["src/**"],
"exclude": ["src/protected/**"]
},
{
"type": "terminal:execute",
"commands": ["npm test", "npm run build"],
"environment": ["CI"]
},
{
"type": "mcp:connect",
"servers": ["github"]
}
]
}Attest 3.x still requires a non-empty legacy scope. The example marker above
is not exercised by the Gateway; its authorization decisions come from the
structured capabilities. Do not mistake a legacy scope entry for a complete
tool policy.
Paths are relative to the verified project root even though ACP sends absolute
paths on the wire. approvalRequired: true fails closed until an owner
approval channel is implemented. Local or session configuration may narrow
this signed ceiling, but cannot expand it.
Sign the edited manifest using the normal local workflow or an external authority:
nxtlinq-attest sign
# Or keep the private key outside the project:
nxtlinq-attest sign --private-key /secure/project-owner.pemThe trust store must be outside the attested repository and must not be group/world writable:
{
"trustedSigners": [
{
"keyId": "project-owner-2026",
"publicKeyPath": "./keys/project-owner.pem"
}
]
}Set "revoked": true to prevent new sessions from using that signer. The
signed manifest's signerKeyId selects exactly one trust-store entry, and the
Gateway verifies with that external public key. Project-local public.key is
used only by standalone nxtlinq-attest workflows and is not a Gateway trust
input. The Proxy snapshots the expanded trust state and the protected attestation files;
changes during an active session cause subsequent sensitive requests to fail
closed.
Validate the project, external trust store, signature, and receipt location without starting the downstream Agent:
nxtlinq-authorization-gateway --check \
--project /absolute/path/to/project \
--trust-store /secure/nxtlinq/trust.json \
--receipt-dir /secure/nxtlinq/receiptsThe command prints a JSON verification report and does not create the receipt directory. It exits non-zero when the attestation or operator-owned trust binding is invalid.
nxtlinq-authorization-gateway \
--project /absolute/path/to/project \
--trust-store /secure/nxtlinq/trust.json \
--audience nxtlinq-authorization-gateway \
--mode acp-enforce \
--receipt-dir /secure/nxtlinq/receipts \
-- downstream-acp-agent --agent-option valueThe executable and arguments following -- are passed directly to
child_process.spawn with shell: false.
Run the bundled, deterministic ACP demonstration without an LLM or API key:
yarn demoFor an interactive presentation UI, run:
yarn demo:webThen open http://127.0.0.1:4173. Press Run verified demo to launch the real child-process flow and visualize trust binding, Client-visible allows, Gateway-local denials, and redacted receipts. The server binds only to loopback and uses no external frontend dependencies.
The Policy Playground also accepts user-selected actions instead of a fixed script. Choose filesystem read, filesystem write, or terminal execution, enter a path or command, and evaluate it through a newly launched programmable ACP Agent and the real Gateway process. The demonstration policy allows:
- Reads from
README.mdandsrc/**, excludingsrc/secret/**. - Writes under
sandbox/**. - Exactly
node --versionfor terminal execution.
Each evaluation creates an isolated temporary project and trust store. Even after a Proxy allow, the Demo Client independently confines file access to that temporary project and executes only the known-safe Node version command. User input is never passed to a shell.
The signed Demo policy uses the portable logical command node --version, not
a user-specific absolute executable path. After the Gateway allows it, the
trusted Demo Client resolves node to its own running process.execPath and
executes that pinned binary. Production Clients should likewise use an
administrator-controlled command resolver or executable identity mapping;
resolving a signed logical name through an attacker-controlled PATH would
weaken this guarantee.
The Demo Client actually executes only the known-safe node --version. If a
user edits and signs the Demo manifest to allow another terminal command such
as npm publish, the request is allowed through the Gateway and reaches the
Client, but the Client returns an allowed · simulated result instead of
executing it. This demonstrates policy changes without publishing packages or
running arbitrary local commands.
The manifest editor is the source of authorization for subsequent actions. Changes remain an unsigned draft until Sign & apply manifest is pressed. The local Demo server validates the editable fields, signs them with an ephemeral Demo-only Ed25519 authority, and displays the active revision, fingerprint, content hash, and signature evidence. Hashes, artifact fields, and the public key are server-managed so the editor cannot forge them.
For example, read .env is denied by default. Add ".env" to the
filesystem:read.include array, sign and apply the new revision, and the same
action becomes allowed. Changing aud away from nxtlinq-authorization-gateway causes
session creation to fail before the Agent action is evaluated. This ephemeral
authority is for demonstration only; production policy keys remain external
to the Gateway and user workspace.
If port 4173 is already occupied, select another loopback port:
NXTLINQ_DEMO_PORT=4174 yarn demo:webBuzz can act as the real ACP Client in front of this Gateway. A bundled safe launcher creates a temporary signed policy and starts a deterministic Reference Agent behind the real Gateway. Configure Buzz's Agent/runtime custom command as:
/absolute/path/to/authorization-gateway/bin/nxtlinq-buzz-demo-agent.mjs
Keep Buzz's ACP harness command as buzz-acp. Then ask the Agent to read
.env, read README.md, run node --version, or run npm publish. See the
Buzz Reference Demo Guide for setup instructions,
expected results, the canonical permission contract, and production boundaries.
README and Node version are actually executed by explicit safe handlers after
allow; .env and npm publish demonstrate zero-execution denials.
For the real integrated Buzz Agent flow, including signer creation with
nxtlinq-attest init, external trust enrollment, Settings configuration,
Recheck/Enable, and receipt verification, follow the
user-project walkthrough.
The Demo Client creates a temporary attested project and external trust store,
starts the real Gateway CLI, and launches examples/demo-agent.mjs as the
downstream ACP Agent. The Agent requests four actions:
| Action | Expected result |
|---|---|
Read src/main.ts |
Allowed and delivered to the Client |
Read .env |
Blocked inside the Gateway |
Run node --version |
Allowed and delivered to the Client |
Run npm publish |
Blocked inside the Gateway |
The output explicitly confirms that denied requests never reached the Client and prints the four redacted decision receipts. This proves the controlled ACP message path and policy behavior. It does not replace interoperability testing with an LLM-backed Agent and a real IDE, and it does not prove OS-level containment for an Agent that bypasses ACP.
Available options:
| Option | Purpose |
|---|---|
--mode observe|acp-enforce |
Record only, or enforce signed policy. |
--max-message-bytes <n> |
Limit each JSON-RPC line; default 1 MiB. |
--pass-env <name> |
Explicitly pass a named environment variable to the Agent. Repeatable. |
--forward-agent-stderr |
Forward downstream stderr. Disabled by default because Agent logs may contain secrets. |
--receipt-private-key <path> |
Sign receipts with an independent Ed25519 key. |
--receipt-key-id <id> |
Operational identity for the receipt key. |
--require-signed-receipts |
Refuse startup without both receipt-key options. |
Only a small baseline environment (PATH, HOME, locale, shell, terminal, and
temporary-directory variables) reaches the Agent by default. API tokens must
be opted in individually with --pass-env; their values are not logged.
A blocked Agent request is not sent to the ACP Client. The Gateway responds to the Agent directly:
{
"jsonrpc": "2.0",
"id": 17,
"error": {
"code": -32041,
"message": "Blocked by Nxtlinq policy",
"data": {
"reason": "outside_scope",
"capability": "filesystem:read",
"receiptId": "..."
}
}
}Receipts store hashes of sensitive action inputs, not paths, command arguments, file contents, environment values, tokens, or private keys. The receipt directory must not be accessible by group or other users.
Policy signing keys and receipt signing keys have different purposes:
- The policy signer authorizes the maximum action scope.
- The receipt signer proves what decision a particular Gateway instance made.
- A receipt signature can never add or modify project authority.
Verify a signed receipt through the public API:
import { verifyDecisionReceipt } from '@nxtlinq/authorization-gateway';
const valid = verifyDecisionReceipt(receipt, receiptSignerPublicKeyPem);acp-enforce controls operations that actually pass through this stdio ACP
connection. It is not an operating-system sandbox.
- An Agent that directly accesses the filesystem, starts a process, opens a network connection, or connects directly to MCP can bypass this Gateway.
- Unknown ACP extension methods are transparently forwarded for compatibility; they are not treated as enforcement points.
terminal/createis checked before execution. Follow-up terminal output, wait, kill, and release calls do not create new execution authority.- Additional workspace directories currently fail closed because each root needs its own attestation policy.
- MCP session configuration is filtered by server name. Executable/URL/config digest pinning and per-tool MCP bridging are future hardening work.
- Path resolution checks existing files or the real parent of a new file, but a separate Client process can still introduce a symlink TOCTOU race between policy evaluation and filesystem access. A controlled filesystem broker or OS sandbox is required to close that gap.
- Active sessions detect changes to the manifest, signature, and trust state. They do not yet maintain a full digest of every Gateway-approved working-tree transition.
- Remote ACP transports are not implemented; this package currently targets the standard subprocess stdio deployment.
Do not describe this mode as complete filesystem, terminal, network, or MCP
containment. That requires a separate sandbox-enforce boundary.
npm install
npm test
npm pack --dry-runThe tests include a real child-process ACP flow, trusted session binding, filesystem and terminal allow/deny decisions, path escape prevention, tamper detection, request-ID routing, malformed transport input, secret redaction, signed receipt verification, and a regression test for the human-readable demonstration.