Agent identity and per-request Ed25519 signing for zylos agents.
Manages Ed25519 key pairs (keygen, registration, rotation) and signs outgoing
HTTP requests with RFC-9421-shaped signatures that are byte-identical to the Go
agentsig verifier in platform/internal/agentsig.
Zero runtime dependencies, pure ESM, no build step. Install from GitHub, pinned to a release:
npm install github:openmaxai/openmax-agentgate#v0.2.0This provides both the agentgate CLI (verify with npx agentgate --version)
and the library import (import { createSigner } from 'openmax-agentgate/client').
Check for updates any time: npx agentgate upgrade --check. Requires Node.js >= 20.
agentgate --version # installed version
agentgate upgrade --check [--json] # check for upgrades (GitHub Releases)
agentgate keygen --kid <id> [--out <path>]
agentgate register --kid <id> --tenant <t> --agent <a> --cp-url <url>
agentgate keys list --tenant <t> --agent <a> --cp-url <url>
agentgate keys revoke --kid <id> --tenant <t> --agent <a> --cp-url <url>Security: the ops token is read from the AGENTGATE_OPS_TOKEN environment
variable, never from CLI arguments (avoids shell history / ps leaks). Private
keys are never printed, logged, or uploaded.
Exit codes (upgrade --check): 0 = up to date, 10 = upgrade available,
20 = check failed.
Each agent instance is bound to a single (tenant_id, agent_id, kid). The
private key is generated locally, stored with 0600 permissions, and never leaves
the agent runtime.
openmax-agentgate/
├── src/cli.js # CLI entry point (agentgate bin)
├── src/lib/signing.js # Signing base + Ed25519 signing (byte-identical to Go)
├── src/lib/keygen.js # Ed25519 key generation, PEM load/save
├── src/lib/register.js # Control plane key registration API
├── src/lib/client.js # AgentSigner class (high-level signing client)
├── src/lib/version.js # Version + upgrade check (GitHub Releases)
├── src/lib/config.js # Config loader with hot-reload
└── src/index.js # PM2 service entry point
import { createSigner } from 'openmax-agentgate/client';
const signer = createSigner({
private_key_path: '/path/to/key.pem',
tenant_id: 'my-tenant',
agent_id: 'my-agent',
key_id: 'my-agent-2026a',
path_prefix: '/ajj/agent', // prefix the reverse proxy strips
});
const headers = signer.sign('POST', 'https://gateway.example/ajj/agent/api/v1/query', body);
// Signature covers /api/v1/query (internal path), not the full public URL.
// headers: Content-Digest, X-Openmax-Tenant, X-Openmax-Agent,
// Signature-Input, SignatureWhen a reverse proxy (e.g. Caddy) strips a path prefix before forwarding to
the control plane, the signature must cover the internal path (after
stripping), not the public URL. Set path_prefix (config) or pathPrefix
(constructor) to the prefix being stripped (e.g. "/ajj/agent"). The client
signs the stripped path automatically while still sending requests to the full
public URL.
Without this, every request silently gets 401 with no hint — the signed path
does not match what the control plane sees.
npm testRuns 135 tests: golden vectors (signing), keygen, register (mock fetch), and pathPrefix verification.
Node's new URL().pathname and Go's url.URL.EscapedPath() diverge on two
characters: | (→ %7C) and ^ (→ %5E). The encodePath() function
supplements Node's pathname by encoding only those two.