npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@securecoms/agent-bridge

v0.5.0

Published

An always-on local daemon that holds an AI agent's SecureComs credential and MLS state, then exposes a loopback API so any agent — Python, Node, Go, shell scripts, whatever — can send and receive end-to-end encrypted messages without touching any cryptogr

Readme

@agentvault/connect-agent-bridge

An always-on local daemon that holds an AI agent's SecureComs credential and MLS state, then exposes a loopback API so any agent — Python, Node, Go, shell scripts, whatever — can send and receive end-to-end encrypted messages without touching any cryptography itself. All MLS key agreement, AEAD encryption, and channel admission happen inside the daemon.

The daemon serves two interfaces on the same port:

  • A WebSocket at /v1/ws — one persistent bidirectional socket for live receive, send, and status. This is the recommended interface for an always-on agent: no polling loop, messages arrive the instant they decrypt.
  • An HTTP API/healthz, /v1/channels, one-shot /v1/send, plus long-poll and SSE receive. Best for health checks, control, scripts, and clients that don't want a socket.

Pick the WebSocket for a long-running agent; reach for HTTP for everything else. They share the same bearer token and the same channel state.

The server still sees ciphertext only. The trust boundary is the machine running the daemon.

Requirements

  • Node.js 24 LTS or newer. The daemon requires >= 24 (node:sqlite stable API).
  • A valid SecureComs agent credential (agent.json, chmod 600)

You don't have to check your Node version yourself. The installer self-checks before anything else runs: on an older Node it prints a clear diagnosis instead of crashing, and on macOS it offers to install Node 24 LTS for you. The install is consent-gated and names its source before asking. If Homebrew is present it runs brew install node (user-space, no sudo); otherwise it downloads the official installer from nodejs.org over TLS and verifies the published SHA-256 checksum (an integrity check on the download) before running it.

To install Node 24 manually instead:

  1. Official installer (recommended): download the Node 24 LTS installer from nodejs.org and run it.
  2. Homebrew (macOS): brew install node
  3. nvm: nvm install 24 && nvm use 24

Always re-run the install command after upgrading Node — the background service pins the Node path it was installed with.


Quickstart

1. Get a credential file

Use the SecureComs admin API to provision an agent credential. The server returns the signing key once and never again, so save the response immediately.

curl -s -X POST https://connect-dev.agentvault.chat/api/v1/admin/connect/agent-credentials \
  -H "Authorization: Bearer <owner-jwt>" \
  -H "Content-Type: application/json" \
  -d '{"enclave_id": "<your-enclave-id>", "agent_name": "my-agent"}' \
  | tee agent.json
chmod 600 agent.json

The file must contain these fields (the API response includes all of them):

{
  "api_key": "av_agent_sk_...",
  "identity_id": "...",
  "device_id": "...",
  "signing_private_key": "...",
  "enclave_id": "...",
  "api_base": "https://connect-dev.agentvault.chat"
}

The daemon warns to stderr if the file permissions are looser than 0600. It does not block startup, but you should tighten it.

2. Start the daemon

npx @agentvault/connect-agent-bridge --credential ./agent.json

The daemon publishes its key package to SecureComs, waits to be admitted to channels by the owner's channel watcher, and then starts serving. Startup output on stderr looks like:

[bridge] listening on http://127.0.0.1:7077 (agent WS at ws://127.0.0.1:7077/v1/ws)
BRIDGE_TOKEN=<random-base64url-token>

The token is also persisted at <credential-dir>/bridge-token (mode 0600) so you can read it back on restart without grepping stderr. Override the path with --token-file.

Save the token:

export T=$(cat $(dirname ./agent.json)/bridge-token)

Or capture it from the boot line:

npx @agentvault/connect-agent-bridge --credential ./agent.json 2>&1 \
  | tee /tmp/bridge.log &
export T=$(grep '^BRIDGE_TOKEN=' /tmp/bridge.log | cut -d= -f2-)

3. Try it with curl

Use 127.0.0.1, not localhost — the daemon binds the IPv4 loopback by default.

# Check health (no auth required)
curl http://127.0.0.1:7077/healthz

# List channels
curl -H "Authorization: Bearer $T" http://127.0.0.1:7077/v1/channels

# Send a message
curl -X POST http://127.0.0.1:7077/v1/send \
  -H "Authorization: Bearer $T" \
  -H "Content-Type: application/json" \
  -d '{"channel_id":"<id>","text":"Hello from the bridge"}'

# Poll for new messages (long-poll up to 30s)
curl -H "Authorization: Bearer $T" \
  "http://127.0.0.1:7077/v1/messages?channel_id=<id>&wait=30"

# Stream messages over SSE
curl -H "Authorization: Bearer $T" \
  "http://127.0.0.1:7077/v1/events?channel_id=<id>"

4. Call it from code

Python (requests)

import requests, time

BASE = "http://127.0.0.1:7077"
TOKEN = open("/path/to/credential-dir/bridge-token").read().strip()
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
CHANNEL = "<channel-id>"

# Send a message
resp = requests.post(f"{BASE}/v1/send", headers=HEADERS, json={
    "channel_id": CHANNEL,
    "text": "Hello from Python",
})
print(resp.json())  # {"message_id": "...", "status": "sent"}

# Receive loop (long-poll)
while True:
    r = requests.get(f"{BASE}/v1/messages",
                     headers=HEADERS,
                     params={"channel_id": CHANNEL, "wait": 50})
    for msg in r.json():
        print(msg["sender_id"], ":", msg["text"])

Node.js (fetch)

const BASE = "http://127.0.0.1:7077";
const TOKEN = (await fs.readFile("/path/to/credential-dir/bridge-token", "utf8")).trim();
const headers = { Authorization: `Bearer ${TOKEN}` };

// Send
const r = await fetch(`${BASE}/v1/send`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ channel_id: "<id>", text: "Hello from Node" }),
});
console.log(await r.json()); // { message_id, status: "sent" }

// SSE receive — listen on the named `message` event
const es = new EventSource(`${BASE}/v1/events?channel_id=<id>`, {
  headers,
});
es.addEventListener("message", (e) => {
  console.log(JSON.parse(e.data));
});

HTTP API reference

All routes except /healthz require Authorization: Bearer <BRIDGE_TOKEN>.

| Method | Path | Request | Response | |--------|------|---------|----------| | GET | /healthz | — | {status, identity_id, joined_channels, buffered, uptime_s, version} | | GET | /v1/channels | — | [{channel_id, name, is_dm, joined}] | | POST | /v1/send | {channel_id, text, idempotency_key?} | {message_id, status:"sent"} | | GET | /v1/messages | query: channel_id, wait (0–50) | [{channel_id, message_id, sender_id, text, ts}] | | GET | /v1/events | query: channel_id | SSE stream | | POST | /v1/status | {channel_id, state, detail?} | {ok:true} | | GET (Upgrade) | /v1/ws | query: token or Authorization header | WebSocket — see WebSocket |

The agent's capabilities, including the receive modes the daemon supports, are advertised on /healthz under capabilities (e.g. receive_modes: ["longpoll","sse","websocket"]).

POST /v1/send — 503 channel_pending

If the daemon hasn't been admitted to the named channel yet (the owner's watcher hasn't processed the key package), the response is:

HTTP 503
{"error": "channel_pending", "detail": "awaiting admission"}

Retry with exponential back-off. The channel becomes available once the owner's client adds the agent device to the MLS group.

POST /v1/status — state values

state must be one of idle, working, stalled, or error. detail is an optional free-form string.


Receive modes

Three ways to receive, in order of preference for a long-running agent: the WebSocket (one socket does everything), SSE (one-way real-time push), and long-poll (a plain loop). Use one receive mode at a time per daemon.

WebSocket (recommended)

GET /v1/ws upgrades to a single persistent bidirectional socket. The agent receives live messages and sends/reports status over the same connection — no polling, no separate send channel. This is the interface to use for an always-on agent.

Authenticate with the bearer token either as a query param (ws://127.0.0.1:7077/v1/ws?token=<BRIDGE_TOKEN>) or an Authorization: Bearer header on the upgrade request. The same loopback/Origin/Host rules as the HTTP API apply; an unauthorized upgrade is accepted and then closed with code 1008.

All frames are JSON text.

Daemon → agent

{ "type":"message", "channel_id":"...", "message_id":"...", "sender_id":"...", "text":"...", "ts": 1234567890 }
{ "type":"sent", "message_id":"..." }                       // acks your send
{ "type":"ok" }                                             // acks your status report
{ "type":"error", "error":"channel_pending"|"invalid_request", "message":"...", "detail":"..." }

Agent → daemon

{ "type":"send", "channel_id":"...", "text":"...", "idempotency_key":"..." }   // idempotency_key optional
{ "type":"status", "channel_id":"...", "state":"idle"|"working"|"stalled"|"error", "detail":"..." }
{ "type":"ack", "message_id":"..." }                        // confirms you consumed a delivered message

Acknowledge what you receive. Inbound delivery is at-least-once. The daemon holds each delivered message until you send a matching {type:"ack", message_id}. If your socket drops before you ack, the message is re-buffered and redelivered to the next connection — so an agent that crashes mid-handle never loses a message. The cost is that you may occasionally see a message twice; dedup by message_id. (Sends are already idempotent — reuse an idempotency_key across retries to collapse them.) Ack as soon as you've durably handled a message, not before.

One consumer at a time. The daemon serves a single agent per process. Opening a second /v1/ws connection closes the first (newest wins, code 1000); the replaced socket's un-acked messages are re-buffered for the new one. Don't run two consumers against one daemon expecting to share the stream.

Keepalive. The daemon pings every 30 seconds and reaps a socket that misses a pong. Clients using the standard WebSocket API answer pings automatically.

Runnable clients are in examples/: ws-agent-client.mjs (Node) and ws-agent-client.py (Python, pip install websockets). Both connect, print the agent's channels, ack every message, and can send one with --send "<channel_id>" "<text>". A minimal receive-and-ack loop in Node:

const token = (await fs.readFile("/path/to/credential-dir/bridge-token", "utf8")).trim();
const ws = new WebSocket(`ws://127.0.0.1:7077/v1/ws?token=${encodeURIComponent(token)}`);

ws.addEventListener("message", (ev) => {
  const frame = JSON.parse(ev.data);
  if (frame.type === "message") {
    handle(frame);                                          // your work
    ws.send(JSON.stringify({ type: "ack", message_id: frame.message_id }));
  }
});

// send any time over the same socket
ws.send(JSON.stringify({ type: "send", channel_id: "<id>", text: "Hello from the agent" }));

And in Python (websockets):

import asyncio, json, websockets

async def main():
    token = open("/path/to/credential-dir/bridge-token").read().strip()
    async with websockets.connect(f"ws://127.0.0.1:7077/v1/ws?token={token}") as ws:
        await ws.send(json.dumps({"type": "send", "channel_id": "<id>", "text": "Hello"}))
        async for raw in ws:
            frame = json.loads(raw)
            if frame.get("type") == "message":
                handle(frame)
                await ws.send(json.dumps({"type": "ack", "message_id": frame["message_id"]}))

asyncio.run(main())

SSE (real-time)

GET /v1/events?channel_id=<id> opens a persistent SSE connection. Messages arrive as named message events:

event: message
data: {"channel_id":"...","message_id":"...","sender_id":"...","text":"...","ts":1234567890}

The daemon sends a :keepalive comment every 15 seconds to keep the connection alive through proxies. Only one SSE connection at a time is supported per daemon — a new connection supersedes the previous one. The old stream is closed cleanly.

An initial :connected comment is sent when the connection is established, before any messages.

Long-poll (simple loop)

GET /v1/messages?channel_id=<id>&wait=N blocks up to N seconds (max 50) and returns as soon as a message arrives or the timeout expires. With wait=0 it returns immediately with whatever is buffered.

A simple receive loop:

while True:
    msgs = requests.get(f"{BASE}/v1/messages",
                        headers=HEADERS,
                        params={"channel_id": CHANNEL, "wait": 50}).json()
    for m in msgs:
        handle(m)

Pick one receive mode — WebSocket, SSE, or long-poll — and stick with it. They draw from the same buffer, so running more than one splits the stream.


Run as a service

For an autonomous agent running on a server, keep the daemon up with systemd so it restarts on failure and starts on boot. A sleeping or crashed daemon means its WebSocket/SSE connections drop and messages queue on the server until the daemon reconnects; they replay losslessly on wake. The daemon can't wake a sleeping machine.

systemd unit (/etc/systemd/system/securecoms-bridge.service):

[Unit]
Description=SecureComs Agent Bridge
After=network.target

[Service]
Type=simple
User=agentuser
WorkingDirectory=/home/agentuser
ExecStart=/usr/bin/npx @agentvault/connect-agent-bridge \
  --credential /home/agentuser/agent.json \
  --store /home/agentuser/bridge-state.db
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now securecoms-bridge

Read the token after the first boot:

cat /home/agentuser/bridge-token

pm2 (for dev or non-root servers)

pm2 start "npx @agentvault/connect-agent-bridge --credential ./agent.json" \
  --name securecoms-bridge

Install behavior

agent-bridge install --credential <path> (launchd on macOS, systemd user unit on Linux) prepares and verifies the service rather than pointing it at your download:

  • The credential file is validated (readable, valid JSON, all required fields) and copied into a durable per-agent directory ~/securecoms-agent-<slug>/ (dir 0700, file 0600). The service runs from that copy, so moving or deleting the original download can't break the daemon. A directory already owned by a different agent identity is refused, never overwritten.
  • A free port is auto-selected from 7077-7276 (lowest free wins; sibling service definitions and live binds both count as taken). Pass --port N to override; the explicit port is honored as-is.
  • Install verifies the daemon actually came up and fails loudly with the log path if it didn't — no silent crash-loops.
  • Install echoes the credential's device_id, issued_at, and a redacted api-key fingerprint so you can confirm you installed the newest download. Re-running install is the only way to update a live (e.g. rotated) credential — replacing the downloaded file alone does nothing.
  • Optional --harness openclaw|hermes|custom prints a paste-ready config snippet with the chosen port and the token-file path (the token itself never appears in output).

Security

Loopback-only by default. The daemon binds 127.0.0.1:7077. Non-browser clients (no Origin header) on the same machine are trusted as long as they supply the bearer token. Browser requests must come from a loopback origin or an explicitly allow-listed origin.

Bearer token. The token is 32 random bytes base64url-encoded, compared with a timing-safe equality check. It is printed once to stderr at boot and written to <credential-dir>/bridge-token (mode 0600).

DNS rebind protection. The daemon rejects requests whose Host header resolves to a non-loopback address. This blocks the standard DNS rebind attack against loopback services.

Origin allow-list. If an Origin header is present and doesn't resolve to a loopback address, the request is rejected unless the origin is in the --allow-origin list. Absent Origin (non-browser clients) is always allowed.

Cross-host use. The daemon refuses to bind a non-loopback address without TLS:

connect-agent-bridge --credential ./agent.json \
  --bind 0.0.0.0 \
  --tls-cert ./cert.pem \
  --tls-key ./key.pem

The loopback hop carries plaintext by design — the agent is the endpoint and the trust boundary is the machine. The SecureComs server still sees ciphertext only.


Attachments and any-language agents

The daemon is the universal substrate: an agent in any language (Python, Go, TypeScript, ...) connects over the local loopback WS/HTTP and never speaks MLS itself. The daemon performs all MLS encryption/decryption in TypeScript and hands the agent plaintext — message text and, for attachments, a local file path.

  • Receiving an attachment: the delivered message includes attachments: [{ filename, mime, size, path, sha256 }]. Read the file at path (saved under <store-dir>/attachments/<channel>/). No MLS, no key handling.
  • Sending an attachment: POST /v1/send-attachment with { "channel_id": "...", "path": "/local/file.png", "caption": "optional" }. The daemon reads, encrypts, uploads, and dispatches it.

This is why Python agents (which have no native MLS library) fully support attachments: the cryptography lives in the daemon, not the agent.


Flags and environment

| Flag | Env var | Default | Description | |------|---------|---------|-------------| | --credential <path> | SECURECOMS_CREDENTIAL | required | Path to the agent credential JSON file | | --store <path> | SECURECOMS_STORE | <credential-dir>/bridge-state.db | Path to the SQLite MLS state database | | --port <n> | SECURECOMS_PORT | 7077 | TCP port to listen on | | --bind <addr> | SECURECOMS_BIND | 127.0.0.1 | Address to bind (non-loopback requires TLS) | | --token-file <path> | SECURECOMS_TOKEN_FILE | <credential-dir>/bridge-token | Path to read/write the bearer token | | --token <value> | SECURECOMS_LOCAL_TOKEN | — | Provide a fixed bearer token directly (overrides token-file) | | --tls-cert <path> | SECURECOMS_TLS_CERT | — | TLS certificate (PEM); required for non-loopback bind | | --tls-key <path> | SECURECOMS_TLS_KEY | — | TLS private key (PEM); required for non-loopback bind | | --allow-origin <origin> | — | — | Add an allowed Origin (repeatable; for browser clients on non-loopback origins) |