惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
Microsoft Security Blog
Microsoft Security Blog
S
SegmentFault 最新的问题
Forbes - Security
Forbes - Security
爱范儿
爱范儿
Stack Overflow Blog
Stack Overflow Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
CERT Recently Published Vulnerability Notes
S
Schneier on Security
Scott Helme
Scott Helme
C
Check Point Blog
T
Tenable Blog
博客园 - 三生石上(FineUI控件)
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
Lohrmann on Cybersecurity
Google DeepMind News
Google DeepMind News
人人都是产品经理
人人都是产品经理
N
News and Events Feed by Topic
B
Blog
P
Privacy International News Feed
I
Intezer
T
Threatpost
Google DeepMind News
Google DeepMind News
L
LangChain Blog
Last Week in AI
Last Week in AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
L
LINUX DO - 最新话题
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
Cybersecurity and Infrastructure Security Agency CISA
N
News and Events Feed by Topic
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
News | PayPal Newsroom
The Hacker News
The Hacker News
S
Security @ Cisco Blogs
罗磊的独立博客
PCI Perspectives
PCI Perspectives
Y
Y Combinator Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
大猫的无限游戏
大猫的无限游戏
U
Unit 42
Hacker News: Ask HN
Hacker News: Ask HN
D
Docker
AI
AI
小众软件
小众软件
博客园 - 叶小钗
H
Help Net Security
TaoSecurity Blog
TaoSecurity Blog
P
Privacy & Cybersecurity Law Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Building an outbound-only WebSocket bridge for local AI agents
CTRLNODE.AI · 2026-05-23 · via DEV Community

CTRLNODE.AI

Building an outbound-only WebSocket bridge for local AI agents
I work with AI agents every day. Claude Code, Copilot, Gemini CLI — running locally, with access to my filesystem, my repos, my tools. The results are genuinely good. But there's a wall: the moment you leave your desk, you lose control. There's no real way to kick off an agent task from your phone, monitor a long-running pipeline from a coffee shop, or schedule something to run overnight.

Every solution I found had the same trade-off: you either open a port, install a tunnel daemon, or upload your code to someone's cloud. None of those felt right for infrastructure that has access to your local filesystem.

So I built CTRL NODE — a browser-based control plane for local AI agents. The key piece is a process called the Bridge: a lightweight Node.js daemon that runs on your machine and connects to the cloud without ever accepting an inbound connection.

This article is about how that works, why the design choices matter, and what the actual code looks like.


Why outbound-only?

The naive approach is to expose your local agent runtime on a port and let the cloud reach in. Tools like ngrok do exactly this — they create a reverse proxy to your localhost. It works, but it has real costs:

  • Open port = attack surface. Every ngrok tunnel is a publicly reachable endpoint. If auth breaks, someone else can talk to your agent.
  • Third-party traffic relay. Your prompts, file paths, and agent responses travel through ngrok's infrastructure.
  • Daemon complexity. You're running persistent infrastructure that you didn't write and can't audit easily.

The alternative: flip the connection direction. The Bridge connects out to the cloud. The cloud pushes commands down that connection. The local machine never listens on a public port.

Your machine                     ctrlnode.ai cloud
──────────────────────────────────────────────────
Bridge ──── ws:// connect() ────▶ WebSocket server
       ◀─── {action: "run_task", ...} ────────────
       ─────  stdout/stderr events ──────────────▶

Enter fullscreen mode Exit fullscreen mode

This is the same pattern used by IoT devices, CI agents (like the GitHub Actions runner), and remote desktop clients. The cloud doesn't initiate — it waits.


The connection lifecycle

Here's the core of websocket.ts:

export function connect(): void {
  const url = buildWsUrl();
  ws = new WebSocket(url, { headers: buildAuthHeaders() });

  ws.on("open", () => {
    logger.info("Bridge connected to SAAS");
    flushPendingQueue();
    startHeartbeat();
  });

  ws.on("message", (data: WebSocket.RawData) => {
    const message = JSON.parse(data.toString()) as InboundMessage;
    handleInboundMessage(message);
  });

  ws.on("close", (code: number, reason: Buffer) => {
    stopHeartbeat();
    if (isAuthError(code, reason.toString())) {
      logger.warn(`Auth error (${code}), retrying in ${AUTH_RETRY_MS / 1000}s`);
      setTimeout(connect, AUTH_RETRY_MS);
    } else {
      scheduleReconnect();
    }
  });

  ws.on("error", (err: Error) => {
    logger.error(`WebSocket error: ${err.message}`);
  });
}

Enter fullscreen mode Exit fullscreen mode

Three things to notice:

  1. Auth errors get a longer timeout. If the server returns 1008 (Policy Violation) or 1002, or the reason string contains "401"/"403"/"Unauthorized", we wait 30 seconds before retrying. Hammering an auth-rejected endpoint is pointless and noisy.

  2. Normal closes trigger exponential backoff. scheduleReconnect() uses a standard backoff so a transient network blip doesn't flood logs.

  3. On open, we flush the queue. More on this below.


Keeping the connection alive through load balancers

Cloud load balancers will kill idle WebSocket connections after 30–60 seconds. The fix is a heartbeat:

const HEARTBEAT_INTERVAL_MS = 20_000;
let heartbeatTimer: NodeJS.Timeout | null = null;

function startHeartbeat(): void {
  heartbeatTimer = setInterval(() => {
    sendToSaas({ type: "heartbeat", timestamp: Date.now() });
  }, HEARTBEAT_INTERVAL_MS);
}

function stopHeartbeat(): void {
  if (heartbeatTimer) {
    clearInterval(heartbeatTimer);
    heartbeatTimer = null;
  }
}

Enter fullscreen mode Exit fullscreen mode

Every 20 seconds, a small message goes up. The server acknowledges it (or doesn't — we don't care, the goal is just to keep TCP active). This is cheap and it works reliably with AWS ALB, Cloudflare, and most managed WebSocket proxies.


Buffering outbound messages during disconnection

When the Bridge is reconnecting, agent output still arrives. If we drop those events, the user watching a pipeline in their browser sees a gap in the live log. The solution is a small in-memory queue:

const PENDING_QUEUE_MAX = 100;
const pendingQueue: OutboundMessage[] = [];

export function sendToSaas(message: OutboundMessage): void {
  if (!ws || ws.readyState !== WebSocket.OPEN) {
    if (pendingQueue.length < PENDING_QUEUE_MAX) {
      pendingQueue.push(message);
    }
    return;
  }
  ws.send(JSON.stringify(message));
}

function flushPendingQueue(): void {
  while (pendingQueue.length > 0) {
    const msg = pendingQueue.shift()!;
    ws!.send(JSON.stringify(msg));
  }
}

Enter fullscreen mode Exit fullscreen mode

Cap at 100 messages, flush on reconnect. Simple, and it handles the common case of a 2–3 second reconnect window without losing events.


Multi-agent routing via the filesystem

Here's the part that took the most thought: how do you run multiple agents — Claude, Copilot, Gemini — on the same machine, routing tasks to the right one?

The answer isn't a routing layer in the WebSocket code. It's the filesystem.

Each pipeline task gets an isolated directory:

workspace/
  tasks/
    task-abc123/
      input/
        TASK.md          ← instructions for the agent
        context-files/   ← any files the user attached
      output/
        TASK.md          ← agent writes progress here
        artifacts/       ← anything the agent produces

Enter fullscreen mode Exit fullscreen mode

The Bridge watches these directories. When a run_task command arrives:

case "run_task": {
  const { taskId, agentProvider, workspacePath } = message.payload;
  const provider = getProvider(agentProvider); // Claude | Copilot | Gemini | ...
  await provider.executeTask(taskId, workspacePath);
  break;
}

Enter fullscreen mode Exit fullscreen mode

Each provider implementation knows how to invoke its agent CLI with the right arguments and working directory. Claude Code gets claude --print with the task directory. Copilot gets its own invocation. They never share context — each runs in its own subprocess, reading from and writing to its own task folder.

This means:

  • No prompt pollution. Agent A's context doesn't leak into Agent B.
  • Parallel execution. Two agents can run simultaneously without coordination overhead.
  • Auditability. Every task leaves a paper trail on disk.
  • Portability. The cloud control plane never sees your file contents. It only sees task metadata and status events.

Provider selection and gating

Some actions only make sense for certain providers. The message handler maintains an explicit set:

const OPENCLAW_ONLY_ACTIONS = new Set([
  "openclaw_configure",
  "openclaw_stream_chunk",
  "openclaw_reset_context",
]);

function handleInboundMessage(message: InboundMessage): void {
  if (OPENCLAW_ONLY_ACTIONS.has(message.action) && activeProvider !== "openclaw") {
    logger.warn(`Received ${message.action} but provider is ${activeProvider} — ignoring`);
    return;
  }
  // ... dispatch to handler
}

Enter fullscreen mode Exit fullscreen mode

This prevents misconfigured cloud deployments from accidentally sending the wrong command type to the wrong agent. The Bridge is the last line of defense before your filesystem.


The startup sequence

index.ts ties it together:

async function main(): Promise<void> {
  const providers = await createProviders(config);
  const multi = new MultiProvider(providers);

  connect(); // start WebSocket, non-blocking

  const keepaliveInterval = setInterval(() => {}, 1 << 30);
  keepaliveInterval.unref(); // don't prevent process exit

  process.on("SIGINT", gracefulShutdown);
  process.on("SIGTERM", gracefulShutdown);

  await multi.runSyncAgents(); // provider-specific background sync
}

Enter fullscreen mode Exit fullscreen mode

The keepaliveInterval trick (unref()) is worth noting: it keeps the event loop alive when nothing else is pending, but doesn't prevent a clean SIGINT/SIGTERM from shutting the process down. Without it, connect() is async and Node exits immediately after starting.


What this enables

With the Bridge running, the CTRL NODE web app can:

  • Launch tasks against any connected agent from any browser, anywhere
  • Watch live output streamed back over the same WebSocket
  • Schedule routines — the cloud scheduler wakes the Bridge at the configured time, no cron job needed on the local machine
  • Run multi-step pipelines where each node can use a different agent

None of your code leaves your machine. The cloud only sees: "task started", "task output line", "task completed". The actual file contents, prompts, and agent context stay local.


Why open source?

The Bridge is MIT licensed (github.com/ctrlnode-ai/ctrlnode). You can read every line of the WebSocket handler, every message type, every auth check. If you don't trust the binary, build it yourself.

The rest of CTRL NODE — the cloud scheduler, the web app, the real-time pipeline view — runs as a hosted service. The Bridge is the trust boundary: it's the piece that runs with access to your local system, and it needs to be auditable.


Try it

If you work with AI agents and want a way to control them remotely without sacrificing privacy:

  1. Install the Bridge: npm install -g @ctrlnode/bridge && ctrlnode bridge start
  2. Sign up at ctrlnode.ai — it's free
  3. Open the web app from anywhere and connect

Questions, issues, or PRs: github.com/ctrlnode-ai/ctrlnode or reply here.


Javier Vil — Creator of CTRL NODE