Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nxtlinq Authorization Gateway

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

Documentation

  • 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 init to 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.

Stable integration boundary

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.

Implemented enforcement

  • ACP v1 newline-delimited JSON-RPC over stdio.
  • Bidirectional request, response, notification, and request-ID correlation.
  • session/new, session/load, session/resume, and session/fork attestation binding.
  • session/close policy cleanup.
  • External trusted-signer store, revocation state, expiry, audience, signature, and baseline artifact verification through @nxtlinq/attest.
  • fs/read_text_filefilesystem:read.
  • fs/write_text_filefilesystem:write.
  • terminal/createterminal:execute.
  • session/request_permission reads a canonical action from the standard ACP extension field toolCall._meta.nxtlinq.action.
  • A signed-policy deny dynamically selects the request's reject_once option and never forwards it to the Client. An allow is forwarded so the Client or user may still narrow the decision.
  • Relative glob include / exclude path 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.

Signed project policy

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

Trust store

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

Run

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/receipts

The 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 value

The executable and arguments following -- are passed directly to child_process.spawn with shell: false.

Interactive demonstration

Run the bundled, deterministic ACP demonstration without an LLM or API key:

yarn demo

For an interactive presentation UI, run:

yarn demo:web

Then 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.md and src/**, excluding src/secret/**.
  • Writes under sandbox/**.
  • Exactly node --version for 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:web

Buzz Client demonstration

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

Denial response

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": "..."
    }
  }
}

Decision receipts

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

Security boundary and known limitations

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/create is 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.

Development

npm install
npm test
npm pack --dry-run

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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages