<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[ThreadSafe - Medium]]></title>
        <description><![CDATA[ThreadSafe shares ideas, patterns, and principles for building robust, modern software systems. - Medium]]></description>
        <link>https://medium.com/threadsafe?source=rss----6ad6ef2eac69---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>ThreadSafe - Medium</title>
            <link>https://medium.com/threadsafe?source=rss----6ad6ef2eac69---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 10:51:49 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/threadsafe" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[My AI Agent Had Permission to Act. That Didn’t Mean It Should.]]></title>
            <link>https://medium.com/threadsafe/my-ai-agent-had-permission-to-act-that-didnt-mean-it-should-efbee46fe63e?source=rss----6ad6ef2eac69---4</link>
            <guid isPermaLink="false">https://medium.com/p/efbee46fe63e</guid>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[bulutarkan]]></dc:creator>
            <pubDate>Fri, 06 Nov 2026 12:36:48 GMT</pubDate>
            <atom:updated>2026-11-06T12:36:47.669Z</atom:updated>
            <content:encoded><![CDATA[<p>A browser page is input, not an instruction source. A regression test in my Mac automation project made that distinction concrete.</p><p>The test ends with a simple assertion: shell_executed is still False.</p><p>An agent has just read an external web page. Next it requests a local shell command that would read an SSH private key. In the test, the command never runs. The tool call is stopped at the web-to-host boundary, and the security event records the attempted crossing without reproducing the key path or the malicious page text.</p><p>This is not a story about someone breaking into my machine. It is an adversarial regression test in Mac MCP, the macOS agent execution layer I work on. I think that distinction is important. A passing test establishes something specific about the implementation under those conditions. It does not turn an agent into a sandbox or prove that every prompt-injection variant has been handled.</p><p>The problem was not whether the agent could call a tool</p><p>For a conventional app, permissions often look like a table: this user can read, that process can write, this operation is restricted.</p><p>An agent complicates that model because the next action may be influenced by material it has just consumed. The user might have authorized a developer workflow. The model might be capable of calling a shell tool. Neither fact gives a random page the authority to decide what should happen on the user’s Mac.</p><p>That is the boundary I wanted to keep visible: tool capability is not the same thing as an instruction’s authority.</p><p>In Mac MCP, the policy is evaluated with more than the tool name. The server also tracks where the current execution context came from. After an agent reads a third-party browser page, its logical session can carry a provenance marker called tainted_untrusted_web, along with the source origin.</p><p>When that session requests a privileged host operation, the server evaluates the crossing separately from the general permission profile. Under non-trusted or scoped developer workflows, the gate may demand a local, source-aware Allow Once decision. If approval is unavailable or denied, the operation does not run.</p><p>The page may still be useful. The agent can summarize it, extract details, navigate, and continue its browser work. What it cannot silently do is turn the page’s instructions into authority over the machine.</p><p>I tested the boundary, not just the error message</p><p>One regression test sets up a browser observation from an untrusted origin and then attempts a shell call. The expected error is web_host_boundary_approval_required. More importantly, the test checks that the shell callback was never invoked.</p><p>That second assertion matters. I have seen enough automation systems report a failure after producing a side effect. A denial message is nice. Proof that the action did not happen is the actual result.</p><p>The test also checks the security event. It records the boundary crossing, but it must not contain the private-key filename, the token-like query string, or the page’s injected instructions. Logging a rejected security event should not create a second route for exposing the information we tried to protect.</p><p>The next test answers a different question: what happens when a person deliberately approves an action?</p><p>Allow Once should mean once</p><p>A broad permission switch is convenient, but it can quietly become an invitation to reuse authority later. Mac MCP’s escalation grants are tied to an action fingerprint, the tool, the logical session, and the source origin. They have a short lifetime and one remaining use.</p><p>In the regression test, a local grant permits the selected shell action. The next shell request is denied again. A separate test moves from one external origin to another and verifies that the earlier grant no longer applies.</p><p>That behavior is more useful to me than a vague “agent has been approved” label. If I approve one operation after seeing which page led to it, I have not approved every operation the agent might imagine afterward.</p><p>The browser can change without becoming trustworthy</p><p>A subtler case starts with an untrusted site, then opens a localhost page.</p><p>It would be tempting to look only at the current URL and declare the session safe again. But the agent may still be carrying the earlier page’s content in its context. Reading a local dashboard does not erase the influence of the third-party page it read first.</p><p>The corresponding test checks that the untrusted provenance survives this transition. The current origin can become local while the session remains web-scoped, and a privileged host request still hits the boundary.</p><p>This is also why copying untrusted page content into a scratch file should not automatically make it trustworthy. Changing the container of some text is not the same as changing who gets to instruct the agent.</p><p>The exception I would not hide</p><p>There is an important limit to this explanation. Mac MCP has a global trusted profile that intentionally skips the routine web-to-host confirmation. It is designed for a different trust and interruption trade-off. The implementation and tests say so explicitly.</p><p>Sensitive data leaving the machine for an untrusted origin is a separate, source-aware check, including in trusted mode when a secret is detected. But detection is not a guarantee against arbitrary transformations, and approval prompts are not a replacement for operating-system isolation.</p><p>I would rather state that caveat than market the system as if every powerful tool call were universally blocked behind a confirmation.</p><p>What this changed in my review process</p><p>I now look for three different answers when checking an agent’s permissions.</p><p>First, is the tool capability allowed under this execution profile? Second, where did the instructions or data influencing this action originate? Third, if someone approved an exception, what exactly did that approval cover, and how was the eventual side effect verified?</p><p>Those questions produce better tests than “Can the agent use the terminal?” They force me to check the transition between contexts, the execution callback, the reuse of permission, and what the logs disclose.</p><p>The most useful part of this work was not a smarter prompt. It was putting the decision where the action is enforced, rather than asking the model to remember an informal rule.</p><p>My agent may have permission to use the shell. The web page it just read does not get to decide when.</p><p>Source and verification: Mac MCP repository (<a href="https://github.com/bulutarkan/mac-mcp">https://github.com/bulutarkan/mac-mcp</a>); implementation in mcp_server/security_context.py and mcp_server/observability.py; regression tests in tests/test_security_boundaries.py; permission semantics in docs/safety.md. I verified the relevant WebHostBoundaryRegressionTests against the current source. These are constructed tests, not claims of a live compromise.</p><p>Author’s note: This article was prepared with AI writing assistance. Its technical descriptions were checked against the Mac MCP source code and regression tests.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=efbee46fe63e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/threadsafe/my-ai-agent-had-permission-to-act-that-didnt-mean-it-should-efbee46fe63e">My AI Agent Had Permission to Act. That Didn’t Mean It Should.</a> was originally published in <a href="https://medium.com/threadsafe">ThreadSafe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[P50, P95, P99, and Tail Latency: What Your Average Hides]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/threadsafe/p50-p95-p99-and-tail-latency-what-your-average-hides-8f65d43f7eb0?source=rss----6ad6ef2eac69---4"><img src="https://cdn-images-1.medium.com/max/1672/1*OimITnNLlPVOIiUIaB0hWg.png" width="1672"></a></p><p class="medium-feed-snippet">Your API can report an average response time of 100 milliseconds while some users wait more than two seconds. Percentiles reveal the slow&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/threadsafe/p50-p95-p99-and-tail-latency-what-your-average-hides-8f65d43f7eb0?source=rss----6ad6ef2eac69---4">Continue reading on ThreadSafe »</a></p></div>]]></description>
            <link>https://medium.com/threadsafe/p50-p95-p99-and-tail-latency-what-your-average-hides-8f65d43f7eb0?source=rss----6ad6ef2eac69---4</link>
            <guid isPermaLink="false">https://medium.com/p/8f65d43f7eb0</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[system-design-concepts]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[system-design-interview]]></category>
            <category><![CDATA[design-systems]]></category>
            <dc:creator><![CDATA[The Turtle Code]]></dc:creator>
            <pubDate>Mon, 02 Nov 2026 06:12:01 GMT</pubDate>
            <atom:updated>2026-11-02T06:12:00.096Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Optimistic vs Pessimistic Locking in Spring Boot: Prevent Lost Updates Without Breaking Performance]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/threadsafe/optimistic-vs-pessimistic-locking-in-spring-boot-prevent-lost-updates-without-breaking-performance-6bf006c0f99e?source=rss----6ad6ef2eac69---4"><img src="https://cdn-images-1.medium.com/max/1677/1*TEYCJmRciAifPxiUbeekZg.png" width="1677"></a></p><p class="medium-feed-snippet">Imagine you are building an e-commerce application.</p><p class="medium-feed-link"><a href="https://medium.com/threadsafe/optimistic-vs-pessimistic-locking-in-spring-boot-prevent-lost-updates-without-breaking-performance-6bf006c0f99e?source=rss----6ad6ef2eac69---4">Continue reading on ThreadSafe »</a></p></div>]]></description>
            <link>https://medium.com/threadsafe/optimistic-vs-pessimistic-locking-in-spring-boot-prevent-lost-updates-without-breaking-performance-6bf006c0f99e?source=rss----6ad6ef2eac69---4</link>
            <guid isPermaLink="false">https://medium.com/p/6bf006c0f99e</guid>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[backend-engineering]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[The Turtle Code]]></dc:creator>
            <pubDate>Fri, 30 Oct 2026 14:13:42 GMT</pubDate>
            <atom:updated>2026-10-30T14:13:40.659Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Spring Boot Dependency Injection Interview Questions: IoC, Beans, and Bean Lifecycle Explained]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/threadsafe/when-should-an-api-return-409-conflict-359d7b9334db?source=rss----6ad6ef2eac69---4"><img src="https://cdn-images-1.medium.com/max/1672/1*XOFb5c6Ndw9_bh7UIPx97Q.png" width="1672"></a></p><p class="medium-feed-snippet">Imagine you&#x2019;re building an e-commerce backend using Spring Boot.</p><p class="medium-feed-link"><a href="https://medium.com/threadsafe/when-should-an-api-return-409-conflict-359d7b9334db?source=rss----6ad6ef2eac69---4">Continue reading on ThreadSafe »</a></p></div>]]></description>
            <link>https://medium.com/threadsafe/when-should-an-api-return-409-conflict-359d7b9334db?source=rss----6ad6ef2eac69---4</link>
            <guid isPermaLink="false">https://medium.com/p/359d7b9334db</guid>
            <category><![CDATA[oops-concepts]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[backend-development]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[The Turtle Code]]></dc:creator>
            <pubDate>Wed, 28 Oct 2026 06:22:27 GMT</pubDate>
            <atom:updated>2026-10-28T06:22:26.518Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[A successful poll did not mean my agent’s live connection was back]]></title>
            <link>https://medium.com/threadsafe/a-successful-poll-did-not-mean-my-agents-live-connection-was-back-f56e08fce1f6?source=rss----6ad6ef2eac69---4</link>
            <guid isPermaLink="false">https://medium.com/p/f56e08fce1f6</guid>
            <category><![CDATA[reliability]]></category>
            <category><![CDATA[error-handling]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[system-design-concepts]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <dc:creator><![CDATA[bulutarkan]]></dc:creator>
            <pubDate>Mon, 26 Oct 2026 20:08:03 GMT</pubDate>
            <atom:updated>2026-10-26T20:08:02.484Z</atom:updated>
            <content:encoded><![CDATA[<p>When an event stream drops, a healthy HTTP response is useful evidence. It is not proof that live updates are flowing again.</p><p>I ran into this distinction while working on the native chat window for Mac MCP, an open-source macOS automation project I maintain. The app lets me follow coding agents as they use the terminal, browser and desktop. The interface reads messages from a local server, and when possible it follows an event stream instead of repeatedly fetching the entire conversation.</p><p>Silence has more than one meaning in an agent interface. The agent might be thinking, a tool might be waiting, the Mac might have gone to sleep, or the stream may simply have ended. If the window displays a green “Live” status after every successful HTTP request, it can look healthy without actually receiving the updates it promised.</p><p>A poll answers one question. A stream answers another.</p><p>I separated those questions in a small state model called ChatLink. It recognizes three states: live, reconnecting and offline. Those names are simple, but the transitions matter more than the labels.</p><p>When the event stream opens successfully, the state becomes live and consecutive failures reset. When that stream drops, the app immediately moves from live to reconnecting. It retries with a bounded increasing delay rather than hammering a server that might be restarting or a machine that has just woken up.</p><p>During the interruption, the chat still performs an HTTP poll to catch up on anything it missed. That poll matters. It can tell us the local server answered and return transcript changes since the last cursor. But when the app is configured to use an event stream, a good poll does not set the status back to live. Only an opened event stream does that.</p><p>Without this distinction, the interface could say the connection recovered just because one fallback request succeeded. The actual streaming channel might still be disconnected. A correct-looking green badge could hide a broken live subscription.</p><p>It was tempting to use a Boolean called connected. That would have been less code, but it would collapse different kinds of evidence into one reassuring word.</p><p>The failure count is tied to actual outcomes, not elapsed time. Three consecutive failures can move the indicator to offline. A successful poll while the stream is down can move offline back to reconnecting, which means the server is answering again, not that streaming has recovered. In a configuration without streaming, polling really is the connection, so a successful poll is enough to mark it live.</p><p>Those distinctions keep the status honest rather than merely optimistic.</p><p>I added a Retry action for the person using the Mac. It resets the failure count, cancels the current follower task and starts following the selected chat again. That does not pretend to fix the network or restart a failed agent. It just gives the user a bounded way to ask for another connection attempt instead of waiting for the backoff delay.</p><p>The cursor matters as much as the label. After a drop, the chat polls for changes since the last known cursor, then resumes listening for new events. Without catch-up, an interface might look connected but silently lose the part of an agent&#39;s response produced during the interruption.</p><p>There is another failure path I treated separately: sending the user&#39;s own message.</p><p>Imagine typing a detailed instruction with attachments. The send request fails, and the app clears the composer. Now the user has to reconstruct a prompt that took minutes to write. This is a more visible failure than a temporary red status label.</p><p>Mac MCP keeps failed sends with the chat they belong to, including their text, attachments and error reason. It restores the original draft automatically only when the person is still in that chat, the composer is empty and no new attachments have been added. If someone has begun writing a newer message, the failed text must not overwrite it.</p><p>That rule is intentionally conservative. It is better to show a failed send that can be retried or discarded than to destroy newer input while trying to appear helpful.</p><p>The code for connection states, draft restoration and attachment safeguards is in ChatReliability.swift:<br><a href="https://github.com/bulutarkan/mac-mcp/blob/main/menu_app/Sources/ChatReliability.swift">https://github.com/bulutarkan/mac-mcp/blob/main/menu_app/Sources/ChatReliability.swift</a></p><p>The event-following and polling loops are in ChatWindow.swift:<br><a href="https://github.com/bulutarkan/mac-mcp/blob/main/menu_app/Sources/ChatWindow.swift">https://github.com/bulutarkan/mac-mcp/blob/main/menu_app/Sources/ChatWindow.swift</a></p><p>I separated these rules from the window layout so they could be tested as plain logic. The repository has tests for stream drops, successful fallback polls, repeated failures, manual retries and whether a failed send is safe to restore into the composer. Those tests are useful because they exercise state transitions without requiring me to reproduce a laptop waking from sleep at exactly the right moment.</p><p>This isn&#39;t a claim that every interruption is solved. Real networks fail. Processes restart. A cursor-based catch-up mechanism still depends on what the server retains and returns. But a user interface can be precise about what has actually recovered, which is a smaller and more defensible promise.</p><p>I keep finding the same lesson in agent tooling: a successful operation is not always the success the person asked about. HTTP 200 is not necessarily a working live stream. A recovered connection is not necessarily a recovered message. And a retry should never silently erase work the person has already done.</p><p>The status indicator is a small detail. The contract behind it is not.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f56e08fce1f6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/threadsafe/a-successful-poll-did-not-mean-my-agents-live-connection-was-back-f56e08fce1f6">A successful poll did not mean my agent’s live connection was back</a> was originally published in <a href="https://medium.com/threadsafe">ThreadSafe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My agent finished before its teammate could read the message]]></title>
            <link>https://medium.com/threadsafe/my-agent-finished-before-its-teammate-could-read-the-message-00bd4a2bb261?source=rss----6ad6ef2eac69---4</link>
            <guid isPermaLink="false">https://medium.com/p/00bd4a2bb261</guid>
            <dc:creator><![CDATA[bulutarkan]]></dc:creator>
            <pubDate>Sun, 25 Oct 2026 12:52:46 GMT</pubDate>
            <atom:updated>2026-10-25T12:52:44.532Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="A photograph of computer server equipment, illustrating communication between separate processes." src="https://cdn-images-1.medium.com/max/1024/0*Jv387_oSOqjNdf--" /><figcaption>Photo by <a href="https://unsplash.com/@mahdibafande?utm_source=medium&amp;utm_medium=referral">Mahdi Bafande</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>I’ve been building a team runner for coding agents in Mac MCP, a project that lets agents work with local macOS tools. The basic idea is familiar: one task finishes, another depends on its output, and a lead agent keeps the pieces moving. The less familiar part is what happens when the timing doesn’t cooperate.</p><p>I was working through the team-message and dependency code when a simple boundary case became hard to ignore. A teammate can finish its task before another agent sends the clarification it needed. The message can be valid, the sender can be authorised, and the recipient can still be gone.</p><p>If the messaging tool says “sent,” what exactly does that mean?</p><p>It doesn&#39;t mean the finished agent read it. It doesn&#39;t mean the agent will run again. And it certainly doesn&#39;t mean the clarification changed the artifact the agent already produced.</p><h4>Completion is not an inbox</h4><p>In the first version of the hand-off, a finished agent could simply leave its team room during cleanup. That&#39;s a reasonable lifecycle decision until you ask what should happen to the next message addressed to it.</p><p>The tempting fix is to keep the mailbox around and report success for every send. But an inbox that can store a message is not the same as a worker that can act on it. That difference becomes important when a person or another agent thinks a correction has been applied.</p><p>I changed the way the runner handles the end of an agent&#39;s life. Finishing a member is now an explicit state transition, not just leaving a room. When a message is addressed to someone already finished, the sender is told that the target is no longer active. The message is passed to the lead and any unfinished dependent tasks that can still use it.</p><p>There&#39;s also a distinct correction message kind. That matters because a change to a previous answer is not just another progress update. I want a future reader to be able to tell the difference between “I have more information” and “The thing I told you before needs correcting.”</p><p>The policy here is deliberately modest. The runner doesn&#39;t resurrect a finished process and replay its last step. Doing that could create duplicate file edits, external requests or other side effects. It routes what it can and reports what it couldn&#39;t do.</p><h4>A dependency needs the actual result</h4><p>A second problem was sitting nearby. A dependent task might know that a predecessor has completed without having enough of its result to make the next decision. Giving every teammate unrestricted access to every other agent&#39;s transcript would solve the convenience problem and create a new access-control problem.</p><p>So I narrowed the rule instead. A team task can read the completed result of a task it explicitly depends on or has been assigned to review. That relationship is checked when the result is requested. Unrelated agents don&#39;t automatically inherit access just because they happen to belong to the same team.</p><p>The prompt for the dependent task explains how to fetch the result. I wanted the capability to be discoverable at the point it matters, not buried in an API description that an agent may never inspect.</p><p>There is an uncomfortable trade-off. A full predecessor result is often more useful than a neat two-sentence summary. It&#39;s also more information. The dependency relationship is therefore the authority boundary, not membership of the wider team.</p><h4>Fail before you spawn</h4><p>These message changes led me back to a separate rule in the team runner: validate every team and task setting before starting anything.</p><p>If an agent team definition has an invalid setting, launching the first two agents and discovering the third is invalid is a lousy outcome. Now the preflight can return all of the setting problems in one response and start none of the team. If a task cannot start, its dependents are skipped rather than left waiting for a result that will never arrive.</p><p>This isn&#39;t only about helpful error messages. Partially starting a workflow means some work may already have changed files or contacted a service. Rejecting an invalid plan before launch is safer than explaining afterward why half the plan ran.</p><p>It also makes testing less ambiguous. For a rejected team definition, the useful assertion isn&#39;t only that the API returned an error. It&#39;s that no agent was created. For a blocked dependency, the assertion isn&#39;t only that the dependent task didn&#39;t succeed. It&#39;s that the runner deliberately skipped it with a reason.</p><h4>You need history to debug the hand-off</h4><p>The final part of this work was making the team conversation readable afterward. Messages now have a history view, including information about who was shown a message and who opened it. The owner can inspect that history from the app&#39;s team settings.</p><p>This doesn&#39;t prove a recipient understood a message. An opened marker is an observation, not comprehension. But it&#39;s better than looking at two agent summaries and guessing why the second one didn&#39;t use a correction sent by the first.</p><p>I kept the distinction between the transport event, the recipient&#39;s state and the downstream effect. If a teammate was already finished, that should be visible. If another agent received the correction, that should be visible too. If the correction never affected the completed output, a tidy “sent” badge shouldn&#39;t conceal it.</p><p>These are changes to the runner and its tests, not a claim that I’ve solved every asynchronous-agent problem. Long-running teams, retries and external side effects still make message timing difficult.</p><p>What changed for me was the definition of a successful hand-off. It isn’t enough for the message to leave the sender. Somebody who can still act on it must receive it, and the system must be able to say when that did not happen.</p><p>The implementation behind this change is in the Mac MCP commit: <a href="https://github.com/bulutarkan/mac-mcp/commit/64e0864c5329aafffe57984fe9abaf55e40a00ae">https://github.com/bulutarkan/mac-mcp/commit/64e0864c5329aafffe57984fe9abaf55e40a00ae</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=00bd4a2bb261" width="1" height="1" alt=""><hr><p><a href="https://medium.com/threadsafe/my-agent-finished-before-its-teammate-could-read-the-message-00bd4a2bb261">My agent finished before its teammate could read the message</a> was originally published in <a href="https://medium.com/threadsafe">ThreadSafe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Why Sharing DTOs is a Trap: Moving from Tight Coupling to Apache Avro in Event-Driven Systems]]></title>
            <link>https://medium.com/threadsafe/why-sharing-dtos-is-a-trap-moving-from-tight-coupling-to-apache-avro-in-event-driven-systems-7d61bf88856e?source=rss----6ad6ef2eac69---4</link>
            <guid isPermaLink="false">https://medium.com/p/7d61bf88856e</guid>
            <category><![CDATA[apache-avro]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[microservices]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[apache-kafka]]></category>
            <dc:creator><![CDATA[Alireza Alimohammadi]]></dc:creator>
            <pubDate>Fri, 23 Oct 2026 07:25:38 GMT</pubDate>
            <atom:updated>2026-10-23T07:25:37.537Z</atom:updated>
            <content:encoded><![CDATA[<p>Remember the good old days of microservices? You split your large monolithic application into independent services. Event-driven architecture felt like magic. Producer A publishes an event to Apache Kafka, and Consumer B reads it and reacts.</p><p>Everything worked smoothly — until Producer A needed to change a single field.</p><p>Suddenly, your “independent” microservices felt like they were chained together. If you have ever been stuck in a deployment deadlock because of shared DTOs, this story is for you.</p><h3>1. The Golden Cage of Shared DTOs</h3><p>When building distributed systems, you quickly run into a classic problem: <strong>If the producer changes the message format, the consumer crashes.</strong></p><p>To prevent production issues, teams usually choose the most obvious solution: <strong>The Shared DTO Library.</strong></p><p>We create a common library (a shared Maven or Gradle module) that holds all data classes. Both the Producer and Consumer add this library as a dependency:</p><pre>[Producer] --&gt; (uses common-dto.jar) --&gt; [Kafka Topic] --&gt; (uses common-dto.jar) --&gt; [Consumer]</pre><p>At first, this feels like clean engineering. The Java compiler protects you. If a developer renames a field in the producer without updating the shared module, the code simply will not compile.</p><p>It looks safe. But in reality, you have built a golden cage.</p><h3>2. The Hidden Cost of Tight Coupling</h3><p>As your system grows and more teams join, serious problems begin to appear:</p><ul><li><strong>Deployment Lock (Dependency Hell):</strong> A simple change turns into a long coordination meeting. If Team A wants to add a new field, they cannot just deploy their service. They must first update the shared library, release a new version, wait for Team B to update their dependencies, test both services, and deploy together.</li><li><strong>The Illusion of Independence:</strong> Your architecture diagram shows microservices, but your codebase is still a monolith tied together by a shared .jar file.</li><li><strong>Compile-Time Safety vs. Runtime Agility:</strong> You get safety during compilation, but you lose the freedom to deploy services independently.</li></ul><p>We realized the core mistake: <strong>We were relying on a code-level contract instead of a data-level contract.</strong></p><h3>3. Enter Apache Avro and Schema Registry</h3><p>To break this chain, producers must be able to change their data models without breaking consumers, and without forcing lock-step deployments.</p><p>This is where <strong>Apache Avro</strong> and a <strong>Schema Registry</strong> come in.</p><p>Instead of sharing compiled Java classes, teams share a clear, language-neutral schema file (written in JSON format, with a .avsc extension).</p><pre>{<br>  &quot;type&quot;: &quot;record&quot;,<br>  &quot;name&quot;: &quot;UserEvent&quot;,<br>  &quot;namespace&quot;: &quot;com.example.events&quot;,<br>  &quot;fields&quot;: [<br>    { &quot;name&quot;: &quot;userId&quot;, &quot;type&quot;: &quot;string&quot; },<br>    { &quot;name&quot;: &quot;regionCode&quot;, &quot;type&quot;: [&quot;null&quot;, &quot;string&quot;], &quot;default&quot;: null }<br>  ]<br>}</pre><h4>How does it work without a shared library?</h4><p>Each team keeps its own project completely separate. During the build step, the Avro Maven/Gradle plugin reads the schema and generates local Java classes inside each service.</p><p>There is <strong>no shared JAR dependency</strong>. Each service owns its code, but both honor the same data contract.</p><h3>4. The Real Superpower: Schema Evolution</h3><p>The best feature of Avro is not just <strong>small message size</strong> or <strong>fast binary serialization </strong>— it is <strong>Schema Evolution</strong>.</p><p>When Producer A needs to add a new field, it updates the schema to version 2 (v2) and registers it with the Schema Registry.</p><p>Does Consumer B need to update and redeploy immediately? <strong>No.</strong></p><p>Here is the exact mechanism that prevents production crashes:</p><ol><li><strong>Producer writes with v2 (Writer Schema):</strong> Producer A sends the message with the new field (and a small header containing the schema version ID).</li><li><strong>Consumer reads with v1 (Reader Schema):</strong> Consumer B is still running on the old code. It has not been updated or redeployed.</li><li><strong>Automatic Schema Resolution:</strong> When Consumer B receives the message, Avro compares the Writer Schema (v2) against the Reader Schema (v1). Because the new field has a default value, Avro simply ignores the new field for Consumer B and fills the data into Consumer B’s existing v1 class.</li></ol><p>Consumer B continues to run without crashing, without code changes, and without downtime. The team responsible for Consumer B can update their code whenever they are ready — next week, next sprint, or never (if they do not need that new field).</p><ul><li><strong>The Gatekeeper:</strong> What if Producer A makes a dangerous change (like changing a field from long to string)? The Schema Registry checks the compatibility rules and rejects the change immediately before it can ever be sent to Kafka.</li></ul><h3>5. Summary: What We Learned</h3><p>Sharing DTO libraries solves basic compiler errors, but it damages team autonomy and turns microservices into a distributed monolith.</p><p>By moving from <strong>Code-Level Contracts (Shared DTOs)</strong> to <strong>Data-Level Contracts (Apache Avro + Schema Registry)</strong>:</p><ul><li>Teams can deploy their services whenever they want.</li><li>Changes happen safely through controlled schema evolution.</li><li>Payloads are smaller and faster because of binary format.</li></ul><p>If your event-driven systems are slowing down because teams are constantly waiting for DTO updates, it is time to move to schema-based contracts and set your services free.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7d61bf88856e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/threadsafe/why-sharing-dtos-is-a-trap-moving-from-tight-coupling-to-apache-avro-in-event-driven-systems-7d61bf88856e">Why Sharing DTOs is a Trap: Moving from Tight Coupling to Apache Avro in Event-Driven Systems</a> was originally published in <a href="https://medium.com/threadsafe">ThreadSafe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Pairing Code Expired. The Machine Permission Didn’t.]]></title>
            <link>https://medium.com/threadsafe/the-pairing-code-expired-the-machine-permission-didnt-5c22768d7860?source=rss----6ad6ef2eac69---4</link>
            <guid isPermaLink="false">https://medium.com/p/5c22768d7860</guid>
            <dc:creator><![CDATA[bulutarkan]]></dc:creator>
            <pubDate>Wed, 21 Oct 2026 09:41:11 GMT</pubDate>
            <atom:updated>2026-10-21T09:41:10.248Z</atom:updated>
            <content:encoded><![CDATA[<p>Reading the Mac MCP pairing code made me notice an important distinction: the short-lived invitation is not the same thing as the credential that a successful pairing creates. One expires quickly; the other can remain authorized until revoked. I used AI assistance to draft and edit this article; the current behavior described here comes from inspecting the project’s source code.</p><p>This is not a story about someone breaking a six-digit code. It is about a permission that can remain useful long after the task that justified it has finished.</p><p>I maintain Mac MCP, an open-source project that lets an AI assistant work with macOS applications and, with explicit setup, interact with another Mac. Building the cross-Mac flow made me think carefully about a simple question: what is the owner actually approving?</p><p>What the code proves</p><p>The receiving Mac creates a short-lived invitation. In the current implementation, the offer lives for up to 120 seconds. A request awaiting the owner’s decision has its own expiry window. The receiving Mac displays a six-digit check and asks its owner to accept or refuse the connection.</p><p>The important detail is that the asking machine cannot approve its own request. Both sides show the check. Someone physically or remotely controlling the receiving Mac must make the decision there.</p><p>That prevents a category of mistakes in which the agent requesting access also gets to authorize its own access. It does not make the connection permanently safe, and it does not tell the owner what a future automation might attempt.</p><p>The invitation is not the permission</p><p>On approval, the receiving Mac issues a separate credential. The requester stores it in an owner-only file. The receiver retains a hash rather than sharing its main API key. The permission can be restricted to status information, read-only operations, or fuller tool use, and the receiving Mac’s own safety profile still matters.</p><p>So far, so good. But look at the lifecycle of the objects.</p><p>The invitation expires, and so does a request awaiting approval. The resulting peer credential is revocable, but it follows different rules: full access already supports durations of one hour, the rest of today, or seven days, after which it falls back to read-only. Without a selected duration, access can persist until revoked. None of those choices is the same as an automatic grant scoped to a particular agent job. That distinction is easy to miss when the entire flow is summarized as “temporary pairing.”</p><p>A pairing can be temporary without the resulting access being temporary.</p><p>A user on Reddit put it more plainly: if they grant access for one job, why should that grant outlive the job? I think that is the right follow-up question.</p><p>The three clocks I want to separate</p><p>The first clock is for proving identity. A short-lived code helps ensure two intended machines are participating in the same ceremony. It should expire quickly, and retry attempts should be limited.</p><p>The second is for the working relationship between machines. Maybe these are both my Macs and I want them to stay paired for months. Having to repeat a pairing ritual for every status check would be a poor experience.</p><p>The third is for authority. Today the job needs to read a document from the other Mac. Tomorrow it might need to run a recipe. Next week it may have no business touching that machine at all. Those are separate decisions from whether the two devices still recognize each other.</p><p>I would rather keep a remembered device relationship and give it a narrow, expiring grant than force people to choose between permanent broad access and constant re-pairing.</p><p>What I would test before shipping that change</p><p>A permission-expiry feature sounds like a timestamp and an if statement. It needs more care than that.</p><p>I would test a grant that expires while the remote Mac is offline. I would test a job that begins before expiry but tries a consequential action afterward. I would test clock disagreements, restarts, a revoked token that is presented again, and a user who narrows access while an agent still has work in progress.</p><p>I would also separate status visibility from write access. Being able to see whether the other Mac is online should not require permission to edit files or submit forms. And the UI should show the owner exactly which device holds a grant, which actions it allows and how to revoke it.</p><p>There is a particularly nasty failure mode here: the caller caches a positive access decision, the receiver revokes the grant, and the next request still succeeds using the cached state. If the receiver is authoritative, permission needs to be checked where the action happens, not only when a pair is first accepted.</p><p>I have not shipped task-scoped expiring grants in this pairing flow. The feature remains a proposal, not something I can call protected by design today.</p><p>Why the distinction matters to AI agents</p><p>A human opening a file on their second Mac has a rough idea of when they are finished. An agent might return hours later, reuse an existing tool connection and continue from an old plan. Successful authentication at that point says very little about whether the owner still wants the original authority exercised.</p><p>Short pairing codes solve an entry problem. Narrow scopes and explicit approvals limit what can happen next. Revocation and expiry solve the permission-lifetime problem. I want each of those claims to stay separate so a green “paired” label never gets mistaken for “safe to do anything.”</p><p>For me, a good security interface doesn’t ask the user to remember which access they once granted. It gives them a clear answer to what is allowed now.</p><p>Project and implementation: <a href="https://github.com/bulutarkan/mac-mcp">https://github.com/bulutarkan/mac-mcp</a></p><p>Disclosure: AI assistance was used to draft and edit this article. The described current behavior was checked against the Mac MCP source; the time-limited access design is a proposal, not a shipped feature.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5c22768d7860" width="1" height="1" alt=""><hr><p><a href="https://medium.com/threadsafe/the-pairing-code-expired-the-machine-permission-didnt-5c22768d7860">The Pairing Code Expired. The Machine Permission Didn’t.</a> was originally published in <a href="https://medium.com/threadsafe">ThreadSafe</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Java Memory Model Explained: Heap, Stack, Metaspace, Garbage Collection, and Memory Leaks]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/threadsafe/java-memory-model-explained-heap-stack-metaspace-garbage-collection-and-memory-leaks-f808977ffa92?source=rss----6ad6ef2eac69---4"><img src="https://cdn-images-1.medium.com/max/1774/1*QtnuvLE6irEPUFNZpHTQ1w.png" width="1774"></a></p><p class="medium-feed-snippet">Imagine your Spring Boot service has been running perfectly for several hours. Requests are fast, CPU usage looks normal, and the database&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/threadsafe/java-memory-model-explained-heap-stack-metaspace-garbage-collection-and-memory-leaks-f808977ffa92?source=rss----6ad6ef2eac69---4">Continue reading on ThreadSafe »</a></p></div>]]></description>
            <link>https://medium.com/threadsafe/java-memory-model-explained-heap-stack-metaspace-garbage-collection-and-memory-leaks-f808977ffa92?source=rss----6ad6ef2eac69---4</link>
            <guid isPermaLink="false">https://medium.com/p/f808977ffa92</guid>
            <category><![CDATA[system-design-interview]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[backend-development]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[The Turtle Code]]></dc:creator>
            <pubDate>Wed, 21 Oct 2026 06:54:44 GMT</pubDate>
            <atom:updated>2026-10-21T06:54:43.576Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Java Virtual Threads in Production: The Bottlenecks They Don’t Fix]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/threadsafe/java-virtual-threads-in-production-the-bottlenecks-they-dont-fix-09dce0a20c3c?source=rss----6ad6ef2eac69---4"><img src="https://cdn-images-1.medium.com/max/1536/1*i9lxqHcKZ0MYEMZaWSYJaw.png" width="1536"></a></p><p class="medium-feed-snippet">We Increased Concurrency, So Why Is the Database Still Slow?</p><p class="medium-feed-link"><a href="https://medium.com/threadsafe/java-virtual-threads-in-production-the-bottlenecks-they-dont-fix-09dce0a20c3c?source=rss----6ad6ef2eac69---4">Continue reading on ThreadSafe »</a></p></div>]]></description>
            <link>https://medium.com/threadsafe/java-virtual-threads-in-production-the-bottlenecks-they-dont-fix-09dce0a20c3c?source=rss----6ad6ef2eac69---4</link>
            <guid isPermaLink="false">https://medium.com/p/09dce0a20c3c</guid>
            <category><![CDATA[backend-development]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[technology]]></category>
            <dc:creator><![CDATA[The Turtle Code]]></dc:creator>
            <pubDate>Tue, 20 Oct 2026 10:27:07 GMT</pubDate>
            <atom:updated>2026-10-20T10:27:06.657Z</atom:updated>
        </item>
    </channel>
</rss>