<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CTRLNODE.AI</title>
    <description>The latest articles on DEV Community by CTRLNODE.AI (@ctrlnodeai).</description>
    <link>https://dev.to/ctrlnodeai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3946913%2F6220fab0-ea7b-4f94-a371-0c5d87d20ef1.png</url>
      <title>DEV Community: CTRLNODE.AI</title>
      <link>https://dev.to/ctrlnodeai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ctrlnodeai"/>
    <language>en</language>
    <item>
      <title>We Gave Five Claude Models the Same Repo Audit. Fable Didn't Win — and That's the Point.</title>
      <dc:creator>CTRLNODE.AI</dc:creator>
      <pubDate>Fri, 03 Jul 2026 00:05:07 +0000</pubDate>
      <link>https://dev.to/ctrlnodeai/we-gave-five-claude-models-the-same-repo-audit-fable-didnt-win-and-thats-the-point-2k44</link>
      <guid>https://dev.to/ctrlnodeai/we-gave-five-claude-models-the-same-repo-audit-fable-didnt-win-and-thats-the-point-2k44</guid>
      <description>&lt;p&gt;When Anthropic shipped &lt;strong&gt;Claude Fable&lt;/strong&gt;, the obvious question was: &lt;em&gt;does the new tier beat everything else on hard engineering work?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We didn't want a benchmark score or a vibe check. We wanted a &lt;strong&gt;principal-engineer audit&lt;/strong&gt; of a real production monorepo — with &lt;code&gt;file:line&lt;/code&gt; evidence, severity labels, and an execution plan.&lt;/p&gt;

&lt;p&gt;So we ran a controlled experiment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One prompt&lt;/strong&gt; (4 phases: repo map → audit → strategy → task plan)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One target&lt;/strong&gt;: the &lt;a href="https://github.com/langchain-ai/langchain" rel="noopener noreferrer"&gt;LangChain&lt;/a&gt; Python monorepo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five Claude models&lt;/strong&gt;: Opus 4.8, Fable 5, Sonnet 5, Sonnet 4.6, Haiku 4.5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same setup for every run&lt;/strong&gt; — project, work directory, WORK DIRECTORY task mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We ran it through &lt;strong&gt;CTRL NODE&lt;/strong&gt; (Bridge on a real machine, one agent per model tier). Not five browser tabs — a workflow you'd actually use on a team.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we asked every model to deliver
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a world-class, principal-engineer-level software engineer and technical audit expert.
Perform an in-depth analysis of this code repository, provide an honest audit report,
and offer a prioritized, actionable improvement plan.

Follow four phases in order: Discovery → Audit → Strategy → Task Plan.
All judgments must cite real file paths and line numbers. Do not guess.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each run had to produce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;audit-report-&amp;lt;model&amp;gt;.md&lt;/code&gt; — full Markdown report&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;audit-report-&amp;lt;model&amp;gt;.html&lt;/code&gt; — interactive dashboard (Overview, Map, Audit, Strategy, Tasks)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The headline result
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;There is no single winner.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Five reports, five roles. If you only pay for the most expensive tier thinking it "does everything better," you miss findings.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Grade&lt;/th&gt;
&lt;th&gt;Best at&lt;/th&gt;
&lt;th&gt;Weak at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Opus 4.8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A−&lt;/td&gt;
&lt;td&gt;Threat modeling (TOCTOU, agent shell defaults)&lt;/td&gt;
&lt;td&gt;CI lockfile, default &lt;code&gt;load()&lt;/code&gt;, README gaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fable 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A−&lt;/td&gt;
&lt;td&gt;Strategy, milestones, quick wins, eng debt&lt;/td&gt;
&lt;td&gt;Agent-specific threats, SSRF adoption map&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sonnet 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;B+&lt;/td&gt;
&lt;td&gt;SSRF infra vs adoption, repo hygiene&lt;/td&gt;
&lt;td&gt;Lockfile CI, README, SECURITY.md&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sonnet 4.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;B+&lt;/td&gt;
&lt;td&gt;Ops: lockfile CI, &lt;code&gt;load()&lt;/code&gt; default, docs&lt;/td&gt;
&lt;td&gt;Newer SSRF adoption analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Haiku 4.5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A*&lt;/td&gt;
&lt;td&gt;Fast LOC map, callback cycles&lt;/td&gt;
&lt;td&gt;*Inflated grade; wrong CI lockfile claim&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;*Haiku's &lt;strong&gt;A&lt;/strong&gt; looked confident on paper. Cross-checking against Sonnet 4.6 exposed a factual error on lockfile validation in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opus and Fable tied on grade — but not on role.&lt;/strong&gt; Opus sees design-level threats. Fable turns findings into a shippable backlog (M0–M3, effort/risk, explicit non-goals).&lt;/p&gt;




&lt;h2&gt;
  
  
  Who saw what (selected)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;Op&lt;/th&gt;
&lt;th&gt;Fb&lt;/th&gt;
&lt;th&gt;S5&lt;/th&gt;
&lt;th&gt;S4.6&lt;/th&gt;
&lt;th&gt;Hk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TOCTOU / DNS rebinding&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSRF transport ~2 call sites&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default &lt;code&gt;load()&lt;/code&gt; unsafe&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan M0–M3 + non-goals&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lockfile CI commented&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗ wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fable did &lt;strong&gt;not&lt;/strong&gt; surface several issues other models caught (TOCTOU, shell host defaults, SSRF gaps in &lt;code&gt;graph_mermaid.py&lt;/code&gt;, commented lockfile CI). That gap is the point: &lt;strong&gt;Fable is not a replacement for a multi-model pipeline.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The pipeline we'd actually use
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Haiku        → fast map &amp;amp; architecture hotspots
Sonnet 5     → primary audit + security adoption gaps
Sonnet 4.6   → CI, docs, onboarding landmines
Opus         → threat review for agent-facing surfaces
Fable        → merge into one prioritized backlog
Human        → verify _lint.yml, load.py, README in your checkout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Model choice is a workflow decision&lt;/strong&gt;, not a vanity tier pick.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaways for builders
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A high grade ≠ a better report.&lt;/strong&gt; Two models at A−; Haiku at A — with a factual miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security has layers&lt;/strong&gt; — Opus (TOCTOU), Sonnet 5 (SSRF adoption), Sonnet 4.6 + Fable (unsafe default &lt;code&gt;load()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sonnet evolved&lt;/strong&gt; — 5 and 4.6 complement each other; neither replaces the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WORK DIRECTORY mode matters&lt;/strong&gt; — an output-only sandbox wouldn't have produced citations across CI, core, and partner packages.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Full write-up + all artifacts
&lt;/h2&gt;

&lt;p&gt;This post is the teaser. Everything else lives on our site:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://ctrlnode.ai/news/fable-claude-model-audit-experiment/" rel="noopener noreferrer"&gt;Read the full experiment&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step-by-step CTRL NODE setup (project, agents, tasks)&lt;/li&gt;
&lt;li&gt;What Fable returned in detail (executive summary, exclusive findings)&lt;/li&gt;
&lt;li&gt;Full comparison report + &lt;strong&gt;14-slide deck&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All five audit reports&lt;/strong&gt; (Markdown + interactive HTML dashboards)&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;complete audit prompt&lt;/strong&gt; to rerun on your own repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try this on your stack — or disagree with a grade — we'd love to hear what surprised you.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>claude</category>
      <category>llm</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Building an outbound-only WebSocket bridge for local AI agents</title>
      <dc:creator>CTRLNODE.AI</dc:creator>
      <pubDate>Sat, 23 May 2026 00:25:34 +0000</pubDate>
      <link>https://dev.to/ctrlnodeai/building-an-outbound-only-websocket-bridge-for-local-ai-agents-2o6g</link>
      <guid>https://dev.to/ctrlnodeai/building-an-outbound-only-websocket-bridge-for-local-ai-agents-2o6g</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6onw7h59eo0wi17pv17y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6onw7h59eo0wi17pv17y.png" alt="Building an outbound-only WebSocket bridge for local AI agents" width="799" height="336"&gt;&lt;/a&gt;&lt;br&gt;
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: &lt;strong&gt;the moment you leave your desk, you lose control&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://ctrlnode.ai" rel="noopener noreferrer"&gt;CTRL NODE&lt;/a&gt; — a browser-based control plane for local AI agents. The key piece is a process called the &lt;strong&gt;Bridge&lt;/strong&gt;: a lightweight Node.js daemon that runs on your machine and connects to the cloud without ever accepting an inbound connection.&lt;/p&gt;

&lt;p&gt;This article is about how that works, why the design choices matter, and what the actual code looks like.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why outbound-only?
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

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

&lt;p&gt;The alternative: flip the connection direction. The Bridge connects &lt;em&gt;out&lt;/em&gt; to the cloud. The cloud pushes commands &lt;em&gt;down&lt;/em&gt; that connection. The local machine never listens on a public port.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your machine                     ctrlnode.ai cloud
──────────────────────────────────────────────────
Bridge ──── ws:// connect() ────▶ WebSocket server
       ◀─── {action: "run_task", ...} ────────────
       ─────  stdout/stderr events ──────────────▶
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The connection lifecycle
&lt;/h2&gt;

&lt;p&gt;Here's the core of &lt;code&gt;websocket.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildWsUrl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildAuthHeaders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bridge connected to SAAS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;flushPendingQueue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;startHeartbeat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RawData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;InboundMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;handleInboundMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;stopHeartbeat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isAuthError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Auth error (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;), retrying in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;AUTH_RETRY_MS&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AUTH_RETRY_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;scheduleReconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`WebSocket error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auth errors get a longer timeout.&lt;/strong&gt; If the server returns 1008 (Policy Violation) or 1002, or the reason string contains &lt;code&gt;"401"&lt;/code&gt;/&lt;code&gt;"403"&lt;/code&gt;/&lt;code&gt;"Unauthorized"&lt;/code&gt;, we wait 30 seconds before retrying. Hammering an auth-rejected endpoint is pointless and noisy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Normal closes trigger exponential backoff.&lt;/strong&gt; &lt;code&gt;scheduleReconnect()&lt;/code&gt; uses a standard backoff so a transient network blip doesn't flood logs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;On open, we flush the queue.&lt;/strong&gt; More on this below.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Keeping the connection alive through load balancers
&lt;/h2&gt;

&lt;p&gt;Cloud load balancers will kill idle WebSocket connections after 30–60 seconds. The fix is a heartbeat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HEARTBEAT_INTERVAL_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;heartbeatTimer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NodeJS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Timeout&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startHeartbeat&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;heartbeatTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;sendToSaas&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heartbeat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;HEARTBEAT_INTERVAL_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stopHeartbeat&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heartbeatTimer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heartbeatTimer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;heartbeatTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Buffering outbound messages during disconnection
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PENDING_QUEUE_MAX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pendingQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OutboundMessage&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendToSaas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OutboundMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readyState&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pendingQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;PENDING_QUEUE_MAX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;pendingQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;flushPendingQueue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pendingQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pendingQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h2&gt;
  
  
  Multi-agent routing via the filesystem
&lt;/h2&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;The answer isn't a routing layer in the WebSocket code. It's the &lt;strong&gt;filesystem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each pipeline task gets an isolated directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Bridge watches these directories. When a &lt;code&gt;run_task&lt;/code&gt; command arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run_task&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agentProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;workspacePath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agentProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Claude | Copilot | Gemini | ...&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;workspacePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each provider implementation knows how to invoke its agent CLI with the right arguments and working directory. Claude Code gets &lt;code&gt;claude --print&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Provider selection and gating
&lt;/h2&gt;

&lt;p&gt;Some actions only make sense for certain providers. The message handler maintains an explicit set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OPENCLAW_ONLY_ACTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openclaw_configure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openclaw_stream_chunk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openclaw_reset_context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleInboundMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InboundMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OPENCLAW_ONLY_ACTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;activeProvider&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openclaw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Received &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; but provider is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;activeProvider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — ignoring`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ... dispatch to handler&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The startup sequence
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;index.ts&lt;/code&gt; ties it together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createProviders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;multi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MultiProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// start WebSocket, non-blocking&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keepaliveInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;keepaliveInterval&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unref&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// don't prevent process exit&lt;/span&gt;

  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGINT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gracefulShutdown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gracefulShutdown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;multi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runSyncAgents&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// provider-specific background sync&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h2&gt;
  
  
  What this enables
&lt;/h2&gt;

&lt;p&gt;With the Bridge running, the CTRL NODE web app can:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why open source?
&lt;/h2&gt;

&lt;p&gt;The Bridge is MIT licensed (&lt;a href="https://github.com/ctrlnode-ai/ctrlnode" rel="noopener noreferrer"&gt;github.com/ctrlnode-ai/ctrlnode&lt;/a&gt;). 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you work with AI agents and want a way to control them remotely without sacrificing privacy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the Bridge: &lt;code&gt;npm install -g @ctrlnode/bridge &amp;amp;&amp;amp; ctrlnode bridge start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sign up at &lt;a href="https://ctrlnode.ai" rel="noopener noreferrer"&gt;ctrlnode.ai&lt;/a&gt; — it's free&lt;/li&gt;
&lt;li&gt;Open the web app from anywhere and connect&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Questions, issues, or PRs: &lt;a href="https://github.com/ctrlnode-ai/ctrlnode" rel="noopener noreferrer"&gt;github.com/ctrlnode-ai/ctrlnode&lt;/a&gt; or reply here.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Javier Vil — Creator of CTRL NODE&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>websocket</category>
      <category>node</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
