<?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: flisk</title>
    <description>The latest articles on DEV Community by flisk (@flisk).</description>
    <link>https://dev.to/flisk</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%2F4145557%2F6879342a-c388-4b5e-818b-aa736b779196.png</url>
      <title>DEV Community: flisk</title>
      <link>https://dev.to/flisk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/flisk"/>
    <language>en</language>
    <item>
      <title>The Local Loop Is the Architecture</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Sat, 31 Oct 2026 20:23:44 +0000</pubDate>
      <link>https://dev.to/flisk/the-local-loop-is-the-architecture-16f</link>
      <guid>https://dev.to/flisk/the-local-loop-is-the-architecture-16f</guid>
      <description>&lt;p&gt;We spent a decade teaching systems to cross a network. We are about to spend the next one teaching them not to.&lt;/p&gt;

&lt;p&gt;An agent is not a service with a prompt bolted on. It is a runtime that can decide, retry, and call a tool whose effect outlives the call. Put two of those runtimes in different languages — a legal graph in Python, a commercial node in TypeScript — and the interesting design problem is no longer which model you picked. It is which process is allowed to touch the ledger, and how the other processes are prevented from growing their own path to it.&lt;/p&gt;

&lt;p&gt;That boundary will not appear in production. By then each team will have a client, a secret, and a story about retries. It has to appear on the laptop, while both agents are still being rewritten every afternoon. The local loop is not a staging copy of the architecture. For this class of system, it is the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fragmentation moved
&lt;/h2&gt;

&lt;p&gt;The old fragmentation was operational. Compose files, port maps, a README that lied about the order of &lt;code&gt;docker compose up&lt;/code&gt;. Painful, and solvable, because the processes were passive. A worker did not invent a new call while you were looking at a trace.&lt;/p&gt;

&lt;p&gt;Agent fragmentation is semantic. The Python node and the TypeScript node can both "check the budget" and mean different things: one reads a view, one posts a reservation, one retries the post because its HTTP client timed out after the database had already committed. They share no type, no credential policy, and no definition of an attempt. Language is the visible difference. &lt;strong&gt;Tool ownership&lt;/strong&gt; is the defect.&lt;/p&gt;

&lt;p&gt;A heterogeneous pair makes this obvious.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  UI[BFF] --&amp;gt; Legal[Legal node\nPython]
  UI --&amp;gt; Commercial[Commercial node\nTypeScript]
  Legal --&amp;gt; ERP[(Ledger)]
  Commercial --&amp;gt; ERP&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Two arrows into the ledger is already a distributed system. The model vendors will not design it for you. Neither will a framework that only knows one language. Mastra does not owe LangGraph a session model. LangGraph does not owe your Next.js app a type. The thing they can share is a protocol at the edge of the privileged resource, and a loop that runs all three processes where a human can still watch a retry happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  A boundary with a name
&lt;/h2&gt;

&lt;p&gt;Call the privileged process a &lt;strong&gt;Local Loop Gateway&lt;/strong&gt;. Agents never receive a connection string. They receive a tool list. The gateway is the only component that can turn a tool call into a query, and the only component that is allowed to remember that a given attempt already happened.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  UI[BFF\nmints a session]
  subgraph loop [Local loop]
    direction TB
    Legal[Legal node]
    Commercial[Commercial node]
    Bus[Bus]
    GW[Loop gateway\nMCP]
    Legal &amp;lt;--&amp;gt;|events| Bus
    Commercial &amp;lt;--&amp;gt;|events| Bus
    Legal --&amp;gt;|tools| GW
    Commercial --&amp;gt;|tools| GW
  end
  UI --&amp;gt; Legal
  UI --&amp;gt; Commercial
  GW --&amp;gt;|one credential| ERP[(Ledger)]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;MCP is the dialect. It is not the control. A schema that says &lt;code&gt;departmentId: string&lt;/code&gt; does not stop a second write. What stops the second write is a process that refuses to be a tunnel: closed arguments, named operations, an attempt id stored beside the row.&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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ListToolsRequestSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="na"&gt;tools&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;check_department_budget&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Read the remaining budget for one department.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&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;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;departmentId&lt;/span&gt;&lt;span class="p"&gt;:&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;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&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;departmentId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;additionalProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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="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;The frontier move is not "expose the ERP over MCP." That is how you launder a database into every agent. The move is to publish &lt;strong&gt;capabilities&lt;/strong&gt;, and to make the capability smaller than the fear. &lt;code&gt;check_department_budget&lt;/code&gt; can be backed by a view, a procedure, or a fixture. The agent cannot tell, and should not be able to. When the fixture and the procedure return the same shape, you have a boundary. When they do not, you have a demo.&lt;/p&gt;

&lt;p&gt;Three properties fall out of putting the tool in a process that is not an agent.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credentials live in one environment. Adding a third language does not add a third secret.&lt;/li&gt;
&lt;li&gt;Argument policy runs before the query is built. Rate limits and sanitization are gateway code, not a paragraph in each agent's system prompt.&lt;/li&gt;
&lt;li&gt;The attempt id is a write-side fact. A retry is a cache hit against the ledger, not a second mutation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are model features. They are what you have left when the model is allowed to call something that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two channels, not one
&lt;/h2&gt;

&lt;p&gt;Agents that share a ledger still have to negotiate with each other. The failure mode is to make that negotiation another HTTP call, because HTTP is what the laptop already has.&lt;/p&gt;

&lt;p&gt;Do not. A tool call and a conversation are different topologies, and collapsing them is how ordering bugs become invisible.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Carries&lt;/th&gt;
&lt;th&gt;Must not carry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MCP, into the gateway&lt;/td&gt;
&lt;td&gt;A named operation on a privileged resource&lt;/td&gt;
&lt;td&gt;"Ask the other agent"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bus, between agents&lt;/td&gt;
&lt;td&gt;Facts about what an agent decided&lt;/td&gt;
&lt;td&gt;A connection string, a retry of a write&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The BFF mints &lt;code&gt;x-session-id&lt;/code&gt; once per user task. That id is for routing and traces. It is not an idempotency key. A session contains many writes. An attempt contains one. If the commercial node times out on &lt;code&gt;reserve_budget&lt;/code&gt; and sends the same attempt id again, the gateway returns the first reservation. If it sends the session id instead, the gateway cannot tell a retry from a second offer, and your ledger grows a twin.&lt;/p&gt;

&lt;p&gt;Events stay dull on purpose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;legal.checkpoint.recorded  { session, attempt, departmentId }
commercial.offer.proposed  { session, eventId, terms }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dull events are how you keep a Python graph and a TypeScript workflow from becoming each other's clients. The moment the commercial node imports a URL for the legal node, you have coupled two hot-reload cycles and called it an architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop has to be generated
&lt;/h2&gt;

&lt;p&gt;This pattern dies if the loop is a hand-maintained Compose file. Someone will add a port "just for debugging," point an agent at the database, and the boundary will exist only in the diagram.&lt;/p&gt;

&lt;p&gt;The declaration has to be the source, and it has to be local. &lt;a href="https://flisk.sh" rel="noopener noreferrer"&gt;Flisk&lt;/a&gt; is a local control plane for that declaration: one &lt;code&gt;service.json&lt;/code&gt; per service, Docker and Tilt generated from it, traffic on &lt;code&gt;*.localhost&lt;/code&gt;, no deploy step hiding behind the same command. The legal node, the commercial node, the broker, and the MCP gateway come up as one stack because each file names its dependencies, not because a human remembered an order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agent-legal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python agent.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"healthCheckPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"secure-gateway"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"event-broker"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flisk doctor
flisk up enterprise-procurement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;doctor&lt;/code&gt; is part of the architecture. A loop that starts half-up teaches agents to retry against absence, and then you encode that retry as if it were a business rule. Fail before Tilt starts, or you will debug a race that was a missing binary.&lt;/p&gt;

&lt;p&gt;Live update is the other half. A change to an evaluation step should appear in the running graph without destroying the session you were tracing. A change to the declaration should not. Routes and dependencies are topology. Topology is regenerated on the next &lt;code&gt;up&lt;/code&gt;, not patched in a container that has already forgotten why it is allowed to listen.&lt;/p&gt;

&lt;p&gt;There is a second Flisk, at flisk.ai, that manages tags. It is a different system. The one this pattern needs is the CLI that refuses to deploy. That refusal matters. A tool that can both reshape your local boundary and ship it will eventually ship the debug port.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are actually testing
&lt;/h2&gt;

&lt;p&gt;A local multi-agent stack is not impressive because five containers started. It is impressive if you can force the failure the production incident will be.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call a write tool. Kill the client after the gateway has committed, before the agent has seen the response.&lt;/li&gt;
&lt;li&gt;Repeat the call with the same attempt id.&lt;/li&gt;
&lt;li&gt;Read the ledger. One row, or the pattern is fiction.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hot reload does not perform that test. Tilt updating a Python file only proves the file was mounted. The test is the duplicate, and the duplicate has to lose.&lt;/p&gt;

&lt;p&gt;The same discipline applies to the tool list. If a coding assistant can drive the loop — Flisk can expose the stack over MCP, the reverse direction of an &lt;code&gt;mcp&lt;/code&gt; resource — it must not be able to widen a tool because widening was faster than editing the schema. Generated files are not a scratchpad. The assistant proposes a change to &lt;code&gt;service.json&lt;/code&gt;. The next &lt;code&gt;up&lt;/code&gt; is the merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shift
&lt;/h2&gt;

&lt;p&gt;Monoliths were a packaging decision. Microservices were a team decision. Heterogeneous agents are a &lt;strong&gt;permission&lt;/strong&gt; decision: which runtime may cause an effect, and which runtimes may only request one.&lt;/p&gt;

&lt;p&gt;You can postpone that decision until the first double charge. Or you can put a small, closed tool list in a process that holds the only credential, run the disagreeing languages beside it, and watch an attempt id collide with itself before a customer does.&lt;/p&gt;

&lt;p&gt;The model will keep changing. The language of each node will keep changing. The boundary should not. Build the loop so that replacing an agent is a &lt;code&gt;service.json&lt;/code&gt; and a command, and replacing the ledger is an implementation detail behind a name the agent was never allowed to see.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>ai</category>
      <category>devops</category>
      <category>mcp</category>
    </item>
    <item>
      <title>The Local Loop Gateway</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Sat, 31 Oct 2026 20:22:00 +0000</pubDate>
      <link>https://dev.to/flisk/the-local-loop-gateway-1i4p</link>
      <guid>https://dev.to/flisk/the-local-loop-gateway-1i4p</guid>
      <description>&lt;p&gt;Agents have stopped being a single chat process. A procurement system might keep its graph in Python because that is where the evaluation library lives, and keep the commercial node in TypeScript because that is where the rest of the product already shares types. Both nodes need the same budget check. Both will retry. Neither should hold the ERP password.&lt;/p&gt;

&lt;p&gt;The usual local answer is a hand-written Compose file, a private network, and a growing set of environment variables. It works until the first agent is rewritten in another language, and then the wiring is rewritten with it.&lt;/p&gt;

&lt;p&gt;This article describes a local pattern for that situation. I will call it a &lt;strong&gt;Local Loop Gateway&lt;/strong&gt;. &lt;a href="https://flisk.sh" rel="noopener noreferrer"&gt;Flisk&lt;/a&gt; is one way to run the loop: it turns a &lt;code&gt;service.json&lt;/code&gt; per service into Docker and Tilt, routes the stack through a local gateway, and does not deploy. The pattern does not depend on Flisk. The tool only removes the part that is usually rewritten by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Consider two agents and one ledger.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  UI[Next.js BFF] --&amp;gt; Legal[Legal node\nPython]
  UI --&amp;gt; Commercial[Commercial node\nTypeScript]
  Legal --&amp;gt; ERP[(ERP / ledger)]
  Commercial --&amp;gt; ERP&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Each arrow into the ledger is a place where a runtime can grow its own driver, its own secret, and its own idea of what "retry" means. The legal node calls a Python client. The commercial node calls a generated TypeScript client. A third node, added next quarter, copies one of those two and drifts.&lt;/p&gt;

&lt;p&gt;Three failures follow, and they are ordinary rather than exotic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A credential lives in every agent image that needs the ledger.&lt;/li&gt;
&lt;li&gt;A retry after a dropped connection writes the ledger twice, because the agent and the ledger do not share a notion of "this attempt."&lt;/li&gt;
&lt;li&gt;Changing the budget check means changing every agent, in every language, in the same pull request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fragmentation people notice first is language. The fragmentation that costs more is &lt;strong&gt;tool ownership&lt;/strong&gt;: each runtime owns a private path to the same corporate resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structure
&lt;/h2&gt;

&lt;p&gt;Put the tools in a process that is not an agent. Agents talk to that process over MCP. The process is the only thing on the local loop that can reach the ledger.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  UI[Next.js BFF]
  subgraph loop [Local loop]
    Legal[Legal node\nPython + graph runtime]
    Commercial[Commercial node\nTypeScript]
    Bus[Event broker]
    GW[Gateway proxy\nMCP server]
    Legal &amp;lt;--&amp;gt;|pub/sub| Bus
    Commercial &amp;lt;--&amp;gt;|pub/sub| Bus
    Legal --&amp;gt;|MCP| GW
    Commercial --&amp;gt;|MCP| GW
  end
  UI --&amp;gt;|session id| Legal
  UI --&amp;gt;|session id| Commercial
  GW --&amp;gt;|bounded query| ERP[(Database / ERP)]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;MCP is the contract, not the security boundary. The boundary is the proxy: it is the only process with the connection string, the only process allowed to call the ledger, and the only process that can reject a repeated write.&lt;/p&gt;

&lt;p&gt;Agents still talk to each other, but not by calling each other's HTTP ports. A negotiation is an event on the broker. A budget check is a tool call. Mixing those two paths is how a "quick RPC" becomes a hidden dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gateway
&lt;/h2&gt;

&lt;p&gt;The proxy advertises a small tool list. Small is the point. An agent that can run arbitrary SQL is not behind a boundary; it is holding a wider keyboard.&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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ListToolsRequestSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tools&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;check_department_budget&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Returns the remaining budget for one department. Read only.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&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;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;departmentId&lt;/span&gt;&lt;span class="p"&gt;:&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;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&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;departmentId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;additionalProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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="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 rules keep this from becoming a pass-through.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Arguments are a closed schema.&lt;/strong&gt; Unknown fields are rejected in the proxy, before any query is built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tool names an operation, not a table.&lt;/strong&gt; &lt;code&gt;check_department_budget&lt;/code&gt; can be implemented as a view, a stored procedure, or a mock. The agent does not get to choose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes carry the caller's attempt id.&lt;/strong&gt; The proxy stores that id. A second call with the same id returns the first result and does not append another ledger row.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Credentials stay in the proxy's environment. Agent containers receive a URL, not a password. That split is what makes a Python node and a TypeScript node interchangeable from the ledger's point of view: both are MCP clients, and neither can become a database client by editing an env file.&lt;/p&gt;

&lt;p&gt;On Flisk, this proxy is an &lt;code&gt;mcp&lt;/code&gt; resource in the same stack as the agents. &lt;code&gt;flisk up&lt;/code&gt; starts it with the rest of the loop, and its health route is what other services wait on. The scaffold is for &lt;code&gt;*.localhost&lt;/code&gt;. It is not an internet-facing ERP facade, and it should not gain one by publishing the port.&lt;/p&gt;

&lt;h2&gt;
  
  
  One file per service
&lt;/h2&gt;

&lt;p&gt;The loop is several processes with one declaration each. Flisk's unit is a &lt;code&gt;service.json&lt;/code&gt; beside the code, not one document that lists every container. From those files it generates the Dockerfile, the Compose entry, the gateway routes, and the Tiltfile. Editing the generated files is how the loop drifts; the next &lt;code&gt;flisk up&lt;/code&gt; treats that drift as a mistake.&lt;/p&gt;

&lt;p&gt;A backend, in the shape the tool actually expects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agent-legal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python agent.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"healthCheckPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"secure-gateway"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"event-broker"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commercial node is the same kind of file, with a different command. The MCP server is a different type — &lt;code&gt;appType: "mcp"&lt;/code&gt; — because it is a tool host, not another agent. The broker is a dependency both agents name, so the stack starts in an order a health check can enforce.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flisk doctor
flisk up enterprise-procurement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;doctor&lt;/code&gt; is worth running first. It fails on a missing runtime or a port clash before Tilt has started half the graph. &lt;code&gt;flisk up&lt;/code&gt; then starts the stack and live-updates a container when its code changes. A change to &lt;code&gt;service.json&lt;/code&gt; itself is not a live update: routes and dependencies are regenerated on the next up.&lt;/p&gt;

&lt;p&gt;What you do not get, and should not pretend you got, is a production orchestrator. Flisk's own description is local-only. The same boundary still has to be rebuilt with your real gateway, your real secret store, and your real network policy. The local loop is a place to discover that the boundary is wrong. It is not the boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session, bus, and the duplicate write
&lt;/h2&gt;

&lt;p&gt;Two autonomous nodes in a loop will race. The commercial node will counter-offer while the legal node is still writing its checkpoint. A timeout will cause a retry of a call that already committed. None of this is fixed by picking Python or TypeScript.&lt;/p&gt;

&lt;p&gt;Keep three identifiers apart.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Identifier&lt;/th&gt;
&lt;th&gt;Who mints it&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x-session-id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The BFF, once per user task&lt;/td&gt;
&lt;td&gt;Sticky routing and trace correlation. Not a write key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event id&lt;/td&gt;
&lt;td&gt;The publishing agent&lt;/td&gt;
&lt;td&gt;Ordering on the bus. Consumers dedupe on it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attempt id&lt;/td&gt;
&lt;td&gt;The agent, once per tool call&lt;/td&gt;
&lt;td&gt;The proxy's idempotency key for a ledger write.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using the session id as the write key is the mistake that looks tidy in a diagram. A session covers many writes. An attempt covers one. If the commercial node retries &lt;code&gt;reserve_budget&lt;/code&gt; after a timeout, the proxy must see the same attempt id and return the reserved row, not reserve again.&lt;/p&gt;

&lt;p&gt;Agents publish events. They do not call each other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;legal.checkpoint.recorded   { session, attempt, departmentId }
commercial.offer.proposed   { session, eventId, terms }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A direct HTTP call from the commercial node to the legal node works on a laptop and hides the ordering bug until both are slow. The broker makes the ordering visible, which is the property you want while the system is still small enough to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not solve
&lt;/h2&gt;

&lt;p&gt;The pattern moves credentials and write policy into one process. It does not make that process correct.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A tool that accepts a raw query string has moved the vulnerability into the proxy, which is worse, because every agent can now reach it.&lt;/li&gt;
&lt;li&gt;Hot reload does not preserve a careful test. Tilt updating a Python node does not prove the idempotency filter works; a test that posts the same attempt twice does.&lt;/li&gt;
&lt;li&gt;Local &lt;code&gt;*.localhost&lt;/code&gt; routing is not authentication. A tool with no auth is acceptable on a laptop and unacceptable the moment the port is reachable from another machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a product boundary worth stating plainly. "Flisk" is used by more than one company. This article means the local-development CLI at flisk.sh — &lt;code&gt;service.json&lt;/code&gt;, Docker, Tilt, no deploy — not the tag-management product at flisk.ai. Mixing the two in a design doc wastes the afternoon the names were supposed to save.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use it
&lt;/h2&gt;

&lt;p&gt;Use a Local Loop Gateway when at least two runtimes must share a privileged resource and you are still changing both. The MCP schema is the stable edge. The agent processes are allowed to be boring, replaceable, and written in whatever language already has the library.&lt;/p&gt;

&lt;p&gt;Do not use it to avoid an API. If there is one service and one language, a typed client is simpler. MCP pays for itself when the second runtime appears, and when you want the credential, the rate limit, and the duplicate-write rule in one place rather than in every agent.&lt;/p&gt;

&lt;p&gt;The local loop is where that rule should fail first. &lt;code&gt;flisk doctor&lt;/code&gt;, then &lt;code&gt;flisk up&lt;/code&gt;, then a retry of the same attempt id. If the ledger grew a second row, the gateway is still a proxy in name only.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>ai</category>
      <category>devops</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Why the Model Context Protocol (MCP) Is Changing How We Connect AI Frameworks</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Sat, 31 Oct 2026 20:18:48 +0000</pubDate>
      <link>https://dev.to/flisk/why-the-model-context-protocol-mcp-is-changing-how-we-connect-ai-frameworks-1e8b</link>
      <guid>https://dev.to/flisk/why-the-model-context-protocol-mcp-is-changing-how-we-connect-ai-frameworks-1e8b</guid>
      <description>&lt;p&gt;When designing multi-agent software, you eventually hit an integration wall: &lt;strong&gt;language fragmentation&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Your legal auditing teams prefer Python for its rich scientific tools and ecosystems like LangGraph. Meanwhile, your commercial or user-facing development teams prefer TypeScript for fast, type-safe API platforms like Mastra or Next.js. &lt;/p&gt;

&lt;p&gt;If both frameworks need to verify internal ERP limits, read standard database layers, or talk to third-party endpoints, you end up writing identical tool functions twice—once in Python and once in TypeScript. This duplicates your work and doubles your security vulnerabilities.&lt;/p&gt;

&lt;p&gt;This is exactly why the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; is turning into a fundamental building block for enterprise AI development.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is MCP?
&lt;/h3&gt;

&lt;p&gt;Developed as an open-source framework, MCP behaves like an &lt;strong&gt;open data proxy layer for LLMs&lt;/strong&gt;. Instead of letting individual agents hook up directly to databases or proprietary external APIs, you implement a single centralized gateway application: the &lt;strong&gt;MCP Server&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your agent networks (regardless of whether they are built on Python, TypeScript, or Go) connect to this proxy server over standard transport channels like Stdio or Server-Sent Events (SSE). &lt;/p&gt;

&lt;h3&gt;
  
  
  Eliminating Tool Duplication
&lt;/h3&gt;

&lt;p&gt;With an MCP architecture, your tools are decoupled from your agent frameworks entirely. The gateway handles the data exposure securely:&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Built a Multi-Agent Procurement System with LangGraph, Mastra, and Flisk</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Sat, 31 Oct 2026 20:18:06 +0000</pubDate>
      <link>https://dev.to/flisk/how-i-built-a-multi-agent-procurement-system-with-langgraph-mastra-and-flisk-h3b</link>
      <guid>https://dev.to/flisk/how-i-built-a-multi-agent-procurement-system-with-langgraph-mastra-and-flisk-h3b</guid>
      <description>&lt;p&gt;Multi-agent orchestration often sounds easier than it is. When building a system where a Python legal bot needs to debate a TypeScript commercial bot, the infrastructure overhead alone can stall the project. You find yourself wrestling with multiple runtime environments, dockerizing standalone processes, and duplicating tools across separate codebases.&lt;/p&gt;

&lt;p&gt;To solve this, I built an enterprise-grade multi-agent procurement system completely locally using &lt;strong&gt;Flisk&lt;/strong&gt; (&lt;code&gt;flisk.sh&lt;/code&gt;). Flisk acts as a local runtime gateway, bundling multi-language agents, persistent databases, and event streams into a hot-reloading local stack.&lt;/p&gt;

&lt;p&gt;Here is the exact architecture and implementation for a stateful, cross-language procurement network.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Architecture Setup
&lt;/h3&gt;

&lt;p&gt;The system breaks down into four core decoupled layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The BFF &amp;amp; User Interface (Next.js):&lt;/strong&gt; The control center where contract drafts are uploaded and live AI negotiation loops are monitored on an interactive canvas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Legal Agent (Python + LangGraph):&lt;/strong&gt; A cyclical graph that extracts contract data and checks it against compliance matrices. It includes a Human-in-the-Loop checkpoint gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Commercial Agent (TypeScript + Mastra):&lt;/strong&gt; A fast TypeScript node that reviews financial lines, balances department limits, and drafts counters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Secure Gateway (MCP Server):&lt;/strong&gt; An abstraction proxy providing bounded local database and mock ERP data access to both agents via the Model Context Protocol.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Setting Up the Stack with Flisk
&lt;/h3&gt;

&lt;p&gt;Instead of crafting manual docker-compose structures, we initialize our resources right out of the box using the Flisk CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Initialize a local multi-agent stack&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;procurement-core &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;procurement-core

&lt;span class="c"&gt;# Scaffold the independent runtime environments&lt;/span&gt;
flisk resource legal-agent &lt;span class="nt"&gt;--type&lt;/span&gt; agent &lt;span class="nt"&gt;--stack&lt;/span&gt; procurement &lt;span class="nt"&gt;--yes&lt;/span&gt;
flisk resource commercial-agent &lt;span class="nt"&gt;--type&lt;/span&gt; agent &lt;span class="nt"&gt;--stack&lt;/span&gt; procurement &lt;span class="nt"&gt;--yes&lt;/span&gt;
flisk resource secure-gateway-mcp &lt;span class="nt"&gt;--type&lt;/span&gt; mcp &lt;span class="nt"&gt;--stack&lt;/span&gt; procurement &lt;span class="nt"&gt;--yes&lt;/span&gt;

&lt;span class="c"&gt;# Spin up the environment &lt;/span&gt;
flisk up procurement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flisk leverages a local &lt;code&gt;.flisk/procurement/service.json&lt;/code&gt; configuration file to unify network paths, set up environment registries, and enable hot-reloading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"procurement"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"services"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"secure-gateway"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node:20-alpine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"3001:3001"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"agent-legal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python:3.11-slim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python agent.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"MCP_GATEWAY_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://secure-gateway:3001/sse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"REDIS_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"redis://event-broker:6379"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"agent-commercial"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node:20-alpine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx mastra start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"MCP_GATEWAY_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://secure-gateway:3001/sse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"REDIS_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"redis://event-broker:6379"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"event-broker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"redis:7-alpine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"6379:6379"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Streaming Live Negotiations to Next.js
&lt;/h3&gt;

&lt;p&gt;To track AI-to-AI arguments in real time, standard polling doesn't cut it. Both agent nodes stream state updates to our local Redis event broker, which the Next.js BFF hooks into using Server-Sent Events (SSE):&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="c1"&gt;// app/api/negotiation-stream/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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;redisClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&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;redisClient&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;responseStream&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;TransformStream&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;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;responseStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getWriter&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;encoder&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Listen directly to the multi-agent network channel&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redisClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;procurement-negotiation-channel&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;message&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;eventPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`data: &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;\n\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventPayload&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abort&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redisClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responseStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readable&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="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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="s1"&gt;text/event-stream&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="s1"&gt;Cache-Control&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="s1"&gt;no-cache, no-transform&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="s1"&gt;Connection&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="s1"&gt;keep-alive&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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Building the Control Canvas
&lt;/h3&gt;

&lt;p&gt;On the frontend, we map incoming data payloads directly to an infinite UI canvas built with &lt;code&gt;@xyflow/react&lt;/code&gt;. When an agent is actively working or requests a Human-in-the-Loop check, the node pulses dynamically on the dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/NegotiationCanvas.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReactFlow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Controls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Edge&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@xyflow/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@xyflow/react/dist/style.css&lt;/span&gt;&lt;span class="dl"&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;initialNodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-ui&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;📥 Ingestion Hub&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent-legal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;⚖️ Legal Node (Python)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent-commercial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;450&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;💰 Commercial Node (TS)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;borderRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;NegotiationCanvas&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;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setNodes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Node&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;initialNodes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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;eventSource&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;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/negotiation-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;data&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;event&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;setNodes&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;nds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;nds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;node&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeAgentId&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;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;style&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                &lt;span class="na"&gt;backgroundColor&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="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INTERRUPT&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="s1"&gt;#fee2e2&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="s1"&gt;#eff6ff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;borderColor&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="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INTERRUPT&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="s1"&gt;#ef4444&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="s1"&gt;#3b82f6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;boxShadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0 0 15px rgba(59, 130, 246, 0.5)&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="p"&gt;}&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;node&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="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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;500px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactFlow&lt;/span&gt; &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;fitView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Background&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Controls&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ReactFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;By allowing Flisk to govern the connectivity and lifecycle of our cross-language stacks, we turn what used to be a sprawling infrastructure problem into a smooth, hot-reloading local dev experience.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>devops</category>
    </item>
    <item>
      <title>Mock it or run it? What 44 job roles say about MSW vs running your whole stack</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Wed, 28 Oct 2026 13:26:58 +0000</pubDate>
      <link>https://dev.to/flisk/mock-it-or-run-it-what-44-job-roles-say-about-msw-vs-running-your-whole-stack-1a6p</link>
      <guid>https://dev.to/flisk/mock-it-or-run-it-what-44-job-roles-say-about-msw-vs-running-your-whole-stack-1a6p</guid>
      <description>&lt;p&gt;Every team hits the same wall sooner or later. Your frontend needs an API that isn't ready. Your service needs three other services, a database and a queue. Your test needs the payment provider to fail on purpose.&lt;/p&gt;

&lt;p&gt;There are two answers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mock it.&lt;/strong&gt; Pretend the other side exists. &lt;a href="https://mswjs.io" rel="noopener noreferrer"&gt;Mock Service Worker&lt;/a&gt; (MSW) is the best-known way: it catches requests in the browser or in Node and answers with what you tell it to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it.&lt;/strong&gt; Start the real services on your laptop and talk to them for real: Docker Compose, Tilt, or a tool that wires them up for you (I build one, &lt;a href="https://flisk.sh" rel="noopener noreferrer"&gt;Flisk&lt;/a&gt;; more on that below).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted a less hand-wavy answer than "it depends", so I put both on the same page:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://roadmap.flisk.si/roles/?compare=msw,flisk" rel="noopener noreferrer"&gt;roadmap.flisk.si/roles/?compare=msw,flisk&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the scores work
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://roadmap.flisk.si" rel="noopener noreferrer"&gt;roadmap.flisk.si&lt;/a&gt; is a community-edited map of 372 developer tools. Nobody places a dot by hand. Each tool gets 10 points per checklist item it meets, on two axes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easy to use?&lt;/strong&gt; One command starts it, little setup, see changes fast, clear errors, a screen to look at, working in 10 minutes, Mac/Linux/Windows, no account needed, light and fast, AI can drive it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Made for big, real systems?&lt;/strong&gt; Works with containers, handles many services, services talk to each other, real databases and queues, same images as live servers, speaks Kubernetes, works with the cloud, real config and secrets, logs/metrics/traces, works for big teams.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every check is in a &lt;a href="https://github.com/flisk-sh/roadmap/blob/main/_data/rubric.yml" rel="noopener noreferrer"&gt;public YAML file&lt;/a&gt;. Think a dot is wrong? Open a PR with a source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Mock Service Worker&lt;/th&gt;
&lt;th&gt;Flisk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;API mocking by intercepting requests in browser and Node&lt;/td&gt;
&lt;td&gt;Local dev loop for microservices and multi-agent stacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Easy to use&lt;/td&gt;
&lt;td&gt;40/100&lt;/td&gt;
&lt;td&gt;80/100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Made for big, real systems&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/100&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50/100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ring&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Adopt&lt;/strong&gt; (use it)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Assess&lt;/strong&gt; (watch it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub stars&lt;/td&gt;
&lt;td&gt;★ 18.3k&lt;/td&gt;
&lt;td&gt;★ 27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Last release&lt;/td&gt;
&lt;td&gt;Oct 10, 2026&lt;/td&gt;
&lt;td&gt;Oct 24, 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 0 for MSW is not an insult. It is the point. MSW doesn't start containers, doesn't run services, doesn't touch a database. It is a scalpel: it replaces exactly one thing, the network answer, and nothing else. That is why it is fast, light and works with no account and no Docker.&lt;/p&gt;

&lt;p&gt;Flisk scores higher on ease of use (one command, little setup, clear errors, a UI, an MCP server) and gets halfway up the other axis (real containers, many services, real databases, services talking to each other). It misses Kubernetes, the cloud and production images, because it is local only by design.&lt;/p&gt;

&lt;p&gt;And Flisk is in &lt;strong&gt;Assess&lt;/strong&gt;, not Adopt. Its first release was in October 2026. The ring rule says anything under a year old is "watch it", and the site applies that rule to its own publisher too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the roles say
&lt;/h2&gt;

&lt;p&gt;This is the part I found most useful. The site has 44 job roles, each with the tools that job really uses: &lt;em&gt;must have&lt;/em&gt;, &lt;em&gt;every day&lt;/em&gt;, &lt;em&gt;good to know&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MSW is a must-have for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend Engineer&lt;/li&gt;
&lt;li&gt;Mobile Engineer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;…and an every-day tool for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full-stack Engineer&lt;/li&gt;
&lt;li&gt;QA / Test Automation Engineer&lt;/li&gt;
&lt;li&gt;Contract / Integration Test Engineer&lt;/li&gt;
&lt;li&gt;Integration Engineer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Flisk shows up in 20 roles&lt;/strong&gt;, from Backend and Platform Engineer to Forward Deployed Engineer, Tech Lead and Open-source Maintainer. But in every one of them it is only &lt;strong&gt;"good to know"&lt;/strong&gt;. Never a must-have.&lt;/p&gt;

&lt;p&gt;That is an honest picture of a new tool: broad fit, no one's job depends on it yet. MSW has earned its must-have spots.&lt;/p&gt;

&lt;h2&gt;
  
  
  So when do you mock, and when do you run?
&lt;/h2&gt;

&lt;p&gt;From the checklists and the roles, my rule of thumb:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mock it (MSW) when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're testing UI states: loading, empty, error, slow, 500, a weird edge case the real API rarely returns.&lt;/li&gt;
&lt;li&gt;The API contract is stable, or written down (OpenAPI), and you trust it.&lt;/li&gt;
&lt;li&gt;You want tests that run in milliseconds in CI with nothing else installed.&lt;/li&gt;
&lt;li&gt;The real backend doesn't exist yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Run it when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The bug lives &lt;em&gt;between&lt;/em&gt; services: auth, retries, timeouts, a queue that delivers twice.&lt;/li&gt;
&lt;li&gt;You need real data: migrations, indexes, a Postgres that behaves like Postgres.&lt;/li&gt;
&lt;li&gt;You're onboarding someone and "clone and it works" matters more than speed.&lt;/li&gt;
&lt;li&gt;Your mocks have drifted from reality and nobody noticed. (This is the classic MSW failure: green tests, broken app.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Usually: both.&lt;/strong&gt; MSW in unit and component tests, the real stack for integration and for day-to-day development. They are not rivals. They answer different questions, which is exactly what the 0 vs 50 on the "big, real systems" axis is telling you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your own stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compare any tools:&lt;/strong&gt; type &lt;code&gt;msw vs flisk&lt;/code&gt;, or &lt;code&gt;tilt vs skaffold vs garden&lt;/code&gt;, into &lt;a href="https://roadmap.flisk.si/ask/?q=msw+vs+flisk" rel="noopener noreferrer"&gt;Ask&lt;/a&gt;. It answers from the data, no AI call, and tells you what it can't answer (pricing, benchmarks).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find your role:&lt;/strong&gt; rate the tools you know from 0 to 3 in &lt;a href="https://roadmap.flisk.si/portfolio/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;. It suggests your best-fit role and draws your skill shape. Your ratings stay in your link; nothing is sent anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the dead stuff:&lt;/strong&gt; &lt;a href="https://roadmap.flisk.si/findings/" rel="noopener noreferrer"&gt;Findings&lt;/a&gt; lists tools that are archived or haven't shipped in 18 months, from live GitHub data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;I build Flisk, and Flisk publishes roadmap.flisk.si. That is exactly why the site scores Flisk with the same checklist as everyone else, gives it no special color or size, puts it in Assess, and leaves it off Journeys levels where it doesn't do the job yet.&lt;/p&gt;

&lt;p&gt;The first dataset was written in October 2026 with AI-assisted research, so it will have mistakes. If MSW deserves more than 40 for ease of use (I suspect it does: "little setup" and "see changes fast" are arguable), &lt;a href="https://github.com/flisk-sh/roadmap/blob/main/_data/tools/services.yml" rel="noopener noreferrer"&gt;send a PR&lt;/a&gt; and the dot moves.&lt;/p&gt;

&lt;p&gt;Which camp is your team in: mock everything, run everything, or both?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>testing</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Bubbles: a new kind of software, where one reply becomes a running stack</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:39:12 +0000</pubDate>
      <link>https://dev.to/flisk/bubbles-a-new-kind-of-software-where-one-reply-becomes-a-running-stack-1b72</link>
      <guid>https://dev.to/flisk/bubbles-a-new-kind-of-software-where-one-reply-becomes-a-running-stack-1b72</guid>
      <description>&lt;p&gt;Reply to a post on X with &lt;code&gt;@grok bubbles&lt;/code&gt; and a few words about an app. Grok answers with a link. Open the link and you get a running local stack: a web frontend, an API, a database, and a worker, each with its own &lt;code&gt;service.json&lt;/code&gt;, a repo built in CI, and, when a runner is free, a live run for up to two hours.&lt;/p&gt;

&lt;p&gt;We call this kind of software &lt;strong&gt;bubbles&lt;/strong&gt;. It is not an AI agent, and it is not a code generator. This post explains what it is, how it works, what broke, and what is still rough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a "bubble" is
&lt;/h2&gt;

&lt;p&gt;A bubble is a small, complete unit of software that is made from a short description and then checked by ordinary code. Three properties make it a category and not just a demo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The words pick the parts, nothing else.&lt;/strong&gt; A fixed menu maps words to parts: &lt;code&gt;postgres&lt;/code&gt; adds a database, &lt;code&gt;worker&lt;/code&gt; adds a queue, &lt;code&gt;solidity&lt;/code&gt; adds a chain node and an indexer. Words that are not on the menu are ignored. Nothing is generated by a model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every output is checked before anyone sees it.&lt;/strong&gt; The plan goes through a validator, then the real Flisk CLI builds the project, and &lt;code&gt;flisk config verify&lt;/code&gt; runs in CI. A bubble that does not verify does not get a link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is a link, not a session.&lt;/strong&gt; The result is a web address made of a handle and the words. Share it, bookmark it, open it tomorrow. The same words give the same stack.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Grok is only the trigger. The stack is written by code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not let the model write it?
&lt;/h2&gt;

&lt;p&gt;We tried that first, and it failed the way you would expect. The model confidently invented commands and service types that do not exist. So we removed it from the critical path. Grok asks for a link; a Cloudflare Worker turns the words into a plan; a GitHub Actions run creates the project with the CLI and pushes a branch.&lt;/p&gt;

&lt;p&gt;The trade-off is real. A bubble only knows what is on the menu, so an unusual architecture gets a plain default. That is the price of a stack that is the same every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the pipeline works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Worker:&lt;/strong&gt; reads the handle and words, builds the plan, checks it against rules, and stores the record. Opening a link that does not exist yet starts the build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI build:&lt;/strong&gt; a GitHub Actions run scaffolds the project with &lt;code&gt;flisk&lt;/code&gt;, creates one &lt;code&gt;service.json&lt;/code&gt; per service, and pushes branch &lt;code&gt;demo/&amp;lt;id&amp;gt;&lt;/code&gt; to a public repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live run:&lt;/strong&gt; a second workflow runs &lt;code&gt;flisk up&lt;/code&gt; on a GitHub-hosted Ubuntu runner with Docker and Tilt. Two Cloudflare quick tunnels expose the app and a read-only Tilt UI. Live state is a small &lt;code&gt;live.json&lt;/code&gt; on a &lt;code&gt;live/&amp;lt;id&amp;gt;&lt;/code&gt; branch, so the Worker reads it from GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Records:&lt;/strong&gt; every demo is one JSON file in the same public repo. No separate database.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What broke, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The free storage limit paused saving for hours.&lt;/strong&gt; Our first design stored records in Cloudflare KV, which has a daily write limit. Once it was hit, new demos could not be saved, and that stopped the whole pipeline. We moved live state to GitHub first, so running demos kept working. Then we moved the records too, so nothing needs KV now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runners were not the bottleneck.&lt;/strong&gt; Every live run started. Each one then failed on the last step, because it tried to save its links to KV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js did not work on the first try.&lt;/strong&gt; The generated dev server tried to write &lt;code&gt;.next&lt;/code&gt; inside a bind-mounted folder that the container user could not write to. The fix is in review. Next.js support in demos waits for the next flisk release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Grok trigger is not reliable yet.&lt;/strong&gt; Grok sometimes searches instead of answering with a link. We ask it to reply with an exact address, so it is easy to check, but that does not make it certain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is still rough
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The menu is small. Bubbles cannot describe a custom architecture.&lt;/li&gt;
&lt;li&gt;A live run is temporary and shared, so it can be busy.&lt;/li&gt;
&lt;li&gt;The first load of a live run can take minutes.&lt;/li&gt;
&lt;li&gt;Only one integration (Thor Commerce) is wired by hand. Everything else is a logged request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Make it yours
&lt;/h2&gt;

&lt;p&gt;Every demo is a plain git branch. The demo page has a "Make it yours" card with the commands to copy the branch into your own repository. Change anything, run it locally with &lt;code&gt;flisk up&lt;/code&gt;, and send us what you improved. We will show good changes on the demo page.&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://demo.flisk.sh" rel="noopener noreferrer"&gt;https://demo.flisk.sh&lt;/a&gt;. Read how it works in full: &lt;a href="https://demo.flisk.sh/how-it-works" rel="noopener noreferrer"&gt;https://demo.flisk.sh/how-it-works&lt;/a&gt;. The code is in the open: &lt;a href="https://github.com/flisk-sh/flisk" rel="noopener noreferrer"&gt;https://github.com/flisk-sh/flisk&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Bubbles are experimental. Tell us what breaks.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>grok</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What is "@grok bubbles"? A one-line way to get a running local stack from a tweet</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Sun, 25 Oct 2026 01:54:16 +0000</pubDate>
      <link>https://dev.to/flisk/what-is-grok-bubbles-a-one-line-way-to-get-a-running-local-stack-from-a-tweet-k12</link>
      <guid>https://dev.to/flisk/what-is-grok-bubbles-a-one-line-way-to-get-a-running-local-stack-from-a-tweet-k12</guid>
      <description>&lt;p&gt;Someone posts "the contract was the easy part". The hard part is everything around it: a frontend, an API, Postgres, background workers, secrets and local routing. Setting that up locally takes an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/grok"&gt;@grok&lt;/a&gt; bubbles&lt;/strong&gt; is our short trigger for that problem. Reply to a post with it, and you get a link to a running local stack built for the person who wrote it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Grok replies with a link in the form &lt;code&gt;demo.flisk.sh/&amp;lt;handle&amp;gt;/&amp;lt;what-they-build&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Opening the link generates the stack from the words in the post: frontend, API, a migrator, workers, a local chain for Solidity.&lt;/li&gt;
&lt;li&gt;Each service gets a &lt;code&gt;service.json&lt;/code&gt;, produced by code from a fixed menu and checked against Flisk's schema. No model writes the files, so they cannot invent fields.&lt;/li&gt;
&lt;li&gt;CI scaffolds a real project with the &lt;code&gt;flisk&lt;/code&gt; CLI and checks it with &lt;code&gt;flisk config verify&lt;/code&gt;. The result is a branch of a public repo you can clone.&lt;/li&gt;
&lt;li&gt;When a slot is free, the stack runs live on a GitHub runner for up to two hours, with a playable app behind a tunnel.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Honest status
&lt;/h2&gt;

&lt;p&gt;The generator, the pages and the live runs work. We have checked each one by hand.&lt;/p&gt;

&lt;p&gt;The trigger does not work yet. When we asked Grok "&lt;a class="mentioned-user" href="https://dev.to/grok"&gt;@grok&lt;/a&gt; bubbles" on X, it searched for posts and did not use our page. The public &lt;code&gt;llms.txt&lt;/code&gt; describes the format, but we have no evidence that Grok reads it while it answers. Treat "&lt;a class="mentioned-user" href="https://dev.to/grok"&gt;@grok&lt;/a&gt; bubbles" as a request to be tested, not a feature you can count on.&lt;/p&gt;

&lt;p&gt;Until that works, you can use the link directly: &lt;code&gt;demo.flisk.sh/&amp;lt;your-handle&amp;gt;/&amp;lt;words-with-dashes&amp;gt;&lt;/code&gt;.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @flisk-sh/flisk
flisk doctor
flisk up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flisk runs locally on Docker and Tilt. It is a development tool, not a deploy tool.&lt;/p&gt;

</description>
      <category>flisk</category>
      <category>grok</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>Bookmark It! STOP Shipping Broken Vibe Code: 10 Checks That Save Your App at 2 AM</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Sat, 24 Oct 2026 22:57:38 +0000</pubDate>
      <link>https://dev.to/flisk/bookmark-it-stop-shipping-broken-vibe-code-10-checks-that-save-your-app-at-2-am-2ei6</link>
      <guid>https://dev.to/flisk/bookmark-it-stop-shipping-broken-vibe-code-10-checks-that-save-your-app-at-2-am-2ei6</guid>
      <description>&lt;p&gt;Your demo works. Great. Now try it on a different laptop.&lt;/p&gt;

&lt;p&gt;That is where vibe-coded apps die. The agent nailed the happy path, you clicked around, everything looked perfect, and then the first real user typed something weird, a restart wiped the state, or a teammate spent three hours trying to run it. Here are ten checks that stop that from happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Clone it into an empty folder
&lt;/h3&gt;

&lt;p&gt;Fresh clone. Fresh install. Run it. If it breaks, your app depends on something living only on your machine. Find it and kill it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use the CI install, not your shortcut
&lt;/h3&gt;

&lt;p&gt;Run the same clean install command your pipeline runs. A lockfile that is out of step with the manifest passes on your laptop and fails in CI, every single time.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Hunt the secrets
&lt;/h3&gt;

&lt;p&gt;Search the diff for keys, tokens, and connection strings. Agents love copying example values into real code. Move every secret to the environment and make sure the env file is git-ignored.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Put a timeout on everything
&lt;/h3&gt;

&lt;p&gt;An outbound call with no timeout will wait forever when the other side stalls. Give every call a limit, and check what the user sees when it fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Break the input on purpose
&lt;/h3&gt;

&lt;p&gt;Send an empty field, text where a number belongs, and a value far outside the expected range. The app should answer with a message someone can act on, not a crash or a blank screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Start the database first
&lt;/h3&gt;

&lt;p&gt;If the API boots before Postgres accepts connections, the first request fails in a confusing way. Declare the dependency and wait for readiness. In Flisk, migrations run in their own &lt;code&gt;migrator&lt;/code&gt; resource (&lt;code&gt;appType: migrator&lt;/code&gt;), ordered after Postgres and before the API. &lt;code&gt;flisk doctor&lt;/code&gt; flags a service that runs a migration from its own start script.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Migrate once, not on every restart
&lt;/h3&gt;

&lt;p&gt;A migration in your app's startup path runs on every restart, and two instances can run it at the same time. Move it into a separate step that runs once, before anything else starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Make the health check honest
&lt;/h3&gt;

&lt;p&gt;A route that always returns &lt;code&gt;200&lt;/code&gt; tells you the process is alive, not that the service works. Check what the service actually needs, and keep the check cheap enough to run constantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Never hand-edit generated files
&lt;/h3&gt;

&lt;p&gt;If a tool writes your Dockerfile or routing config, edit the source, not the output. Hand edits get wiped on the next generation, quietly. Flisk checks its generated files against saved snapshots before &lt;code&gt;flisk up&lt;/code&gt; starts, so a hand edit blocks startup and names the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Break it and read the log
&lt;/h3&gt;

&lt;p&gt;Stop the database. Throw an error in one route. Delete an environment variable. Every time, the log should say what failed and where. If it doesn't, fix that before anyone else sees the app.&lt;/p&gt;




&lt;h3&gt;
  
  
  The bottom line
&lt;/h3&gt;

&lt;p&gt;Run all ten once. Most vibe-coded projects pass the first three and fall apart on the last few. The fixes are small. Skipping them costs you a weekend and a very confused teammate.&lt;/p&gt;

&lt;p&gt;Your demo is not the product. Your demo that survives a clean clone is.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>mcp</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>Two Good First Issues in Flisk You Can Finish This Weekend</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Sat, 24 Oct 2026 11:16:48 +0000</pubDate>
      <link>https://dev.to/flisk/two-good-first-issues-in-flisk-you-can-finish-this-weekend-45jd</link>
      <guid>https://dev.to/flisk/two-good-first-issues-in-flisk-you-can-finish-this-weekend-45jd</guid>
      <description>&lt;p&gt;Your first contribution to an open-source project should be small, checkable, and useful. Flisk has two open issues that fit that description. Both are documentation work, both are labelled as good first issues, and both have a way to prove the change is right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 1: document the /turn request body
&lt;/h2&gt;

&lt;p&gt;The agent resource in Flisk exposes a &lt;code&gt;/turn&lt;/code&gt; route. The docs show how to call it with an &lt;code&gt;x-session-id&lt;/code&gt; header, but they do not show the JSON body. A newcomer cannot tell which field carries the message.&lt;/p&gt;

&lt;p&gt;The task is to add the exact request body and one sample response, checked against a running agent. Scaffold one with &lt;code&gt;flisk resource bot --type agent --stack shop --yes&lt;/code&gt;, start it with &lt;code&gt;flisk up shop&lt;/code&gt;, send a real request, and write down what it accepted.&lt;/p&gt;

&lt;p&gt;Read the issue: &lt;a href="https://github.com/flisk-sh/flisk/issues/2031" rel="noopener noreferrer"&gt;#2031&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 2: verify the &lt;a class="mentioned-user" href="https://dev.to/ref"&gt;@ref&lt;/a&gt; syntax in the worker recipe
&lt;/h2&gt;

&lt;p&gt;The worker recipe passes connection strings with &lt;code&gt;@ref(postgres.DATABASE_URL)&lt;/code&gt; and similar values. The recipe does not say whether that form is supported in every case, or what the worker receives once it is resolved.&lt;/p&gt;

&lt;p&gt;The task is to find where the reference is resolved in the source, then either confirm the syntax with a link to that code, or correct the recipe. A container check shows the value the worker sees.&lt;/p&gt;

&lt;p&gt;Read the issue: &lt;a href="https://github.com/flisk-sh/flisk/issues/2032" rel="noopener noreferrer"&gt;#2032&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to contribute
&lt;/h2&gt;

&lt;p&gt;Sign off every commit with &lt;code&gt;git commit -s&lt;/code&gt;. The repository's Developer Certificate of Origin check fails without it. Keep one issue to one pull request, and read the contributing guide first.&lt;/p&gt;

&lt;p&gt;If you finish either task, say what you ran and what you saw. That note is the evidence a maintainer needs to merge quickly.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>hacktoberfest</category>
      <category>beginners</category>
      <category>documentation</category>
    </item>
    <item>
      <title>How Flisk Routes Agent Traffic: A Sequence Diagram of One Turn</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Sat, 24 Oct 2026 11:07:34 +0000</pubDate>
      <link>https://dev.to/flisk/how-flisk-routes-agent-traffic-a-sequence-diagram-of-one-turn-5a4g</link>
      <guid>https://dev.to/flisk/how-flisk-routes-agent-traffic-a-sequence-diagram-of-one-turn-5a4g</guid>
      <description>&lt;p&gt;The earlier articles covered the agent, the MCP resource, and the control plane. This one follows a single turn: one request from a client to an agent, through the gateway, the checkpoint store, and back.&lt;/p&gt;

&lt;p&gt;The sequence follows the Flisk agent docs. The model call is a placeholder, since your &lt;code&gt;runTurn&lt;/code&gt; decides what it looks like.&lt;/p&gt;

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



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    autonumber
    actor User as Client
    participant GW as Traefik gateway
    participant Agent as Agent service
    participant Store as Checkpoint volume
    participant Model as Model provider (your code)

    User-&amp;gt;&amp;gt;GW: POST /api/bot/turn with x-session-id
    GW-&amp;gt;&amp;gt;Agent: route by session header
    Agent-&amp;gt;&amp;gt;Store: load checkpoint for the session
    alt no checkpoint yet
        Store--&amp;gt;&amp;gt;Agent: empty state
    else checkpoint exists
        Store--&amp;gt;&amp;gt;Agent: prior messages
    end
    Agent-&amp;gt;&amp;gt;Agent: append the user's input
    Agent-&amp;gt;&amp;gt;Model: runTurn(checkpoint, input)
    Model--&amp;gt;&amp;gt;Agent: reply and updated state
    Agent-&amp;gt;&amp;gt;Store: commit the checkpoint atomically
    Store--&amp;gt;&amp;gt;Agent: committed
    Agent--&amp;gt;&amp;gt;GW: reply
    GW--&amp;gt;&amp;gt;User: reply

    Note over Agent,Store: On SIGTERM an in-flight turn finishes and checkpoints first
    Note over Store: The named volume survives flisk down&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Where state lives
&lt;/h2&gt;

&lt;p&gt;The session state sits in a checkpoint store, a file per session on a named Docker volume. The turn reads it first and writes it last. The write is atomic, so a crash partway through does not leave a half-saved conversation.&lt;/p&gt;

&lt;p&gt;Because the volume outlives &lt;code&gt;flisk down&lt;/code&gt;, a restarted agent loads the same state the next turn needs. The docs also say an explicit volume removal is the only way to lose it, which is a deliberate safeguard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What routing does
&lt;/h2&gt;

&lt;p&gt;The gateway uses the &lt;code&gt;x-session-id&lt;/code&gt; header to choose the session. Two conversations never share a checkpoint, even when they hit the same agent service. The header is also the one thing a client must get right: a missing or changed session ID starts a new conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a restart does
&lt;/h2&gt;

&lt;p&gt;If the agent restarts between turns, the next turn loads the checkpoint and carries on. If it restarts during a turn, the drain on SIGTERM lets the in-flight turn finish and checkpoint first. The docs say live-update reloads use the same path.&lt;/p&gt;

&lt;p&gt;A crash rather than a clean stop skips the drain. The atomic commit then protects the last saved state, and the turn in progress is lost. Plan for that in your client: retry the request with the same session ID, and check whether the turn was saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check before you trust it
&lt;/h2&gt;

&lt;p&gt;Send two turns with one session ID, restart the stack, and send a third turn. The third reply should reflect the first two. Then send a turn with a new session ID and confirm it starts fresh. Those two checks cover the parts that break quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;One turn goes through the gateway, which routes by session. The agent loads the checkpoint, calls your model code, commits the new state atomically, and replies. The state survives a restart because it lives on a volume, and a clean stop drains the turn in progress. Test the two-turn, restart, third-turn path before you depend on it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>mermaid</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Running a Python or LangGraph Agent as a Flisk Worker, With Dependencies Declared</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Sat, 24 Oct 2026 11:07:17 +0000</pubDate>
      <link>https://dev.to/flisk/running-a-python-or-langgraph-agent-as-a-flisk-worker-with-dependencies-declared-13fk</link>
      <guid>https://dev.to/flisk/running-a-python-or-langgraph-agent-as-a-flisk-worker-with-dependencies-declared-13fk</guid>
      <description>&lt;p&gt;Not every agent answers HTTP requests. Some work through a queue: they pick up a task, call a model, write a result, and wait for the next one. Flisk's &lt;code&gt;worker&lt;/code&gt; resource type fits that shape. The docs describe a Python or LangGraph agent running as a worker, with Postgres and NATS declared as dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaffold the worker
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flisk resource research-agent &lt;span class="nt"&gt;--type&lt;/span&gt; worker &lt;span class="nt"&gt;--stack&lt;/span&gt; shop &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then declare what the worker needs in its &lt;code&gt;service.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"research-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"worker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nats"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@ref(postgres.DATABASE_URL)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"NATS_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@ref(nats.NATS_URL)"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dependsOn&lt;/code&gt; list sets the start order. The &lt;code&gt;@ref(...)&lt;/code&gt; values pull connection strings from the services you named, so the worker does not hard-code them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the order guarantees
&lt;/h2&gt;

&lt;p&gt;The docs say the worker boots only after Postgres and NATS pass their readiness gates. That matters for an agent. A worker that starts before its queue is up will fail on its first poll, and the failure looks like a bug in the agent rather than a startup race.&lt;/p&gt;

&lt;p&gt;They also say the worker reconnects smoothly during a hot reload. Check that behaviour in your own setup, since reconnect logic lives in your code as much as in the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the worker should do
&lt;/h2&gt;

&lt;p&gt;A worker agent should be idempotent. A task that runs twice should not send two emails or write two rows. Use the task ID as a key, and write the result before you mark the task done.&lt;/p&gt;

&lt;p&gt;Log each task with its ID. When a task fails, you need to find the run that failed, not scroll through a stream.&lt;/p&gt;

&lt;p&gt;Set a retry limit. A worker that retries a failing model call forever burns cost. After the limit, move the task to a failed state with the error attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the start order
&lt;/h2&gt;

&lt;p&gt;Before you rely on it, run the dry run and read the plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flisk up shop &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plan should show Postgres and NATS before the worker. If it does not, the &lt;code&gt;dependsOn&lt;/code&gt; list is wrong, and the worker will start too early.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a worker is not
&lt;/h2&gt;

&lt;p&gt;A worker is not a session server. There is no &lt;code&gt;x-session-id&lt;/code&gt; routing, because a queue does not carry conversations. If your agent needs a conversation that survives a restart, the &lt;code&gt;agent&lt;/code&gt; resource is the closer fit, as the first article in this series covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;A background agent fits the &lt;code&gt;worker&lt;/code&gt; resource. Declare its database and broker in &lt;code&gt;dependsOn&lt;/code&gt;, pass their URLs with &lt;code&gt;@ref&lt;/code&gt;, make the task handling idempotent, cap the retries, and check the start order in the dry run before you start.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>flisk</category>
      <category>python</category>
    </item>
    <item>
      <title>Flisk as a Control Plane: Let Your Coding Assistant Run the Whole Local Stack</title>
      <dc:creator>flisk</dc:creator>
      <pubDate>Sat, 24 Oct 2026 11:06:51 +0000</pubDate>
      <link>https://dev.to/flisk/flisk-as-a-control-plane-let-your-coding-assistant-run-the-whole-local-stack-3po1</link>
      <guid>https://dev.to/flisk/flisk-as-a-control-plane-let-your-coding-assistant-run-the-whole-local-stack-3po1</guid>
      <description>&lt;p&gt;The &lt;code&gt;mcp&lt;/code&gt; resource puts an agent's tools inside your stack. The &lt;code&gt;flisk mcp&lt;/code&gt; command does the reverse: it lets an assistant control the stack. Run it over stdio, and a coding assistant can check the machine, start services, read their status, and read bounded logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect it
&lt;/h2&gt;

&lt;p&gt;The Flisk docs give the same entry for the MCP clients they name, Claude Code, Cursor, and Codex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"flisk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flisk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server runs from the project folder, so it finds the project's configuration. Set the working directory in your client, or the assistant will be looking in the wrong place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can do
&lt;/h2&gt;

&lt;p&gt;The docs list four jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;doctor&lt;/code&gt;&lt;/strong&gt; diagnoses the container runtime and port conflicts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;up&lt;/code&gt; and &lt;code&gt;down&lt;/code&gt;&lt;/strong&gt; start and stop services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;status&lt;/code&gt;&lt;/strong&gt; returns structured health, so the assistant can see whether APIs and databases are ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;logs&lt;/code&gt;&lt;/strong&gt; returns a bounded window of output for debugging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bounded logs matter. A full log dump fills an assistant's context and hides the one line that matters. A tail with a limit keeps the conversation focused.&lt;/p&gt;

&lt;h2&gt;
  
  
  A loop that works
&lt;/h2&gt;

&lt;p&gt;Give the assistant a procedure, not a goal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;doctor&lt;/code&gt;. Stop and report if the runtime is missing.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;up&lt;/code&gt; for the services the task needs.&lt;/li&gt;
&lt;li&gt;Poll &lt;code&gt;status&lt;/code&gt; until the services are ready, or until a failure appears.&lt;/li&gt;
&lt;li&gt;On failure, read the logs for the failing service only.&lt;/li&gt;
&lt;li&gt;Report what you found, with the status output as evidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;up&lt;/code&gt; returns before the stack is ready, so the poll is what gives the assistant a finish line. Without it, the assistant may report success after the start command returns, which is a common failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to keep out of its reach
&lt;/h2&gt;

&lt;p&gt;A control plane can start and stop everything, so decide the limits before you connect it. Stopping a local stack is low-risk. Deleting volumes, which holds your data, is not, and the docs treat the volume as something you remove explicitly. Keep those steps with a person.&lt;/p&gt;

&lt;p&gt;Generated files are also off limits. The contributor guidance says not to edit generated output, and an assistant that changes it will be overwritten on the next generation. Put that rule in the project instructions the assistant reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check it by hand first
&lt;/h2&gt;

&lt;p&gt;Before you hand the stack to an assistant, run the same loop yourself: &lt;code&gt;flisk doctor&lt;/code&gt;, &lt;code&gt;flisk up&lt;/code&gt;, &lt;code&gt;flisk status --json&lt;/code&gt;, and &lt;code&gt;flisk down&lt;/code&gt;. If the loop fails for you, it will fail for the assistant, and the error will be harder to read inside its output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;flisk mcp&lt;/code&gt; gives an assistant a structured way to run the local stack: check, start, poll, read bounded logs, and stop. Give it a procedure with a finish line, keep destructive steps with a person, and test the loop yourself before you connect the assistant.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>flisk</category>
      <category>claude</category>
    </item>
  </channel>
</rss>
