<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Mauro Morales Blog</title>
        <link>https://www.mauromorales.com/posts/</link>
        <description>Mauro Morales Blog</description>
        <lastBuildDate>Sun, 23 Aug 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[Week 3 of Building a Software Factory: Shipping Kairos 4.2.0]]></title>
            <link>https://www.mauromorales.com/posts/shipping-kairos-4-2-0/</link>
            <guid>https://www.mauromorales.com/posts/shipping-kairos-4-2-0/</guid>
            <pubDate>Sun, 23 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[4.2.0 shipped, reset works again on non-UKI systems, fork pull requests finally get CI, and a reviewer agent now reads every change before I do. 34 changes landed against Kairos this week, up from 19.]]></description>
            <content:encoded><![CDATA[<p><a class="" href="https://www.mauromorales.com/posts/putting-the-agents-on-kairos/">Last week</a> ended on a version that had shipped but not released, and a review load I was not keeping up with. Both moved. 4.2.0 is out, reset works again on non-UKI installs, fork pull requests finally get CI, and there is now a reviewer agent reading every change before I do. 34 changes landed against Kairos this week, up from 19.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-shipped">What shipped<a href="https://www.mauromorales.com/posts/shipping-kairos-4-2-0/#what-shipped" class="hash-link" aria-label="Direct link to What shipped" title="Direct link to What shipped" translate="no">​</a></h2>
<p><strong><a href="https://github.com/kairos-io/kairos/releases/tag/v4.2.0" target="_blank" rel="noopener noreferrer" class="">v4.2.0</a>, on 18 August.</strong> The last thing holding it was two advisories the release scan was still ignoring (<a href="https://github.com/kairos-io/kairos/pull/4332" target="_blank" rel="noopener noreferrer" class="">kairos#4332</a>). Cleared, tagged, <a href="https://github.com/kairos-io/kairos-docs/pull/671" target="_blank" rel="noopener noreferrer" class="">announced</a>.</p>
<p><strong>Reset works again on a non-UKI install.</strong> This is the one I would want fixed if I were running Kairos. A state reset on a non-UKI system could come back with the wrong sysroot labels and without its recovery extensions (<a href="https://github.com/kairos-io/immucore/pull/611" target="_blank" rel="noopener noreferrer" class="">immucore#611</a>, <a href="https://github.com/kairos-io/immucore/pull/613" target="_blank" rel="noopener noreferrer" class="">immucore#613</a>), shipped out through <a href="https://github.com/kairos-io/kairos-init/pull/436" target="_blank" rel="noopener noreferrer" class="">kairos-init#436</a>. It is a bug you only meet on the day you actually needed reset to work. It now has a test covering it (<a href="https://github.com/kairos-io/hadron/pull/565" target="_blank" rel="noopener noreferrer" class="">hadron#565</a>) and a lint that fails the build if someone adds a boot state and forgets to handle it (<a href="https://github.com/kairos-io/immucore/pull/614" target="_blank" rel="noopener noreferrer" class="">immucore#614</a>).</p>
<p><strong>A bad <code>config_url</code> no longer eats your cloud-init.</strong> If the kernel cmdline pointed at a config URL that could not be fetched, the agent skipped the local cloud-init files too, so a typo in one place silently dropped your whole configuration. Fixed in <a href="https://github.com/kairos-io/kairos-agent/pull/1344" target="_blank" rel="noopener noreferrer" class="">kairos-agent#1344</a>.</p>
<p><strong>Riscv64 is back to one build path.</strong> Hadron could already build for riscv64, that part isn't news. What hadn't happened was a release: a split native-plus-emulated pipeline for it was slowing down releases for the architectures that matter more right now. <a href="https://github.com/kairos-io/hadron/pull/572" target="_blank" rel="noopener noreferrer" class="">hadron#572</a> drops the native runner and goes back to plain emulation. Slower, but most of it caches, and it gets stability back. Maybe the next release finally includes a riscv64 image.</p>
<p><strong>Fork pull requests get CI.</strong> Before this week, a contribution from a fork got no build at all, which is a bad first experience and a slow one. It builds now, behind a gate a maintainer has to press (<a href="https://github.com/kairos-io/kairos/pull/4323" target="_blank" rel="noopener noreferrer" class="">kairos#4323</a>, extended in <a href="https://github.com/kairos-io/kairos/pull/4328" target="_blank" rel="noopener noreferrer" class="">kairos#4328</a>).</p>
<p><strong>Builds are off ttl.sh, and that's a security fix as much as a tidiness one.</strong> Images used to hop between CI jobs through a public throwaway registry. They now go to quay or move as tarballs between jobs (<a href="https://github.com/kairos-io/hadron/pull/566" target="_blank" rel="noopener noreferrer" class="">hadron#566</a>, <a href="https://github.com/kairos-io/hadron/pull/567" target="_blank" rel="noopener noreferrer" class="">hadron#567</a>, <a href="https://github.com/kairos-io/kairos-init/pull/440" target="_blank" rel="noopener noreferrer" class="">kairos-init#440</a>), and the credential that publishes to the production registry no longer sits in a workflow that runs on code from a pull request, where anyone could put it there (<a href="https://github.com/kairos-io/kairos/pull/4327" target="_blank" rel="noopener noreferrer" class="">kairos#4327</a>). The approach follows what Dimitris Karakasilis (<a href="https://github.com/jimmykarily" target="_blank" rel="noopener noreferrer" class="">jimmykarily</a>) had already worked out for kcrypt-discovery-challenger.</p>
<p><strong>A reviewer that is not me.</strong> Last week I wrote I was leaning toward a separate agent doing a first pass. It exists. Every pull request opens as a draft, a reviewer subagent reads the diff, the ticket and the tests, and deliberately does not read the conversation that produced them, because a reviewer that inherits the author's reasoning is just the author wearing a hat. It sends the change back or moves it to ready for review. On kairos-io it may comment and nothing else. It never merges anything, anywhere. Merge is still mine, in every repository. I reviewed 25 of the 31 changes the agent opened against Kairos this week, compared with 13 of 18 last week. Too early to say if it helps. Ask me next week.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-numbers">The numbers<a href="https://www.mauromorales.com/posts/shipping-kairos-4-2-0/#the-numbers" class="hash-link" aria-label="Direct link to The numbers" title="Direct link to The numbers" translate="no">​</a></h2>
<p>Same caveats as last time. These count both my own account and the agent's.</p>
<table><thead><tr><th>Activity</th><th style="text-align:right">20 Jul – 6 Aug</th><th style="text-align:right">7 – 15 Aug</th><th style="text-align:right">16 – 21 Aug</th></tr></thead><tbody><tr><td>Changes shipped to Kairos</td><td style="text-align:right">0</td><td style="text-align:right">19</td><td style="text-align:right"><strong>34</strong></td></tr><tr><td>Changes to the agent factory itself</td><td style="text-align:right">22</td><td style="text-align:right">26</td><td style="text-align:right">40</td></tr><tr><td>Changes to this website and the homelab</td><td style="text-align:right">9</td><td style="text-align:right">20</td><td style="text-align:right">6</td></tr><tr><td>Architecture decision records written</td><td style="text-align:right">6</td><td style="text-align:right">11</td><td style="text-align:right">4</td></tr><tr><td>Postmortems filed</td><td style="text-align:right">5</td><td style="text-align:right">2</td><td style="text-align:right">4</td></tr><tr><td>Internal board: issues opened / closed</td><td style="text-align:right">20 / 15</td><td style="text-align:right">99 / 33</td><td style="text-align:right">110 / 90</td></tr></tbody></table>
<p>Two rows are worth a sentence. The board went from ninety-nine opened against thirty-three closed to a hundred and ten against ninety. Last week it was filling about three times faster than it drained. It is now roughly keeping pace, and I would rather have that than a smaller number in either column.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-went-wrong">What went wrong<a href="https://www.mauromorales.com/posts/shipping-kairos-4-2-0/#what-went-wrong" class="hash-link" aria-label="Direct link to What went wrong" title="Direct link to What went wrong" translate="no">​</a></h2>
<p>Four postmortems, and the same failure runs through most of them.</p>
<p><strong>A rule nobody checks does not get followed.</strong> A policy about which model to use for which kind of work, cheaper ones for grunt work, better ones for judgment calls, had been sitting in the agents' constitution for a month. Every session reads that file. Its application rate over the month was zero. Not low. Zero. It surfaced by accident, because a ticket happened to ask what model a session was running.</p>
<p>The failure mode is what makes it expensive. The rule does not get broken loudly. It reports success. For a month every session read that policy, understood it, and did not apply it. Nothing anywhere produced a signal. To anyone auditing this later, an unenforced rule in that file looks exactly like an enforced one.</p>
<p>Two smaller versions of the same failure landed the same day, each fixed the same way.</p>
<p>The fix in both cases was moving the text into the shared file every session reads, through a branch and a pull request. That is the whole difference between a preference and a mechanism, and it costs one paragraph. The model policy got a check at the moment a ticket is claimed. A poller that filed thirty-six junk tickets in a minute now refuses a first run over five items and prints the command to clear the backlog. None of the fixes were a better-worded rule.</p>
<p><strong>Two things reported healthy while being dead.</strong> A user-level timer, the one that tells me when the agent is mentioned on GitHub, went dark for eight hours while <code>is-active</code> and <code>is-enabled</code> both said yes. Neither of those answers the question I care about, which is whether it will ever fire again. Separately, leftover VM disk images filled a shared disk quota on the machine all of this runs on, and every command in every concurrent session started failing with a bare exit code pointing nowhere near the cause. Both cost me more hours than the governance ones did, and both are the same lesson wearing different clothes. The check has to check the thing you care about, not the thing that is easy to report.</p>
<p><strong>What I am doing about it.</strong> The fix is boring: an audit of every rule in that constitution, sorted by whether anything except a session reading it carefully makes it true. I already know that audit will find things. The model policy sat there for a month and I found it by accident.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-that-leaves-me">Where that leaves me<a href="https://www.mauromorales.com/posts/shipping-kairos-4-2-0/#where-that-leaves-me" class="hash-link" aria-label="Direct link to Where that leaves me" title="Direct link to Where that leaves me" translate="no">​</a></h2>
<p>A better week on output and a more uncomfortable one on process. The system produces more every week, and the more it produces, the more expensive the gap gets between the rules I have written down and the rules anything actually enforces.</p>
<p>As before: this post was drafted by the system it describes, and I reviewed, edited and merged it myself. If you want to follow along you can subscribe to the <a href="https://www.mauromorales.com/posts/rss.xml" target="_blank" rel="noopener noreferrer" class="">RSS feed</a>, find me on <a href="https://www.linkedin.com/in/mauromorales/" target="_blank" rel="noopener noreferrer" class="">LinkedIn</a> or <a href="https://www.youtube.com/@mauromrls" target="_blank" rel="noopener noreferrer" class="">YouTube</a>, or say hello through my <a class="" href="https://www.mauromorales.com/contact/">contact page</a>.</p>]]></content:encoded>
            <category>Kairos</category>
            <category>Open Source</category>
            <category>AI</category>
            <category>Reflections</category>
        </item>
        <item>
            <title><![CDATA[Week 2 of Building a Software Factory: Putting the Agents on Kairos]]></title>
            <link>https://www.mauromorales.com/posts/putting-the-agents-on-kairos/</link>
            <guid>https://www.mauromorales.com/posts/putting-the-agents-on-kairos/</guid>
            <pubDate>Sat, 15 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Last week I wrote that the next step was pointing my agents at Kairos itself. This is the first week of that, including the release gate I had been putting off, the migration that broke a working setup, and the reviews that are wearing me down.]]></description>
            <content:encoded><![CDATA[<p>Last week I wrote that I wanted to get the agents working on actual Kairos tickets, and that the next post would either have results or another pile of postmortems. I'm happy to say it has results. It has a few postmortems too, but the results came first this time.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-actually-landed">What actually landed<a href="https://www.mauromorales.com/posts/putting-the-agents-on-kairos/#what-actually-landed" class="hash-link" aria-label="Direct link to What actually landed" title="Direct link to What actually landed" translate="no">​</a></h2>
<p>The one I care about most is a release gate.</p>
<p>Kairos releases were not gated on a vulnerability scan of the bundle we actually ship. It is fiddlier than it sounds, it touches release CI, and it is exactly the kind of ticket I put off until it hurts enough to force my hand.</p>
<p>It landed this week in <a href="https://github.com/kairos-io/kairos/pull/4300" target="_blank" rel="noopener noreferrer" class="">kairos#4300</a>, together with clearing the 23 fixable advisories it immediately surfaced. A monthly repository access audit landed too, in <a href="https://github.com/kairos-io/community/pull/31" target="_blank" rel="noopener noreferrer" class="">community#31</a>.</p>
<p>The rest is dependency bumps, CI plumbing and version pins across nine repositories, and honestly that's fantastic. This is maintainer work. Keeping dependencies current, keeping the release pipeline green and keeping the CVE count down <em>is</em> the job, and it's exactly the kind of work you want to delegate. It's also the part that quietly eats the time I'd rather spend elsewhere.</p>
<p>One caveat: shipped is not released. 4.2.0 is still blocked on a separate problem as I write this.</p>
<p>There's another category I nearly left out, because it isn't code. A good chunk of this week was developer relations: conference admin, keeping track of what is worth writing or posting about, the small things that pile up. That work used to happen by hand, in the gaps between other things, and it was the first thing to slip when I got busy. Now it gets a ticket like everything else, and it mostly gets done without me.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-numbers">The numbers<a href="https://www.mauromorales.com/posts/putting-the-agents-on-kairos/#the-numbers" class="hash-link" aria-label="Direct link to The numbers" title="Direct link to The numbers" translate="no">​</a></h2>
<p>Same caveats as last time. These count both my own account and the agent's.</p>
<table><thead><tr><th>Activity</th><th style="text-align:right">20 Jul – 6 Aug</th><th style="text-align:right">7 – 15 Aug</th></tr></thead><tbody><tr><td>Changes shipped to Kairos</td><td style="text-align:right">0</td><td style="text-align:right"><strong>19</strong></td></tr><tr><td>Changes to the agent factory itself</td><td style="text-align:right">22</td><td style="text-align:right">26</td></tr><tr><td>Changes to this website and the homelab</td><td style="text-align:right">9</td><td style="text-align:right">20</td></tr><tr><td>Architecture decision records written</td><td style="text-align:right">6</td><td style="text-align:right">11</td></tr><tr><td>Postmortems filed</td><td style="text-align:right">5</td><td style="text-align:right">2</td></tr><tr><td>Internal board: issues opened / closed</td><td style="text-align:right">20 / 15</td><td style="text-align:right">99 / 33</td></tr></tbody></table>
<p>The first row is the one I'd point at. It was zero when I published the last post, and I said then that anyone comparing tables should read that row first. It's 19 now: 18 pull requests across nine kairos-io repositories, plus one to <a href="https://github.com/mudler/edgevpn" target="_blank" rel="noopener noreferrer" class="">edgevpn</a>, which ships inside every Kairos release.</p>
<p>The last row is my own board, the private one where I file work, not the Kairos issue tracker. Ninety-nine issues opened against thirty-three closed, up from twenty and fifteen. Roughly: 23 about Kairos, 18 about the homelab, 11 about this website, and about 42 about the factory and its rules. So it's filling about three times faster than it drains. I'll come back to why that isn't purely bad.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-broke">What broke<a href="https://www.mauromorales.com/posts/putting-the-agents-on-kairos/#what-broke" class="hash-link" aria-label="Direct link to What broke" title="Direct link to What broke" translate="no">​</a></h2>
<p>Halfway through the week I changed how sessions are organized. They used to be scoped by project: one for Kairos, one for my own repos, one for planning. Now a session is defined by who it acts as and which machine it runs on. They're lanes, and a lane picks up whatever ticket is labeled for it, Kairos one hour and this website the next.</p>
<p>The design is better and I'd do it again. But a lot of things that had been working quietly stopped working the moment it landed, and <strong>nobody warned me.</strong> The agents applied a migration to a running setup without surfacing what it would break. Then, when things did break, I got confident answers about the cause that turned out to be guesses rather than assessments.</p>
<p>A clean example, while writing this post. I asked which of my posts were published. The agent read a local checkout sitting on an old draft branch and gave me a date ten days off, without saying which branch it had read. There was nothing for me to catch it on. That's now a rule: fetch first, read the published branch, say which branch you read.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="old-habits-die-hard">Old habits die hard<a href="https://www.mauromorales.com/posts/putting-the-agents-on-kairos/#old-habits-die-hard" class="hash-link" aria-label="Direct link to Old habits die hard" title="Direct link to Old habits die hard" translate="no">​</a></h2>
<p>I have a talk recording next week, and I handed part of the preparation to Claude. Not through the board, just me in a chat window, the old way. The analysis and the suggestions were good. The slides still need work.</p>
<p>None of it is measured, because none of it went through the system. It won't show up in any of the tables above, and I only noticed afterward.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="this-suits-the-way-i-think">This suits the way I think<a href="https://www.mauromorales.com/posts/putting-the-agents-on-kairos/#this-suits-the-way-i-think" class="hash-link" aria-label="Direct link to This suits the way I think" title="Direct link to This suits the way I think" translate="no">​</a></h2>
<p>Something I didn't anticipate.</p>
<p>My brain jumps from topic to topic. That usually leaves me two options, both bad. I chase the tangent and end up yak shaving three levels deep in something unrelated to what I sat down to do. Or I write it down, and the note joins a backlog that grows until it's unmanageable and I stop opening it.</p>
<p>There's a third option now. I can write the ticket <em>and</em> have someone start on it. The tangent gets captured and worked without me following it.</p>
<p>That reframes the 99 against 33 above. Part of it is a board filling faster than it drains, which is a real problem. Part of it is that writing something down finally has a payoff instead of being deferred guilt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reviews-and-the-thing-that-makes-them-hard">Reviews, and the thing that makes them hard<a href="https://www.mauromorales.com/posts/putting-the-agents-on-kairos/#reviews-and-the-thing-that-makes-them-hard" class="hash-link" aria-label="Direct link to Reviews, and the thing that makes them hard" title="Direct link to Reviews, and the thing that makes them hard" translate="no">​</a></h2>
<p>Everybody says reviewing AI output is the bottleneck, and I couldn't agree more. What I didn't expect is the specific way it fails.</p>
<p>It's about time, obviously. But not only time. When the reviews are long <em>and</em> there are many of them, I stop reading them at the same level of attention. It creeps. I catch myself skimming a diff I would have read line by line two weeks ago, not because I decided it was low risk, but because it's the eleventh one today. The gate is still there on paper. The attention behind it is not.</p>
<p>I have a data point on that. Of the 18 changes that reached kairos-io, I reviewed 13. One merged with no review from me at all: an external maintainer approved and merged it eight seconds apart, on a pull request the agent had opened without the draft flag that exists precisely to keep other maintainers out of the loop until I've looked. Nothing bad shipped, the change was correct, but the gate wasn't there.</p>
<p>Two things make it worse, and they need different fixes. The agents write a lot, and a long explanation is not a better one. I've started asking for concision, which I can do today. The other is throughput, and I don't have an answer: the better the system gets, the more there is to review, so every improvement makes this harder.</p>
<p>What I'm leaning toward is a separate agent doing a first pass, and finding a way to skim that doesn't quietly become rubber-stamping. Whatever I replace line-by-line reading with has to keep the slop out of a project other people depend on. That's the only thing the gate is for.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="more-time-on-my-phone">More time on my phone<a href="https://www.mauromorales.com/posts/putting-the-agents-on-kairos/#more-time-on-my-phone" class="hash-link" aria-label="Direct link to More time on my phone" title="Direct link to More time on my phone" translate="no">​</a></h2>
<p>The last thing, and I'm genuinely of two minds.</p>
<p>I spend more time on my phone now, because that's where the board is. I can go downstairs, make coffee, unblock an agent from the kitchen and come back. That's good for me. Long stretches at my desk give me back pain and make me tense, and this week I got real work done while not sitting at it.</p>
<p>The other side is obvious. Things being on my phone means I'm more likely to look at them when I'd rather not. Reviewing a pull request while I'm with my family isn't a win just because it's efficient. I don't have a rule for this yet. I'm writing it down before it becomes a habit I stop noticing.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-that-leaves-me">Where that leaves me<a href="https://www.mauromorales.com/posts/putting-the-agents-on-kairos/#where-that-leaves-me" class="hash-link" aria-label="Direct link to Where that leaves me" title="Direct link to Where that leaves me" translate="no">​</a></h2>
<p>A good week. The thing I said I'd start, started, and it produced work I actually care about instead of more scaffolding.</p>
<p>As before: this post was drafted by the system it describes, and I reviewed, edited and merged it myself. If you want to follow along you can subscribe to the <a href="https://www.mauromorales.com/posts/rss.xml" target="_blank" rel="noopener noreferrer" class="">RSS feed</a>, find me on <a href="https://www.linkedin.com/in/mauromorales/" target="_blank" rel="noopener noreferrer" class="">LinkedIn</a> or <a href="https://www.youtube.com/@mauromrls" target="_blank" rel="noopener noreferrer" class="">YouTube</a>, or say hello through my <a class="" href="https://www.mauromorales.com/contact/">contact page</a>.</p>]]></content:encoded>
            <category>Kairos</category>
            <category>Open Source</category>
            <category>AI</category>
            <category>Reflections</category>
        </item>
        <item>
            <title><![CDATA[Does the Language Still Matter When AI Writes the Code?]]></title>
            <link>https://www.mauromorales.com/posts/does-the-language-still-matter/</link>
            <guid>https://www.mauromorales.com/posts/does-the-language-still-matter/</guid>
            <pubDate>Sun, 09 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[I picked Ruby because writing it was a pleasure. Now a model writes most of my code and I mostly read it, which makes me wonder whether a language should be designed for the person reviewing it rather than the person typing it.]]></description>
            <content:encoded><![CDATA[<p>I picked up Ruby because of how expressive it was. Writing it just felt nice, in a way that other languages did not. Later I found out that was deliberate: Matz designed Ruby so that programmers would enjoy using it. I then spent the better part of a decade writing Ruby and enjoying it.</p>
<p>These days I am not really writing code. It gets written by AI and I read it. That shift happened faster than I expected, and it has left me with a question I cannot settle.</p>
<p>Does the language still matter?</p>
<p>I do not mean it in the usual sense. Not typed or untyped, not compiled or interpreted, not fast or slow. I mean something narrower: does it matter whether a language was designed for readability and enjoyment?</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-these-two">Why these two<a href="https://www.mauromorales.com/posts/does-the-language-still-matter/#why-these-two" class="hash-link" aria-label="Direct link to Why these two" title="Direct link to Why these two" translate="no">​</a></h2>
<p>Ruby and Go are not obvious rivals. Nobody is picking between them to write a Kubernetes controller this afternoon. But they bracket a shift I lived through, and they are the two languages I have written most of my code in.</p>
<p>A lot of the early cloud infrastructure was Ruby. Vagrant, BOSH, large parts of Cloud Foundry. I spent a couple of years on the BOSH OpenStack CPI myself. Then Docker and Kubernetes arrived with better answers to the same problems, and Go arrived with them. Cloud Foundry eventually began moving components to Go as well. So this is not an abstract pairing for me. It is the language I came from and the language I ended up in.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="two-ends-of-the-same-table">Two ends of the same table<a href="https://www.mauromorales.com/posts/does-the-language-still-matter/#two-ends-of-the-same-table" class="hash-link" aria-label="Direct link to Two ends of the same table" title="Direct link to Two ends of the same table" translate="no">​</a></h2>
<p>Go is, in my opinion, the opposite of Ruby. I do not think it was made for the programmer to enjoy. It was made to be a lingua franca, something many developers across many projects could pick up quickly. A good enough solution, on purpose.</p>
<p>I am not trying to downplay it. There is real elegance in a lot of Go's design, it is extremely effective, and there are good reasons it became the default in the cloud native space. Its explicitness is a feature, and I rely on it every day. But it feels more like an industrial solution than an artistic expression. Those two languages sit at opposite ends of the same table.</p>
<p>Here is roughly what that looks like. Say you want the names of the nodes that are ready, sorted.</p>
<div class="language-ruby codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ruby codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">def ready_node_names(client)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  client.list_nodes</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        .select(&amp;:ready?)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        .map(&amp;:name)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        .sort</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">end</span><br></span></code></pre></div></div>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">func</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ReadyNodeNames</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx context</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Context</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> c Client</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token builtin">error</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	nodes</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ListNodes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		</span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">nil</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> fmt</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Errorf</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"listing nodes: %w"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	names </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">make</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">len</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">nodes</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">_</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> n </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">range</span><span class="token plain"> nodes </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		</span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> n</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Ready </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">			names </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">append</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">names</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> n</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Name</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	sort</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Strings</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">names</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> names</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">nil</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>I want to be fair about what that comparison shows, because it is easy to rig this kind of example and prove nothing.</p>
<p>The Go version is not unclear. Every line in it is obvious, which is exactly what Go is going for. It also does something the Ruby version does not: it tells you what happens when listing the nodes fails, and it wraps the error with enough context that you will know where it came from. In the Ruby version that failure is invisible. An exception propagates up through code you cannot see from here. If I am reviewing the Go function I know how it behaves when the API is down. If I am reviewing the Ruby one, I have to go and find out.</p>
<p>That is a real trade, and Go's side of it is not boilerplate. It is the part I actually want to read.</p>
<p>The rest is. The <code>make</code> with a capacity hint, the range loop, the append, the second return value threaded through every caller. I have read those exact lines thousands of times. They carry no information specific to this function.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-i-think-it-matters-more-now-not-less">Why I think it matters more now, not less<a href="https://www.mauromorales.com/posts/does-the-language-still-matter/#why-i-think-it-matters-more-now-not-less" class="hash-link" aria-label="Direct link to Why I think it matters more now, not less" title="Direct link to Why I think it matters more now, not less" translate="no">​</a></h2>
<p>Reading that boilerplate was never fun. But it was survivable, because there was only so much code a human could produce in a day. That is no longer the constraint. We can now generate far more code than we used to, and the reading has not gotten any faster.</p>
<p>I do not think I need to control every line anymore, or hold a whole codebase in my head the way I once did. I have made my peace with that. But I do want to be able to maintain software over the long term, and for that, review is still the thing that matters.</p>
<p>I am not claiming it always will be. Maybe we end up with other ways to judge whether a system is maintainable, and then none of this matters and I will happily stop reading diffs. But that is not where we are. Today, when a model gets something wrong, a person still has to go in and fix it, and that person has to read it first.</p>
<p>So while that holds, how a language reads matters more than ever. It does not have to be Ruby's expressiveness specifically. It has to be that the signal-to-noise ratio of a diff is high enough that I am reading decisions instead of scanning past ceremony to find them.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-would-want">What I would want<a href="https://www.mauromorales.com/posts/does-the-language-still-matter/#what-i-would-want" class="hash-link" aria-label="Direct link to What I would want" title="Direct link to What I would want" translate="no">​</a></h2>
<p>I would love to see new languages designed for this: not for the joy of writing them, but for making code review easy and effective.</p>
<p>I have a rough idea of the shape. As little boilerplate as possible. Concise. Pleasant to read. And no special cases you have to master or get bitten by, because the reviewer is now often the person who did <em>not</em> write the code, and every subtlety is a trap for them specifically.</p>
<p>Maybe the code does not even need to be written in that language. Maybe it gets transpiled into it, and the readable form is the one you review.</p>
<p>I am well aware that models can review code too. I use that, and it catches things. But I have yet to see a fully lights-off factory that delivers maintainable code, the kind a human opens six months later without wanting to tear their eyes out. That is the bar, and nothing I have run clears it yet.</p>
<p>And while we are at it: could we please also come up with something better than YAML?</p>
<p>Anyway. What is your favorite language to review code in, and why?</p>]]></content:encoded>
            <category>Software Development</category>
            <category>AI</category>
            <category>Reflections</category>
        </item>
        <item>
            <title><![CDATA[Week 1 of Building a Software Factory: From Pair Programming to Delegation]]></title>
            <link>https://www.mauromorales.com/posts/from-pair-programming-to-delegation/</link>
            <guid>https://www.mauromorales.com/posts/from-pair-programming-to-delegation/</guid>
            <pubDate>Thu, 06 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[I was given six months of a Claude subscription through the Claude for Open Source program, and I spent the first two and a half weeks building a small agent factory. Here is what actually happened, including the five things that broke.]]></description>
            <content:encoded><![CDATA[<p>The email landed on Friday, 17 July 2026, three days before I was due to leave for vacation. The sensible thing would have been to file it away and deal with it when I got back. Instead I spent the next two evenings standing up a system so I could start a small agent factory before I disappeared for a couple of weeks.</p>
<p>The agents have been running since Monday 20 July, the day I flew out, so that is two and a half weeks ago. This post is the first report back, and I want to be precise about what it is and is not. Nothing from this experiment has shipped to Kairos yet. Not one line. Everything the system has produced so far has gone into the scaffolding that runs it, plus my own homelab. So this is not a success story, and it is definitely not a "look what AI did for me" post. It is a write-up of what it took to get the thing running, and of the five separate ways it went wrong while I was doing that.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-would-you-do-with-six-months-of-claude-max-20x">What would you do with six months of Claude Max 20x?<a href="https://www.mauromorales.com/posts/from-pair-programming-to-delegation/#what-would-you-do-with-six-months-of-claude-max-20x" class="hash-link" aria-label="Direct link to What would you do with six months of Claude Max 20x?" title="Direct link to What would you do with six months of Claude Max 20x?" translate="no">​</a></h2>
<p>Not long ago I applied to Anthropic's Claude for Open Source program, and I got accepted. The benefit is a free six-month Claude Max 20x subscription, the top tier, which normally runs about $200 a month, so roughly $1,200 over the six months. When something like that lands in your inbox unplanned, you have to make the best of it.</p>
<p><img decoding="async" loading="lazy" alt="The Claude for Open Source welcome email" src="https://www.mauromorales.com/assets/images/claude-oss-email-00c52da29a0c88aa0892d8978871c28f.png" width="900" height="832" class="img_ev3q">
<em>The email that started it, on 17 July 2026.</em></p>
<p>I settled on two answers.</p>
<p>The first is what to spend it on: getting as far through the Kairos backlog as I can, and doing it as an agent-led factory rather than by pairing my way through tickets one at a time. Kairos is where my open-source time goes, and its backlog is long enough that the difference between "me, in the evenings" and "me plus a factory" should actually be visible.</p>
<p>The second is to document it. Six months of the top tier is an unusual thing to be handed, and the interesting part is not that I have it, it is what comes out of it. So I am writing down what I do with these months and what happens as a result, including the parts that do not work. This post is the first installment, and going by the ratio so far, the parts that do not work are going to be the bulk of the material.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pairing-with-models-already-works-now-what">Pairing with models already works. Now what?<a href="https://www.mauromorales.com/posts/from-pair-programming-to-delegation/#pairing-with-models-already-works-now-what" class="hash-link" aria-label="Direct link to Pairing with models already works. Now what?" title="Direct link to Pairing with models already works. Now what?" translate="no">​</a></h2>
<p>Here is the thing that made me want to try this at all. A lot of my code over the last several months already came out of working with a model, in a chat window, one turn at a time. So for me the question is not whether a model can write useful code. I already have my answer to that, and it is yes, as long as a human is sitting right next to it. Two pieces of my own recent work convinced me.</p>
<p>The first is <a class="" href="https://www.mauromorales.com/posts/introducing-kairos-lab/">kairos-lab</a>. I am honest with myself about this one: I would not have built it if it were not for AI. Not because I could not have written the code, but because I would never have found the time. Pairing with a model lowered the cost of starting enough that a project I had been putting off for months finally happened. That is a real change, and it is easy to underrate. Most of the ideas we never ship do not die because they are too hard. They die because we never find the free evening to begin.</p>
<p>The second is the riscv64 work on Kairos, and it taught me something different. Supporting a new architecture pushed me down to a lower level than I usually work at, into territory where I am not fluent and would normally hesitate before even asking a question out loud. Working alongside a model changed that. I felt confident enough to poke at things I would otherwise have left to someone with more experience, to ask the naive question, and to keep going when the first answer was wrong. It did not just help me write code faster. It widened the set of problems I felt allowed to take on.</p>
<p>Put those two together and the pattern is clear. Pairing with a model helps me start things I would have skipped, and it helps me reach into areas I would have avoided. For me, that result is already in.</p>
<p>So the honest next question is: now what? There is a lot of talk right now about agent factories, teams of models turning out software while their owners sleep. I am wary of the hype, and at the same time genuinely curious. Is a factory like that something only a company or a well staffed team can build, or can one person stand up a small version that actually works? That is a big part of what I want to find out. The specific step I want to test is delegation.</p>
<p><strong>Can I move from pair-programming to delegation?</strong></p>
<p>Calling it pairing might sound a little off, since there is no human on the other side. But I have done a lot of pair programming over my career, some of it full extreme programming, and working with a model honestly feels similar to me. Not the same: you lose the visual cues and the easy back and forth you get from another person. Even so, the shape of it, thinking out loud, taking turns, correcting each other, is close enough that pairing is the word that fits. In that sense, pairing means I am in the loop on every step. Delegation means the agents do the work on their own, and my judgment moves to three gates instead of every keystroke:</p>
<ol>
<li class=""><strong>Issue acceptance.</strong> Deciding what is worth doing, and writing it down clearly enough that it can be handed off.</li>
<li class=""><strong>PR review.</strong> Reading what comes back and deciding whether it is correct, whether it fits, and whether it is honest about its own limitations.</li>
<li class=""><strong>Final testing.</strong> Actually running the thing before it goes anywhere near users.</li>
</ol>
<p>I kept these gates on purpose. It would be tempting to aim for a lights-out factory, one that merges its own work while I sleep, but almost everything I have read says that road leads to real problems with code quality over time. As appealing as it is to become a 10x developer overnight, I care more about being able to maintain what gets written. If the honest price of that is being a 2x developer instead, or whatever the real number turns out to be, I will take it. Maintainable beats fast.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-setup-briefly">The setup, briefly<a href="https://www.mauromorales.com/posts/from-pair-programming-to-delegation/#the-setup-briefly" class="hash-link" aria-label="Direct link to The setup, briefly" title="Direct link to The setup, briefly" translate="no">​</a></h2>
<p>The architecture is deliberately modest, and the hardware bill is the least interesting part of this post, which is exactly the point. One always-on mini PC, a Beelink SER5 with a Ryzen and 32 GB of RAM, sits in my homelab running the sessions. No GPU, no rack, no cloud instance. The 32 GB is more than a setup like this needs for most projects, since the sessions themselves are not memory hungry, but Kairos work means booting VMs to test images and upgrade paths, sometimes several at once, and that is where the headroom earns its place. Everything else is a GitHub project board: issues go in, agents pick them up, pull requests come out. And every one of those pull requests passes through me before it lands.</p>
<p><img decoding="async" loading="lazy" alt="The mission-control project board open on a laptop at an airport gate" src="https://www.mauromorales.com/assets/images/mission-control-board-gate-7e0ccd638f51d07434e5edbbcb3b7fdd.jpg" width="768" height="1024" class="img_ev3q">
<em>The board, checked from a departure gate before the flight.</em></p>
<p>That review gate is not hypothetical, and it is not tied to my desk. The first pull requests this system produced, I read and merged from my phone while standing at a departure gate. It was a one-file change to a node's boot documentation, checked and squashed between boarding announcements. I mention it not as a flex but because it is the honest shape of the thing. If staying in the loop required me to be sitting at my keyboard, this would not be delegation. It would just be a slower way of doing the work myself.</p>
<p><img decoding="async" loading="lazy" alt="Reviewing a pull request diff on a phone from a plane seat" src="https://www.mauromorales.com/assets/images/reviewing-pr-from-phone-0132fcd4c59c1314016e8bd4f46c7d0a.jpg" width="768" height="1024" class="img_ev3q">
<em>Reviewing that first change from my seat: the diff to a node's boot documentation.</em></p>
<p>One decision I did not expect to enjoy as much as I did: the agents commit under their own GitHub identity, <a href="https://github.com/mauro-agent" target="_blank" rel="noopener noreferrer" class="">@mauro-agent</a>. Authorship stays mine, so the work still shows up as mine, but the committer is the agent. That means anyone reading the history can see exactly which commits came out of the factory and which ones I typed myself. If I am going to publish results in six months, I want that line to be visible in the git metadata rather than in a claim I make afterwards.</p>
<p>It matters in the other direction too. I have years of history under <code>mauromorales</code>, and I am not willing to hand that over to a bot account or watch it thin out because a machine is doing the typing. The work is still mine to answer for: if something the agent wrote turns out to be wrong, it went out under my name, and that is on me, not on the tooling. Author mine, committer the agent's, is the best balance I have found. I keep the history, I keep the responsibility, and anyone who wants to know how a given commit came to exist can just look.</p>
<p>The split itself is not new to me. Back when I was pairing with humans, we did the same thing in git: whoever had their hands on the keyboard was the committer, and the other one went in as author, so a single commit carried both people. The only difference here is that we never swap seats. The agent is always the one committing, and I am always the one signing for it.</p>
<p><img decoding="async" loading="lazy" alt="The mauro-agent GitHub avatar: an eighties anime style illustration of a figure in a blue flight suit with an eagle emblem reading AGENT" src="https://www.mauromorales.com/assets/images/mauro-agent-avatar-d37603843c1754d391d20852991e122e.jpg" width="460" height="460" class="img_ev3q">
<em>The avatar I gave it. It is supposed to be me.</em></p>
<p>I gave it an avatar, because of course I did, and the avatar is supposed to be me. My girlfriend took one look at it and recognized me straight away, then could not stop laughing at how geeky I am, which was genuinely one of the better moments of the week. It is a strange time to be working in this field. I am building a small factory of software agents in my spare evenings, and the funniest part of it is that the agent doing the work is wearing my face in an eighties anime flight suit.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-actually-broke">What actually broke<a href="https://www.mauromorales.com/posts/from-pair-programming-to-delegation/#what-actually-broke" class="hash-link" aria-label="Direct link to What actually broke" title="Direct link to What actually broke" translate="no">​</a></h2>
<p>Now the part that matters. In two and a half weeks I have written five postmortems. None of them are serious. Nothing shipped broken, nothing was unrecoverable, nothing reached a user. But I want them in public, because a list of five small failures in seventeen days is a much more useful data point than any number of merged pull requests.</p>
<p><strong>A pull request that said "Merged" and was not.</strong> One change was branched on top of another change that had not landed yet. The base merged first, then the stacked one merged too, and GitHub cheerfully reported both as merged. Except the second one had merged into a branch, not into the main line. Its contents, an architecture decision record and a rule change to the agents' own instructions, silently never arrived. I only found out the next day when a rule that should have existed was not there. The status was technically true and completely misleading, which is a category of bug I now watch for.</p>
<p><strong>A rule that was too absolute the first time it met reality.</strong> I had written "never use <code>--no-verify</code>" into the agents' instructions, meaning never bypass the local git hooks. Then a task required proving that the server-side branch protection actually rejects a direct push to <code>main</code>, and the only way to test that was to get past the local hook so the server could be the thing doing the rejecting. The session did it, disclosed it, and was right to. The rule is now "never use <code>--no-verify</code> to land changes; permitted only for negative tests where rejection is the expected outcome, and it must be stated every time." A rule with no exception is not stricter, it is just wrong more often.</p>
<p><strong>A session that nearly wrote into the wrong repository.</strong> A session scoped to planning work got most of the way through drafting a blog post directly into my website's repository before I interrupted it. There was, in fact, another session whose whole job is that repository. Nothing about the environment signaled that boundary. The clone was right there. What stopped it was me, reading the conversation at that exact moment, by luck. I have since made the routing explicit, but the deeper issue stands: the scoping rules are prose, and prose does not enforce anything. The real fix is credentials that cannot push where the session is not supposed to push, and I have not built that yet.</p>
<p><strong>A session that blocked silently with no way for me to know.</strong> A session tried to open a link and hit a browser extension flow that needed a click to continue. It just stopped and waited. Nothing reached my phone. I found out only because I happened to open the session on the server directly. The whole design of running this while I am away rests on GitHub being the channel back to me, and GitHub had nothing to show, because the session never got as far as writing anything down. The mitigation is a rule that anything potentially blocking gets announced somewhere I can see it <em>before</em> it is attempted. The underlying gap, no reliable way to be told "your agent is stuck", is still open.</p>
<p><strong>An environment quirk that ate real hours.</strong> Certain configuration paths in this sandbox appear as device stubs rather than files, and ordinary git operations that need to write there fail with "Device or resource busy". Disabling the sandbox does not help, because the mechanism sits below it. It cost me the same debugging session five separate times before it got written down properly. Worse, the cleanup step after one of those failures assumed a commit had succeeded when it had not, and rolled a real commit off my local branch. Nothing was pushed, so no harm done, but "assume the previous step worked" is exactly how a safe cleanup becomes a destructive one.</p>
<p>Read as a set, none of these are arguments against working this way. Every one of them is an argument for keeping a human at the gates. Dex Horthy made <a href="https://x.com/dexhorthy/status/2080697380379427275" target="_blank" rel="noopener noreferrer" class="">an argument recently about why software factories fail</a> that has stuck with me: models are trained against tests that pass or fail, and nothing in that loop penalizes making the codebase worse to live in. So a lights-off factory degrades quietly, because nobody is reading the diffs. My five postmortems are small precisely because somebody was reading them. That is the whole finding.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-thing-i-did-not-plan-for">The thing I did not plan for<a href="https://www.mauromorales.com/posts/from-pair-programming-to-delegation/#the-thing-i-did-not-plan-for" class="hash-link" aria-label="Direct link to The thing I did not plan for" title="Direct link to The thing I did not plan for" translate="no">​</a></h2>
<p>On 29 July, Claude went down.</p>
<p><img decoding="async" loading="lazy" alt="The Claude status page showing elevated errors across all models and a major outage" src="https://www.mauromorales.com/assets/images/claude-outage-6038208bd105a9c479b7ee2b062f40bd.png" width="1786" height="1542" class="img_ev3q">
<em>29 July 2026. Nothing to do but wait.</em></p>
<p>There is nothing to do in that situation. The factory stops. And it is worth saying plainly that this is a real drawback of building this way, not a footnote. It is the same shape of dependency as GitHub going down, which would also stop everything, since the board, the issues and the pull requests all live there. I have built a system with two single points of failure and neither of them is mine.</p>
<p>What I would want is a local model good enough to keep doing basic tasks, plus a local copy of the tickets and artefacts so I could keep making progress offline. I am not going to get there in this experiment, because a machine that runs a decent model locally costs considerably more than the mini PC this whole thing runs on, and I am not making that investment to answer this question. The minimal hardware footprint is a feature of the experiment, and I would rather report the limitation honestly than spend my way around it.</p>
<p>The other option is subscribing to several of these services at once so you can fail over between them, but then the cost climbs fast. What I would actually want, and I do not know whether it exists, is a service that sells you <em>a</em> frontier model without guaranteeing which one. If you are strongly attached to a particular model you would hate that. I would take it over being stuck. It reminds me of that xkcd about compiling: the outage is not a break, it is just a period where you cannot do the thing and cannot honestly do anything else either.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-numbers-so-far">The numbers so far<a href="https://www.mauromorales.com/posts/from-pair-programming-to-delegation/#the-numbers-so-far" class="hash-link" aria-label="Direct link to The numbers so far" title="Direct link to The numbers so far" translate="no">​</a></h2>
<p>An experiment needs a baseline, otherwise in six months I will just tell myself a nice story. So before I started, I measured my own output across the Kairos repositories for the six months leading up to this:</p>
<table><thead><tr><th>Activity (previous 6 months, before automation)</th><th style="text-align:right">Count</th></tr></thead><tbody><tr><td>Pull requests merged</td><td style="text-align:right">142</td></tr><tr><td>Pull requests reviewed</td><td style="text-align:right">194</td></tr><tr><td>Blog posts published</td><td style="text-align:right">14</td></tr></tbody></table>
<p>I am not putting these here to brag, and I do not want to oversell what they measure. Merged pull requests are close to a vanity metric, in the same way lines of code are. Plenty of those 142 are one-line fixes and release chores, and a count like this treats them the same as a week of real work. But I have to base the comparison on something, and this is the least dishonest number I could find.</p>
<p>What the table cannot show is how the time actually splits. My Kairos work is part advocacy and part maintainership, and the advocacy side is invisible in a table of pull requests. In the same six-month window I gave <a class="" href="https://www.mauromorales.com/public-speaking/">ten talks across seven events</a>, among them FOSDEM, CfgMgmtCamp and KubeCon EU. A talk is not an hour of work. It is designing it, writing the CFP, building the deck, practicing, traveling and being there. I do not have a number for those hours, but there are a lot of them, and I would not give them up.</p>
<p>That is the honest label on the "before" column: I am not satisfied with the amount of technical work I get to ship. I would like to be. That, more than any count, is what I want out of these six months, and if it works it feeds the other side too, because more work shipped is more work worth proposing a talk about.</p>
<p>Maybe two and a half weeks for this does not sound like much. Worth knowing that I was on vacation for most of it, so I was really only playing with this for an hour in the evenings, if that. Put another way, the whole thing took less than twenty-four hours to build.</p>
<p>For the period since, counted the same way and with the same caveats, here is what the system has actually produced in two and a half weeks:</p>
<table><thead><tr><th>Activity (20 July – 6 August, with agents)</th><th style="text-align:right">Count</th></tr></thead><tbody><tr><td>Changes to the agent factory itself</td><td style="text-align:right">22</td></tr><tr><td>Changes to this website and the homelab</td><td style="text-align:right">9</td></tr><tr><td>Architecture decision records written</td><td style="text-align:right">6</td></tr><tr><td>Postmortems filed</td><td style="text-align:right">5</td></tr><tr><td>Changes shipped to Kairos</td><td style="text-align:right">0</td></tr></tbody></table>
<p>That last row is the honest headline. Almost all of this output is the factory building and documenting itself. That is not a disappointment, it is what the first phase was for, but it does mean anyone reading a comparison of these two tables should read the last row first.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-next">What is next<a href="https://www.mauromorales.com/posts/from-pair-programming-to-delegation/#what-is-next" class="hash-link" aria-label="Direct link to What is next" title="Direct link to What is next" translate="no">​</a></h2>
<p>Now that I am back from vacation, the Kairos phase starts. Up to this point the agents have only had write access to my own repositories, which is a low-stakes place to find out that a rule is too absolute or that a merged pull request never merged. Working against a project other people depend on is a different proposition, and it starts with a narrow scope, fork-and-pull-request only, and nothing landing without my explicit approval on each specific change.</p>
<p>I have no results to report there yet. The next post on this will have some, and if what it has instead is another list of postmortems, I will publish that.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="one-last-honest-note">One last honest note<a href="https://www.mauromorales.com/posts/from-pair-programming-to-delegation/#one-last-honest-note" class="hash-link" aria-label="Direct link to One last honest note" title="Direct link to One last honest note" translate="no">​</a></h2>
<p>This post was drafted by the system I am describing. I then reviewed it, edited it, and merged it, the same way I intend to handle every other pull request it produces. Some people think writing this way is stupid, that if a machine drafted the words they do not really count as yours. I see it more like having a ghost writer. The ideas, the judgment, and the final say are mine; the first draft is help. If that feels a little recursive, it is, and I think that is the most honest way to start. You are reading the first output of the experiment, held to exactly the standard I am promising to hold the rest of it to.</p>
<p>I will report back as I go, not just once at the end but at points along the way, with the failures included. If you want to follow along, you can subscribe to this blog's <a href="https://www.mauromorales.com/posts/rss.xml" target="_blank" rel="noopener noreferrer" class="">RSS feed</a>, find me on <a href="https://www.linkedin.com/in/mauromorales/" target="_blank" rel="noopener noreferrer" class="">LinkedIn</a> or <a href="https://www.youtube.com/@mauromrls" target="_blank" rel="noopener noreferrer" class="">YouTube</a>, or reach out through my <a class="" href="https://www.mauromorales.com/contact/">contact page</a>.</p>]]></content:encoded>
            <category>Kairos</category>
            <category>Open Source</category>
            <category>AI</category>
            <category>Reflections</category>
        </item>
        <item>
            <title><![CDATA[30 times on stage]]></title>
            <link>https://www.mauromorales.com/posts/30-times-on-stage/</link>
            <guid>https://www.mauromorales.com/posts/30-times-on-stage/</guid>
            <pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Reflecting on 30 talks in three years, impostor syndrome, Kairos, and finding the next challenge just outside my comfort zone.]]></description>
            <content:encoded><![CDATA[<p>Last week I realized I had put myself in front of people 30 times since I started promoting <a href="https://kairos.io/" target="_blank" rel="noopener noreferrer" class="">Kairos</a> 3 years ago. That's roughly 10 times per year, which I think is a fantastic number for me.</p>
<p>But it's not like this was my first attempt at public speaking. Back in 2015 and 2016 I gave it my first shot with some minor success. I was even at <a href="https://www.youtube.com/watch?v=JlFyf80sNCQ&amp;list=PLD45kuLwkJxb-v2DNMmH7O2NPrhBWrsKH&amp;index=9" target="_blank" rel="noopener noreferrer" class="">FOSDEM</a> and LinuxCon back then, which was huge for me at the time, but then I stopped.</p>
<p>The reason I used to tell myself and others was that I had stopped working in the open. And there is some truth in that. It is much easier to give a talk when the work is public, when people can see the code, the issues, the discussions, the decisions, and all the rough edges. Open source gives you a shared body of work to point to, not just a story people have to take your word for. But if I'm honest with myself, that was not the full reason. Working on proprietary code has never stopped people from giving good technical talks.</p>
<p>The real reason was impostor syndrome.</p>
<p>I felt like maybe I could convince the people selecting talks, and maybe I could even convince the right kind of audience, but I could not convince myself. And when you cannot convince yourself, it becomes hard to keep going.</p>
<p>The person who originally inspired me to start speaking was <a href="https://blog.cornelius-schumacher.de/" target="_blank" rel="noopener noreferrer" class="">Cornelius Schumacher</a>. He has been active in open source for a long time, and one of the things I learned from him was not to sit too comfortably, but also not to drown myself in a challenge that was too big. The useful place was somewhere in the middle: uncomfortable enough to grow, but not so overwhelming that you cannot move. That lesson stayed with me, even if for a while I was not really acting on it.</p>
<p>What changed with Kairos was not that I suddenly became fearless. I didn't. There is still stage fright. There is still the moment where I look at a topic and think: "Do I really understand this well enough to explain it to other people?" There is still the discomfort of being visible, of being judged, and of maybe being publicly wrong. Being back in open source definitely made it easier, but what really made it click was how I think about teamwork. We all have our skills, but we also all care about certain things more than others. I thrive when I find a spot where I can help and nobody else can, or nobody else wants to. Promoting Kairos was that spot. Someone needed to fill it, and morphing into what a gap needs so that others can keep doing what they are good at is, I think, the thing I am actually good at.</p>
<p>So I decided to step up and take that role, without leaving my maintainer duties. Not because I thought I was the perfect person for it. Not because I think I'm an excellent speaker. And definitely not because my goal is to be in front of people just for the sake of being in front of people. I did it because the project needed it, because I care about the work, and because it forced me to confront an unresolved experience I had been carrying for years.</p>
<p>One moment made this very clear to me. In 2023 I was part of <a href="https://www.youtube.com/watch?v=uD-T9LdfvrM&amp;list=PLD45kuLwkJxb-v2DNMmH7O2NPrhBWrsKH&amp;index=1" target="_blank" rel="noopener noreferrer" class="">a panel at KubeCon Paris</a>, sitting next to people from Microsoft, Google, and AWS. I walked in feeling like the underdog in the room. But at some point during the conversation I noticed I was not just keeping up, I belonged in it. Nobody handed me that feeling. I had to notice it myself.</p>
<p>I believe all this public speaking has helped people discover Kairos, which is the main goal. Preparing a talk forces me to understand things better. Explaining an idea in public exposes the weak parts of my own thinking. Questions from the audience show me where the story is unclear. Crafting a demo helps expose bugs and UX issues in the system. And doing this repeatedly has made me a better engineer, not just a better communicator. But there's also one aspect that I think can be improved. Talks tend to be one-sided communication, except for the few questions you get at the end. So I want to try more ways to promote the project that allow me to engage further with people.</p>
<p>So here's to those 30 times on stage. For me personally, I think I can close a chapter. The question I'm asking myself now is: what is the next thing that sits just outside my comfort zone, but not so far outside it that I cannot grow into it? I think the answer is exploring other formats like video or online workshops. If this sounds interesting to you, please follow me on <a href="https://www.linkedin.com/in/mauromorales/" target="_blank" rel="noopener noreferrer" class="">LinkedIn</a> or <a href="https://www.youtube.com/@mauromrls" target="_blank" rel="noopener noreferrer" class="">YouTube</a>, where I've started publishing some conversations with the Kairos team. And if you would like to collaborate, please reach out through my <a class="" href="https://www.mauromorales.com/contact/">contact page</a>!</p>]]></content:encoded>
            <category>Public Speaking</category>
            <category>Kairos</category>
            <category>Open Source</category>
            <category>Reflections</category>
        </item>
        <item>
            <title><![CDATA[Open Source Isn't Open Governance]]></title>
            <link>https://www.mauromorales.com/posts/open-source-isnt-open-governance/</link>
            <guid>https://www.mauromorales.com/posts/open-source-isnt-open-governance/</guid>
            <pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Why license and activity are not enough when evaluating open source projects, and why governance matters even more at the operating-system layer.]]></description>
            <content:encoded><![CDATA[<p>When I watch teams evaluate open source projects, I notice most of them check two things: the license, and whether the project looks active. Commits in the last month, responsive maintainers, a reasonable release cadence, stars and contributors trending the right way. Both of those signals matter - a project without an OSI-approved license carries a different risk profile than one that has one, and a project that hasn't been committed to in a year is probably not a safe bet.</p>
<p>But those two signals, even taken together, don't tell you much about who actually controls the project, who gets to decide its direction, or what happens if the company behind it changes its mind. That's governance, and it's the signal most evaluations skip - not because people don't care, but because it's harder to check than licensing or commit activity, and nobody has really taught them where to look.</p>
<p>This post focuses on the CNCF and friends - the CNCF specifically, the Linux Foundation umbrellas nearby (LF Edge, NeoNephos), and a couple of LF projects that share governance DNA. That's the neighborhood I'm invested in at the moment. There are plenty of good governance stories in other foundations and in projects outside any foundation at all, but a full tour would make this post much longer than I want it to be. The questions I end up with transfer; apply them wherever you're evaluating.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-governance-actually-is-and-why-it-matters-now-more-than-ever">What governance actually is, and why it matters now more than ever<a href="https://www.mauromorales.com/posts/open-source-isnt-open-governance/#what-governance-actually-is-and-why-it-matters-now-more-than-ever" class="hash-link" aria-label="Direct link to What governance actually is, and why it matters now more than ever" title="Direct link to What governance actually is, and why it matters now more than ever" translate="no">​</a></h2>
<p>Governance is the set of rules determining who makes decisions about a project. Who merges code. Who decides the roadmap. What happens if maintainers disagree, or if the company behind the project is acquired or changes direction. These procedural questions are what determine whether software you depend on will still be serving you in five years.</p>
<p><strong>Distributed governance</strong> - multi-vendor, auditable, with decisions made in public - has real benefits. The project outlives any one company's interest. Decisions get made by people with different perspectives, which generally produces better technical outcomes. You get a legible, predictable path for raising issues and having them addressed. And you get something harder to name: the quiet confidence that you're not betting your platform on one CEO's roadmap.</p>
<p><strong>Concentrated governance</strong> - typically single-vendor, often implicit, usually fine <em>right now</em> - has its own benefits. Decisions are faster. Direction is clearer. There's no committee. A small team that knows what it's doing can move a project forward quickly, and plenty of excellent software has been built this way. The trade-off is that you're betting on that team's continued interest, alignment, and financial health. When those align with your own interests, it's a great bet. When they don't, you have limited recourse.</p>
<p>Neither are right or wrong. They are just different models that have different pros and cons in this context.</p>
<p>What has changed recently is that the volume of code available is about to increase dramatically. AI-assisted development is going to produce a lot more code - and a lot of that code will be published as open source by individuals or small groups who haven't put any governance structure in place, because there hasn't been time to. At the same time, existing open source projects are going to see more contributions flowing in than their maintainers can realistically review. In both cases, governance is the thing that helps. For new projects, governance is what distinguishes "published as open source" from "maintained as open source." For existing projects, the right governance structures are how you absorb a surge of contributions without losing the quality bar.</p>
<p>That shift is what makes governance checking more load-bearing now than it was a few years ago. "It's open source" used to carry an implicit governance signal - if something was open source, there was probably a person accountable to a community somewhere. As the total pool of open source code grows faster than the pool of accountable maintainers, that implicit signal weakens. Foundations are one of the more reliable ways to tell the difference between "open source with real stewardship" and "open source with whoever is around."</p>
<p>That's the context this post is set in.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cncf-project-governance-explained">CNCF project governance, explained<a href="https://www.mauromorales.com/posts/open-source-isnt-open-governance/#cncf-project-governance-explained" class="hash-link" aria-label="Direct link to CNCF project governance, explained" title="Direct link to CNCF project governance, explained" translate="no">​</a></h2>
<p>The CNCF hosts projects at one of three maturity levels: Sandbox, Incubating, and Graduated. All three carry the same "CNCF project" label, but they represent different stages of governance development, and understanding the difference is most of what governance evaluation at this foundation comes down to.</p>
<p>Let me describe what each of them represents, less as a CNCF process description and more as what it means for you if you're adopting.</p>
<p><strong>Sandbox</strong> is the entry rung. Getting into Sandbox is not nothing. You donate your code and IP to a vendor-neutral foundation. You accept the CoC, the trademark policy, the whole framework. That's a real signal - the project is saying, in public, "we want to play the governance game." Most projects that start private never make it this far, and most that get to Sandbox are genuinely trying.</p>
<p>What Sandbox <em>doesn't</em> tell you is whether the hard work has happened yet. And the hard work is getting other companies involved - people from different places with merge rights and accountability, actually shaping the project's direction. That work is harder than it sounds. It's always easier for a company to consume something and trust it'll stay maintained than to put engineering time into maintaining it themselves. The famous <a href="https://xkcd.com/2347/" target="_blank" rel="noopener noreferrer" class="">xkcd 2347</a> about modern infrastructure depending on some random project that one person has been maintaining for years describes this perfectly, and the dynamic that produces it is the same dynamic that makes multi-vendor maintainership expensive.</p>
<p>So when you adopt a Sandbox project, in practice you're in bed with the company maintaining it. There's nothing intrinsically wrong with that - it's a bit like the benevolent dictator model, and plenty of excellent software has been built that way. But you should be clear with yourself that it <em>is</em> that model. The maintaining company's priorities are going to shape the project. If they pivot, so does it.</p>
<p><strong>Incubation</strong> is where that picture starts to change. The TOC runs a real due diligence process. Adopters get interviewed. Governance gets reviewed. A project doesn't reach Incubation without showing that multiple organizations care about it enough to contribute maintainers and use it in production. This is the level where "single-vendor" starts to become "single-vendor by habit rather than by structure" - the scaffolding is there for more than one company to matter.</p>
<p><strong>Graduated</strong> is where the picture has fully changed. Third-party security audit, formal governance review, sustained production adoption across diverse organizations, full TOC due diligence. Kubernetes is graduated. Kyverno graduated in March 2026, as <a href="https://www.cncf.io/announcements/2026/03/24/cloud-native-computing-foundation-announces-kyvernos-graduation/" target="_blank" rel="noopener noreferrer" class="">announced by the CNCF</a>. When you see a project at this level, somebody who doesn't work for the sponsoring company has checked the work.</p>
<p>None of the three levels are wrong, and neither is adopting at any of them. What matters is knowing which model you're getting, because they have genuinely different trade-offs. The question isn't "is this project mature enough" - it's "which model of governance am I willing to get in bed with, and what am I trading off in exchange for what."</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="and-then-theres-the-landscape">And then there's the landscape<a href="https://www.mauromorales.com/posts/open-source-isnt-open-governance/#and-then-theres-the-landscape" class="hash-link" aria-label="Direct link to And then there's the landscape" title="Direct link to And then there's the landscape" translate="no">​</a></h2>
<p>It gets more confusing than that. The <a href="https://landscape.cncf.io/" target="_blank" rel="noopener noreferrer" class="">CNCF Landscape</a> is a separate thing from the list of CNCF projects. The landscape is a categorical map of the cloud-native ecosystem, and it includes CNCF-hosted projects <em>and</em> vendor products and other open-source projects that are not hosted by the CNCF but have a place on the map.</p>
<p>To the CNCF's credit, the landscape does encode the distinction visually. CNCF-hosted projects appear in large boxes with colored frames indicating their maturity level, non-hosted open source projects appear in small white boxes, and proprietary products appear in gray boxes. The <a href="https://github.com/cncf/landscape-2/blob/master/guide.md" target="_blank" rel="noopener noreferrer" class="">landscape guide</a> explains all of this.</p>
<p>The problem isn't that the distinction doesn't exist. The problem is that most practitioners never read the guide. They see a familiar logo on a page that says "CNCF" at the top, and they assume everything on that page is under CNCF governance in some form. It's not. Box size and frame color are visual cues that require prior knowledge to decode, and the unaware visitor doesn't even realize there's a distinction to decode.</p>
<p>The landscape is trying to serve two audiences - people looking for CNCF projects specifically, and people wanting a broader ecosystem map. A more prominent label, a filter on by default, or separate URLs would help the first group.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-at-the-os-layer-specifically">Why this matters at the OS layer, specifically<a href="https://www.mauromorales.com/posts/open-source-isnt-open-governance/#why-this-matters-at-the-os-layer-specifically" class="hash-link" aria-label="Direct link to Why this matters at the OS layer, specifically" title="Direct link to Why this matters at the OS layer, specifically" translate="no">​</a></h2>
<p>When we say "Linux," we actually mean two different things. There's the kernel, which is genuinely the same everywhere - the same code that Linus merges is the same code that eventually lands on your servers, regardless of distribution. Then there's the <em>distribution</em>, which is a set of package choices layered on top of that kernel: which init system, which libc, which package manager, which userspace utilities, which defaults, which patches. The kernel is a technical artifact. The distribution is a governance artifact. And the distribution is what you actually run.</p>
<p>Historically, the distinction was small enough to gloss over. Debian, Fedora, openSUSE, Arch - opinionated on packaging, but all in the mix-and-match tradition. Components were swappable, the init system was replaceable, and the distribution was opinionated without being load-bearing.</p>
<p>That's not true for all modern OSes. Some are more like Android than Linux. Android runs on the Linux kernel, but the parts that matter in practice - Google Play Services, the proprietary framework - are vendor-owned and close to impossible to remove. "Android is built on Linux" is technically true and practically misleading. You're running Google's OS on top of a kernel that happens to be open.</p>
<p>The same pattern shows up in cloud-native operating systems. Some projects genuinely inherit the mix-and-match tradition - you can replace components, swap the orchestrator, change the base distribution. Others bundle vendor-specific choices so tightly that the open-source kernel underneath is almost decorative. Both patterns can be fine depending on what you're trying to do, but they represent fundamentally different bets. In the first case, you're adopting a piece of open infrastructure that happens to have a company maintaining it. In the second case, you're adopting a company's product that happens to have an open kernel underneath. Those are not the same thing, and governance is the lens that tells you which one you're actually looking at.</p>
<p>This matters more at the OS layer than at higher layers because Android-style bundling is hardest to see at the OS layer. A policy engine is bounded - you know what it does, you can swap it. An OS spans thousands of packages, boot paths, and defaults. The cumulative effect of one vendor's choices can reshape "using this OS" in non-obvious ways.</p>
<p>And on top of that, there's an empirical observation: OS evaluation is underpracticed. Most platform teams reach into policy engines, observability stacks, ingress controllers - they've built pattern-matching instincts for those layers. Fewer teams reach into the OS, so when they do, they tend to fall back on the shallowest signals more heavily than they would for a layer they evaluate regularly.</p>
<p>Those two things together - the Android-vs-Linux ambiguity at the distribution level, and underpracticed evaluation - are what make governance checking at the OS layer matter more in practice than at most other layers, even if it isn't uniquely important in theory.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-cloud-native-is-going-and-why-that-amplifies-the-governance-gap">Where cloud-native is going, and why that amplifies the governance gap<a href="https://www.mauromorales.com/posts/open-source-isnt-open-governance/#where-cloud-native-is-going-and-why-that-amplifies-the-governance-gap" class="hash-link" aria-label="Direct link to Where cloud-native is going, and why that amplifies the governance gap" title="Direct link to Where cloud-native is going, and why that amplifies the governance gap" translate="no">​</a></h2>
<p>There's one more thing worth naming, because it changes the stakes of this whole conversation.</p>
<p>Cloud-native, until now, has mostly been a public-cloud, single-cloud story. The CNCF landscape grew out of an environment where most people were deploying into one hyperscaler at a time, and many of the projects we rely on carry assumptions that quietly reflect that environment. This is why people who try to run cloud-native stacks in a sovereign cloud, or at the edge, or across multiple providers, keep running into seams - pieces that work beautifully on the happy path and awkwardly off it.</p>
<p>That's going to change. Sovereign cloud pressure is real and accelerating, driven by geopolitics and regulation. Edge is going to expand dramatically, driven by AI workloads moving closer to where the data is and by latency requirements that can't be served from a faraway region. Cloud-native is going to stop being "running on one cloud" and start being "running across a federation of environments that didn't necessarily coordinate with each other." That's the direction of travel, and the signals are already visible.</p>
<p>In that future, distributed governance isn't just philosophically preferable - it's structurally aligned with where the workloads are going. A project whose maintainers come from multiple organizations, deploying into multiple environments, will accommodate that diversity naturally because it reflects their own reality. A project whose decisions live inside one vendor will, over time, optimize for that vendor's environment - not out of malice, but because the people making decisions are solving their own problems. The exception is a vendor whose commercial posture is itself distributed, and those vendors exist but are rare. For everyone else, concentrated governance means the project is going to fit the single-cloud past better than the federated future.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-operating-systems-currently-in-the-cncf">The operating systems currently in the CNCF<a href="https://www.mauromorales.com/posts/open-source-isnt-open-governance/#the-operating-systems-currently-in-the-cncf" class="hash-link" aria-label="Direct link to The operating systems currently in the CNCF" title="Direct link to The operating systems currently in the CNCF" translate="no">​</a></h2>
<p>At the time I'm writing this, there are three operating systems hosted by the CNCF. Three.</p>
<p>A note before I list them: I'm a Kairos maintainer, so I'm biased. What follows is my personal view, not the view of my employer or of the Kairos team - I'm speaking for myself. I'll try to be fair about all three projects, and where I can't be neutral I'll say so.</p>
<p>I'm listing them in the order they joined the CNCF, because one of the things we're going to talk about checking is <em>intention</em>, and intention is partly revealed by how early a project committed to this path in the first place.</p>
<ul>
<li class=""><strong>Kairos</strong> - Sandbox (accepted April 13, 2024), and the first operating system project accepted into the CNCF, as listed on the <a href="https://www.cncf.io/projects/kairos/" target="_blank" rel="noopener noreferrer" class="">CNCF project page</a>. Kairos is the underdog of this lineup - it's backed by a smaller company rather than a hyperscaler. Big-tech backing cuts both ways: it brings resources and influence, but those companies also retire or deprioritize projects when strategy shifts, and their weight on a project's direction is proportional to their size. An underdog project that's still here and still growing is telling you something about the conviction of the people maintaining it. Kairos is showing several of the signals the TOC looks for at Incubation; the public writeup of the current state is in the <a href="https://kairos.io/blog/2026/04/09/become-a-kairos-adopter-for-cncf-incubation/" target="_blank" rel="noopener noreferrer" class="">Become a Kairos adopter for CNCF Incubation</a> post.</li>
<li class=""><strong>Flatcar Container Linux</strong> - Incubating (accepted October 2024). Flatcar is the first operating system distribution to reach CNCF Incubation in the foundation's history, according to the <a href="https://www.cncf.io/blog/2024/10/29/flatcar-brings-container-linux-to-the-cncf-incubator/" target="_blank" rel="noopener noreferrer" class="">CNCF announcement</a>. Microsoft is the primary steward via the Kinvolk acquisition, with contributions from Cisco, Equinix, Wipro, and others. Flatcar cleared the Incubation bar, which means adopter interviews happened, governance got reviewed, and the project held up to external scrutiny.</li>
<li class=""><strong>bootc</strong> - Sandbox (accepted January 2025). A Red Hat-led project for bootable OSes delivered as OCI images, building on the ostree and composefs machinery already used by Fedora CoreOS and RHCOS. Its current status is reflected on the <a href="https://www.cncf.io/projects/bootc/" target="_blank" rel="noopener noreferrer" class="">CNCF project page</a>. At Sandbox, the governance trajectory beyond Red Hat hasn't yet been externally validated.</li>
</ul>
<p>All three of these projects are in the special-purpose operating system space, and I have genuine admiration for the technical work in all three. I'm looking narrowly at what their current CNCF status says about how far the multi-vendor governance process has developed. One of them has been through Incubation due diligence. Two of them haven't. That's what the shared CNCF label can flatten if you don't look past it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-ones-just-outside-and-what-their-situations-teach">The ones just outside, and what their situations teach<a href="https://www.mauromorales.com/posts/open-source-isnt-open-governance/#the-ones-just-outside-and-what-their-situations-teach" class="hash-link" aria-label="Direct link to The ones just outside, and what their situations teach" title="Direct link to The ones just outside, and what their situations teach" translate="no">​</a></h2>
<p>Part of what makes the CNCF list short at the OS layer is that several of the OSes practitioners encounter live somewhere else. Looking at the broader landscape is useful because it shows the range of governance patterns that an OS project can end up in.</p>
<ul>
<li class=""><strong>Garden Linux</strong> is built by SAP as the base for the Gardener ecosystem. Gardener was donated to the NeoNephos Foundation, a Linux Foundation initiative aligned with European cloud-sovereignty efforts, as described on the <a href="https://gardener.cloud/" target="_blank" rel="noopener noreferrer" class="">Gardener site</a>. Being in NeoNephos is a positive signal - there's real intent there. My honest expectation is that anything claiming to be sovereign should eventually have distributed governance, because concentrated sovereignty from a single vendor is a contradiction in terms. That takes time, and that's fine; the point is that the structural direction is set up to support it.</li>
<li class=""><strong>EVE-OS</strong> sits under LF Edge, a Linux Foundation umbrella for edge projects, as shown on the <a href="https://lfedge.org/projects/eve/" target="_blank" rel="noopener noreferrer" class="">LF Edge project page</a>. The foundation wrapper is real, but in operational practice the maintainer roster is heavily Zededa-affiliated - which is the company that donated the project. This is a common LF Edge pattern and it's not a knock on the project; it's a reminder that foundation membership at the Stage level EVE is in is a different kind of signal than CNCF Incubation.</li>
<li class=""><strong>Talos</strong> - Sidero Labs' OS - is listed in the CNCF Landscape as a non-hosted project, in the same category of entry where commercial offerings also appear. This is exactly the situation the landscape section above describes: present on the landscape page, not under CNCF governance. The distinction is easy enough to see once you know to look for it, and easy to miss if you don't. Talos Linux itself is MPL 2.0 open source; its companion management plane Omni is source-available under BSL 1.1.</li>
<li class=""><strong>Bottlerocket</strong> is AWS-maintained, Apache 2.0 licensed, not in any foundation. My read is that it's essentially designed for internal AWS use - which is fine; it does that job well - but the posture is closer to "internal OS with open source code" than "open source project with external contributors." It's similar in shape to Google COS, just with better visibility: a website, docs, a public repository, a changelog.</li>
<li class=""><strong>Google COS</strong> is Google-maintained, proprietary, used inside Google Cloud across multiple services (including as the default for GKE, but not limited to it). The extent to which it's used beyond what's publicly documented is hard to say from outside Google, which is itself a governance observation. When a widely-deployed OS has almost no public surface, the governance conversation can't really start.</li>
</ul>
<p>One observation about this whole list: neither LF Edge nor NeoNephos publishes a maturity model equivalent to the CNCF's Sandbox/Incubating/Graduated tiers. That's unfortunate, because it means the governance state of projects under those umbrellas is harder to read at a glance - you have to do the work yourself, case by case. Whatever criticism the CNCF deserves about the landscape, the maturity tiers themselves are a real contribution, and it would help the whole ecosystem if other foundations adopted something similar.</p>
<p>None of these are wrong. Several are serving real users in production. But when you line them up next to the three CNCF OSes, the spectrum of governance patterns becomes visible - Incubating at the CNCF, Sandbox at the CNCF, inside an LF umbrella at the NeoNephos or LF Edge level, pure single-vendor open source, single-vendor proprietary. All of them are available to you as a platform team. The question is which one you want under your workloads, for which reasons, with what expectations about what happens in five years.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-to-actually-look-for">What to actually look for<a href="https://www.mauromorales.com/posts/open-source-isnt-open-governance/#what-to-actually-look-for" class="hash-link" aria-label="Direct link to What to actually look for" title="Direct link to What to actually look for" translate="no">​</a></h2>
<p>If you're evaluating any OS - CNCF-hosted or not - these are the signals I'd check after the license:</p>
<ul>
<li class=""><strong>Adoption.</strong> Who is actually using this in production, and are those adopters diverse enough that the project isn't serving one company's internal needs? If the only serious users are the company that maintains it, the project might be great - but you're essentially adopting their internal tool. That's a different bet.</li>
<li class=""><strong>Supporting companies and individuals.</strong> How many organizations are contributing meaningfully, and how many people are in the maintainer role? A project with maintainers from one company isn't necessarily worse than one with five - but the structural fragility is real, and you should know which you're getting.</li>
<li class=""><strong>Public records, not just code.</strong> It's not enough for the code to be open. Decisions, governance changes, maintainer onboarding, votes - the auditable parts of how the project is run should be visible too. GitVote is one mechanism. Public maintainer meetings with published minutes are another. The point is that an interested person can reconstruct how decisions got made without having to ask anyone privately.</li>
<li class=""><strong>Intention to keep growing.</strong> Maturity isn't a static state; it's a direction. A Sandbox project with a dated, public trajectory toward Incubation is in a different place than one that's been at Sandbox for three years without visible movement. Ask projects about their next governance milestone - not just their next feature.</li>
</ul>
<p>Apply those to Kairos if you're evaluating Kairos. Apply them to Flatcar if you're evaluating Flatcar. Apply them to Bottlerocket or Talos or Garden Linux with context-appropriate adjustments. The signals are the same; only the scenery changes.</p>]]></content:encoded>
            <category>Open Source</category>
            <category>Governance</category>
            <category>CNCF</category>
            <category>Cloud Native</category>
            <category>Linux</category>
            <category>Special Purpose Operating Systems</category>
            <category>Cloud Native OS</category>
        </item>
        <item>
            <title><![CDATA[What Are Special-Purpose Operating Systems in the Cloud Native World?]]></title>
            <link>https://www.mauromorales.com/posts/what-are-spos-in-the-cloud-native-world/</link>
            <guid>https://www.mauromorales.com/posts/what-are-spos-in-the-cloud-native-world/</guid>
            <pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Exploring the evolution of operating systems in the Cloud Native landscape, focusing on special-purpose operating systems (SPOS) and their role in modern infrastructure.]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="disclaimer">Disclaimer<a href="https://www.mauromorales.com/posts/what-are-spos-in-the-cloud-native-world/#disclaimer" class="hash-link" aria-label="Direct link to Disclaimer" title="Direct link to Disclaimer" translate="no">​</a></h2>
<p>I'm one of the maintainers of <a href="https://kairos.io/" target="_blank" rel="noopener noreferrer" class="">Kairos</a>, an operating system built for cloud-native workloads. I also served a term as co-chair of what was then the CNCF Special Purpose Operating System Working Group under TAG Runtime. Both of those shape how I see this space. These are my personal views, not the official views of my employer or of the Kairos project. I'll try to be fair to other projects and honest about where my perspective is partial.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="gpos-and-spos">GPOS and SPOS<a href="https://www.mauromorales.com/posts/what-are-spos-in-the-cloud-native-world/#gpos-and-spos" class="hash-link" aria-label="Direct link to GPOS and SPOS" title="Direct link to GPOS and SPOS" translate="no">​</a></h2>
<p>Most people, when they say "operating system," are thinking of a <strong>general-purpose operating system</strong> (GPOS). Linux, macOS, Windows, the BSDs. The defining property of a GPOS is exactly what it sounds like: it's designed to run a wide range of workloads on a wide range of hardware, configurable in a wide range of ways, for a wide range of users. GPOSes are the right answer when you don't know in advance what the machine is going to do, when the machine is going to do several things at once, or when a human is going to log in and use the machine interactively.</p>
<p>A <strong>special-purpose operating system</strong> (SPOS) is the opposite. It's designed around a specific operational model, workload, or hardware profile. You give up generality in exchange for fit: the OS matches its purpose more precisely than a general-purpose OS can, because it doesn't have to accommodate every other purpose too.</p>
<p>SPOS is an umbrella term, and the umbrella is wider than most cloud-native conversations acknowledge. It covers:</p>
<ul>
<li class=""><strong>Mobile operating systems</strong> like Android and iOS, purpose-built for touch-first, battery-constrained, app-store-distributed devices</li>
<li class=""><strong>Real-time operating systems</strong> (RTOS) used in industrial control, avionics, and automotive systems, where deterministic timing is the defining constraint</li>
<li class=""><strong>Embedded operating systems</strong> for routers, appliances, IoT devices, and other devices with fixed functions</li>
<li class=""><strong>Unikernels</strong>: single-application images that collapse the OS/app boundary entirely</li>
<li class=""><strong>Cloud-native operating systems</strong>: the subcategory I'll spend the rest of this post on</li>
</ul>
<p>These are all SPOSes, and they are not interchangeable. An RTOS would be a bad fit for a phone, Android would be a bad fit for an industrial controller, and a cloud-native OS would be a bad fit for either. The "special" in "special-purpose" means something different in each case.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-spos-working-group-and-why-cloud-native-os-is-a-better-name-for-this-subcategory">The SPOS Working Group, and why "cloud-native OS" is a better name for this subcategory<a href="https://www.mauromorales.com/posts/what-are-spos-in-the-cloud-native-world/#the-spos-working-group-and-why-cloud-native-os-is-a-better-name-for-this-subcategory" class="hash-link" aria-label="Direct link to The SPOS Working Group, and why &quot;cloud-native OS&quot; is a better name for this subcategory" title="Direct link to The SPOS Working Group, and why &quot;cloud-native OS&quot; is a better name for this subcategory" translate="no">​</a></h2>
<p>For a while, the CNCF had a working group specifically focused on this space. It was the Special Purpose Operating System Working Group, it sat under TAG Runtime, and it was the home where practitioners from Bottlerocket, Flatcar, Talos, Kairos, openSUSE MicroOS, and others compared notes. In the CNCF TAG restructure that happened in 2025, TAG Runtime and several adjacent TAGs were consolidated into a new TAG called <strong>Workloads Foundation</strong>, and the old working groups, including SPOS, were dissolved as part of that restructure. Kairos, Flatcar, and bootc sit under TAG Workloads Foundation.</p>
<p>The dissolution of the SPOS WG is actually useful for a naming conversation I've been wanting to have for a while. "SPOS" was always a slightly awkward fit for the specific category that working group covered: it's accurate but ambiguous, since SPOS includes things that have nothing to do with cloud-native. The original WG charter scoped itself further to "container OSes," which was more specific but still named the category by its current workload rather than its architecture. Containers are what these systems run today. WASM is what some of them will run tomorrow. Whatever comes after is what they'll run in ten years. Defining the category by the workload makes the definition brittle as workloads evolve.</p>
<p>What I think works better, and what I'll use for the rest of this post and a series that follows it, is <strong>cloud-native OS</strong>. The term names the design goal rather than the implementation, which makes it robust to how workloads and techniques evolve. And now that the SPOS WG no longer exists as an active body, there's no institutional name to displace. The conversation is free to pick up a clearer term.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-makes-an-os-cloud-native">What makes an OS "cloud-native"?<a href="https://www.mauromorales.com/posts/what-are-spos-in-the-cloud-native-world/#what-makes-an-os-cloud-native" class="hash-link" aria-label="Direct link to What makes an OS &quot;cloud-native&quot;?" title="Direct link to What makes an OS &quot;cloud-native&quot;?" translate="no">​</a></h2>
<p>Here's where I want to be careful, because it's easy to define cloud-native OS the wrong way.</p>
<p>The tempting definition is to list features: image-based delivery, immutable root filesystem, atomic upgrades, declarative configuration, OCI-artifact transport, trusted boot. These are the properties most of the projects in this space have, and they're the properties I'll spend most of the rest of the series talking about.</p>
<p>But features aren't the definition. Features are how you <em>achieve</em> the definition. If we say a cloud-native OS is one that has these five features, then any project that adopts those features, even if it doesn't serve cloud-native goals, would qualify, and any project that serves cloud-native goals through different techniques wouldn't. Both of those conclusions seem wrong to me.</p>
<p>A better framing starts with what cloud-native itself is about. The CNCF defines cloud-native as a set of practices for building and running scalable applications in modern, dynamic environments. The original context was public cloud, but the same goals have since been applied on-prem, at the edge, in air-gapped environments, and across sovereign clouds. The goals are:</p>
<ul>
<li class=""><strong>Scalability</strong>: systems that scale up and down with demand without requiring architectural redesign</li>
<li class=""><strong>Reliability</strong>: systems that tolerate the failure of individual components without taking everything down</li>
<li class=""><strong>Portability</strong>: workloads that can move between environments without being rewritten</li>
<li class=""><strong>Manageability at scale</strong>: fleets that can be operated declaratively rather than node-by-node</li>
<li class=""><strong>Observability</strong>: systems whose state is legible to automated monitoring and human operators</li>
</ul>
<p>Cloud-native <em>techniques</em>, containers, immutable infrastructure, declarative APIs, orchestration, are how the industry has been achieving those goals. They're not the goals themselves.</p>
<p>So here's the definition I'd propose: a <strong>cloud-native OS is an operating system designed to serve cloud-native goals</strong>. It's scalable, reliable, portable, manageable at scale, and observable, at the OS layer, for the fleet it's part of. The features most of these projects share (image-based delivery, immutability, atomic upgrades, declarative config) are the current best-known techniques for achieving those goals, but they're not the only possible techniques, and future cloud-native OSes might achieve the same goals in different ways.</p>
<p>I think this framing is better than a features-list definition for three reasons. First, it's honest about what we're actually trying to do: the techniques serve the goals, the goals don't serve the techniques. Second, it's robust: if someone invents a better technique for achieving cloud-native goals at the OS layer ten years from now, we don't have to redefine the category. And third, it makes the evaluation question sharper: instead of asking "does this OS have image-based upgrades," you ask "does this OS actually serve my cloud-native goals in my environment," which is the question you needed answered anyway.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="did-the-os-stop-mattering">Did the OS stop mattering?<a href="https://www.mauromorales.com/posts/what-are-spos-in-the-cloud-native-world/#did-the-os-stop-mattering" class="hash-link" aria-label="Direct link to Did the OS stop mattering?" title="Direct link to Did the OS stop mattering?" translate="no">​</a></h2>
<p>At some point in the cloud-computing timeline, the industry started acting like the OS didn't matter anymore. Platforms-as-a-Service abstracted everything below the application. You wrote code, shipped it, and the platform handled the rest. Kubernetes went a step further: the OS became the thing Kubernetes ran on, and for many teams the answer to "which OS?" was "whichever one the cloud provider picked for us."</p>
<p>That abstraction is genuinely useful for a lot of teams. If you're a startup moving fast, you probably shouldn't be spending time on OS choices. The platform is your operating environment, and that's fine.</p>
<p>But there's a cost to taking the abstraction too far. When the OS is invisible, you lose the ability to reason about things the OS is in the best position to answer: how do I know what's actually running on this machine, how do I verify nothing has been tampered with between build and boot, how do I upgrade a fleet predictably, what happens when a node behaves strangely, what's my recovery story if a physical device is compromised, how do I prove to an auditor that my compliance claims are backed by measurable facts?</p>
<p>These questions don't stop existing because the OS got abstracted. They just stop having good answers. And as platform teams take more direct ownership of edge deployments, sovereign clouds, air-gapped environments, and Kubernetes infrastructure they don't rent from a hyperscaler, the questions come back. Cloud-native OSes are what you get when you take those questions seriously and design the OS layer around them, rather than inheriting a general-purpose distribution and hoping for the best.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="examples-in-the-current-landscape">Examples in the current landscape<a href="https://www.mauromorales.com/posts/what-are-spos-in-the-cloud-native-world/#examples-in-the-current-landscape" class="hash-link" aria-label="Direct link to Examples in the current landscape" title="Direct link to Examples in the current landscape" translate="no">​</a></h2>
<p>A few projects in this space, as of now:</p>
<ul>
<li class=""><strong>Bottlerocket</strong>: AWS's purpose-built OS for container workloads, Apache 2.0 licensed, not in any foundation</li>
<li class=""><strong>Flatcar Container Linux</strong>: CNCF Incubating, Microsoft-stewarded via the Kinvolk acquisition, the first OS to reach CNCF Incubation</li>
<li class=""><strong>Talos Linux</strong>: Sidero Labs', API-driven with no shell access, MPL 2.0 for the OS itself</li>
<li class=""><strong>Kairos</strong>: CNCF Sandbox, first operating system accepted into the CNCF, distribution-agnostic and Kubernetes-first</li>
<li class=""><strong>bootc</strong>: CNCF Sandbox, Red Hat-led, bootable OS images using ostree and composefs</li>
<li class=""><strong>EVE OS</strong>: LF Edge project, created by ZEDEDA for distributed edge and fleet-managed deployments</li>
<li class=""><strong>Garden Linux</strong>: SAP-built, donated to the NeoNephos Foundation under Linux Foundation Europe</li>
<li class=""><strong>Google Container-Optimized OS</strong>: Google's internal OS for GCP, not open source in any meaningful sense</li>
<li class=""><strong>openSUSE MicroOS</strong>: a transactional-update variant of openSUSE aimed at container workloads</li>
</ul>
<p>Each of these makes different trade-offs, and I'll go deeper on several of them in other posts. The point of listing them here is just to make clear that the cloud-native OS category isn't hypothetical. It has real projects, real adopters, and a real body of practice that has accumulated over the last several years.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-this-series-will-cover">What this series will cover<a href="https://www.mauromorales.com/posts/what-are-spos-in-the-cloud-native-world/#what-this-series-will-cover" class="hash-link" aria-label="Direct link to What this series will cover" title="Direct link to What this series will cover" translate="no">​</a></h2>
<p>This post is the on-ramp to a series I'm starting on cloud-native operating systems: what they are, what they promise, what they actually deliver, how to evaluate them, and where the category is going. Upcoming posts will cover:</p>
<ul>
<li class="">Why image-based delivery is the foundational technical shift, and why most cloud-native-OS properties depend on it</li>
<li class="">How to think about governance at the OS layer, since the people who control the OS project end up controlling part of your platform</li>
<li class="">What trusted boot and measured boot actually prove, and why the "platform contract" framing matters more than the pure security framing</li>
<li class="">How to evaluate security claims in this space without being taken in by marketing that oversells attack-surface arguments</li>
<li class="">The honest trade-offs: what you give up moving from a general-purpose OS to a cloud-native one, and when those trade-offs aren't worth it</li>
<li class="">A deeper survey of the landscape above, with governance, licensing, and adoption context for each project</li>
</ul>
<p>If you're evaluating any of these systems for real, or just trying to make sense of the category, I hope the series is useful.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lets-continue-the-conversation">Let's continue the conversation<a href="https://www.mauromorales.com/posts/what-are-spos-in-the-cloud-native-world/#lets-continue-the-conversation" class="hash-link" aria-label="Direct link to Let's continue the conversation" title="Direct link to Let's continue the conversation" translate="no">​</a></h2>
<p>If you're working on this space too, or just thinking through these ideas, feel free to reach out through my <a class="" href="https://www.mauromorales.com/contact/">contact page</a>.</p>]]></content:encoded>
            <category>Operating Systems</category>
            <category>Cloud Native</category>
            <category>Kubernetes</category>
            <category>SPOS</category>
            <category>Infrastructure</category>
            <category>Cloud Native OS</category>
        </item>
        <item>
            <title><![CDATA[Introducing kairos-lab]]></title>
            <link>https://www.mauromorales.com/posts/introducing-kairos-lab/</link>
            <guid>https://www.mauromorales.com/posts/introducing-kairos-lab/</guid>
            <pubDate>Fri, 03 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A small CLI to remove local VM setup friction for first-time Kairos users on macOS and Linux.]]></description>
            <content:encoded><![CDATA[<p>I want to introduce <a href="https://github.com/kairos-io/kairos-lab" target="_blank" rel="noopener noreferrer" class="">kairos-lab</a>, a small CLI built to remove friction from first-time Kairos experiments on macOS and Linux.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-it-exists">Why it exists<a href="https://www.mauromorales.com/posts/introducing-kairos-lab/#why-it-exists" class="hash-link" aria-label="Direct link to Why it exists" title="Direct link to Why it exists" translate="no">​</a></h2>
<p>Quickstarts fall apart when people get stuck on local virtualization. On Apple Silicon, VirtualBox is a non-starter. On Linux vs macOS, QEMU setup and packages differ just enough to slow people down. That detour is not the point. The goal of the quickstart is to teach the Kairos lifecycle and the idea of immutable systems, not to turn someone into a host-virtualization expert.</p>
<p><code>kairos-lab</code> simplifies this path. It gives newcomers one consistent set of steps across macOS and Linux so they can focus on learning Kairos, not wrestling with host-specific setup.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="who-its-for-and-who-its-not">Who it's for (and who it's not)<a href="https://www.mauromorales.com/posts/introducing-kairos-lab/#who-its-for-and-who-its-not" class="hash-link" aria-label="Direct link to Who it's for (and who it's not)" title="Direct link to Who it's for (and who it's not)" translate="no">​</a></h2>
<p>This is for people who rarely touch VMs and just want to try Kairos. It's also for workshops, where I want everyone starting from the same baseline so we can spend time on the actual content.</p>
<p>Power users should absolutely keep using their preferred virtualization software. <code>kairos-lab</code> is not a replacement for full-featured VM tooling. It's the on-ramp.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="getting-started">Getting started<a href="https://www.mauromorales.com/posts/introducing-kairos-lab/#getting-started" class="hash-link" aria-label="Direct link to Getting started" title="Direct link to Getting started" translate="no">​</a></h2>
<p>Install via Homebrew or download a binary from the <a href="https://github.com/kairos-io/kairos-lab/releases" target="_blank" rel="noopener noreferrer" class="">releases page</a>. That's it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="my-first-vibe-coded-work-project">My first vibe-coded work project<a href="https://www.mauromorales.com/posts/introducing-kairos-lab/#my-first-vibe-coded-work-project" class="hash-link" aria-label="Direct link to My first vibe-coded work project" title="Direct link to My first vibe-coded work project" translate="no">​</a></h2>
<p>This is the first project I'm fully vibe coding for work. I didn't write the code myself; I acted as a tester and product owner. That might sound like a bad thing, but I see it as a positive in this case.</p>
<p>This is not production infrastructure. It's a helper tool for onboarding and workshops. I would not have had the time to build this on top of my day-to-day work without vibecoding it. And in my opinion the value it brings (removing onboarding friction) is higher than the cost of maintaining it, especially because maintenance will probably be vibemaintenance.</p>
<p>Side note: I used ChatGPT 5.3 codex (via open-code) for the alpha version, which worked well on macOS. To fix some bridged networking issues on Linux, I used Opus 4.5 (via cursor-agent).</p>
<p>This is also a follow-up to <a class="" href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/">"Coding with AI: From Blank Canvas to Working App in a Day"</a>, which I wrote exactly 10 months ago. The level of improvement since then is insane. I can clearly see this becoming a de facto way of building many projects, and with the right guardrails, also for production code.</p>]]></content:encoded>
            <category>Kairos</category>
            <category>Linux</category>
            <category>Virtualization</category>
            <category>Developer Experience</category>
            <category>AI</category>
        </item>
        <item>
            <title><![CDATA[Why you need a Raspberry Pi 5 in your homelab if you're building ARM software]]></title>
            <link>https://www.mauromorales.com/posts/raspberry-pi-5-arm-builds/</link>
            <guid>https://www.mauromorales.com/posts/raspberry-pi-5-arm-builds/</guid>
            <pubDate>Wed, 21 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Native ARM builds on a Raspberry Pi 5 can outperform x86 emulation by nearly 2x for OS-heavy workloads.]]></description>
            <content:encoded><![CDATA[<p>I've been working on ARM images lately. They are not application containers, but a distro-style image built from <code>ubuntu:22.04</code> that installs large package sets, runs post-install scripts, enables services, and pulls NVIDIA repositories. It's much closer to OS assembly than a typical app container.</p>
<p>I wasn't surprised that building these images natively on ARM would be faster than doing it from an x86 machine using emulation. That part was expected.</p>
<p>What surprised me was <strong>how much faster it was</strong>.</p>
<p>Seeing a Raspberry Pi 5 consistently outperform a 3-year-old high-end Intel laptop by almost <strong>2x</strong> forced me to re-evaluate some assumptions I had about build performance, hardware specs, and what "powerful enough" actually means when you're building ARM software.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-setup">The setup<a href="https://www.mauromorales.com/posts/raspberry-pi-5-arm-builds/#the-setup" class="hash-link" aria-label="Direct link to The setup" title="Direct link to The setup" translate="no">​</a></h2>
<p>I ran the same Docker-based build targeting <code>linux/arm64</code> on two machines.</p>
<table><thead><tr><th>Spec</th><th>Raspberry Pi 5 (native ARM)</th><th>Intel laptop (ARM via emulation)</th></tr></thead><tbody><tr><td>Architecture</td><td>aarch64</td><td>x86_64</td></tr><tr><td>CPU</td><td>Cortex-A76 (4 cores @ 2.4 GHz)</td><td>Intel i7-1280P (14 cores / 20 threads, up to 4.8 GHz)</td></tr><tr><td>RAM</td><td>~8 GB (no swap)</td><td>62 GB (+ swap)</td></tr><tr><td>Disk</td><td>ext4 on <code>/dev/sda2</code> (USB SSD, not microSD)</td><td>NVMe, btrfs</td></tr><tr><td>Kernel</td><td>6.17.x (Ubuntu raspi kernel)</td><td>6.17.x</td></tr><tr><td>Docker Engine</td><td>28.x (linux/arm64)</td><td>building <code>linux/arm64</code> via QEMU (<code>binfmt_misc</code>)</td></tr></tbody></table>
<p>On paper, the Intel laptop should dominate: it has about 3.5x more CPU cores (14 vs 4) with much higher boost clocks, roughly 8x the RAM (62 GB vs ~8 GB), and an NVMe drive that is typically much faster than a USB SSD in both throughput and latency.</p>
<p>On the Intel system, ARM execution is handled through Docker BuildKit with binfmt_misc enabled using tonistiigi/binfmt, which registers qemu-aarch64 at the kernel level. No static QEMU binary is copied into the image; emulation happens transparently during the build.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-dockerfile">The Dockerfile<a href="https://www.mauromorales.com/posts/raspberry-pi-5-arm-builds/#the-dockerfile" class="hash-link" aria-label="Direct link to The Dockerfile" title="Direct link to The Dockerfile" translate="no">​</a></h2>
<p>The Dockerfile is not compiling application code. It performs a lot of OS-level work:</p>
<ul>
<li class=""><code>apt-get update</code></li>
<li class="">installing many packages</li>
<li class="">running post-install scripts</li>
<li class="">enabling system services</li>
<li class="">generating initramfs/dracut bits</li>
<li class="">pulling NVIDIA repositories</li>
</ul>
<p>In other words: this is much closer to assembling a small Linux distribution than building a typical container image.</p>
<p>If you want to see the exact build steps, the Dockerfile is <a href="https://github.com/kairos-io/kairos/blob/0ef2f69f978b2ac3df6f9a1bba7270ee438bb05b/images/Dockerfile.nvidia-orin-nx" target="_blank" rel="noopener noreferrer" class="">here</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="results">Results<a href="https://www.mauromorales.com/posts/raspberry-pi-5-arm-builds/#results" class="hash-link" aria-label="Direct link to Results" title="Direct link to Results" translate="no">​</a></h2>
<p>Using the exact same Dockerfile and target architecture:</p>
<ul>
<li class=""><strong>Raspberry Pi 5:</strong> <code>real 7m35s</code></li>
<li class=""><strong>Intel laptop:</strong> <code>real 14m31s</code></li>
</ul>
<p>Despite the Intel machine having dramatically more CPU power, memory, and faster storage, it still took almost <strong>twice as long</strong>. For transparency, I ran the build once on the Raspberry Pi 5 and a couple of times on the Intel machine; the Intel timings were consistent enough that cold vs warm effects did not change the overall outcome.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-happens">Why this happens<a href="https://www.mauromorales.com/posts/raspberry-pi-5-arm-builds/#why-this-happens" class="hash-link" aria-label="Direct link to Why this happens" title="Direct link to Why this happens" translate="no">​</a></h2>
<p>The decisive difference isn't raw performance, it's architecture alignment.</p>
<p>On the Raspberry Pi, everything runs natively. ARM binaries execute directly on an ARM CPU.</p>
<p>On the Intel machine, all ARM binaries run through <strong>QEMU user-mode emulation</strong>. That means every instruction has to be translated before it can execute.</p>
<p>This build workload is particularly unfriendly to emulation:</p>
<ul>
<li class="">heavy <code>apt</code> / <code>dpkg</code> usage</li>
<li class="">many short-lived processes</li>
<li class="">lots of filesystem operations</li>
<li class="">syscall-heavy post-install scripts</li>
<li class="">very little meaningful parallelism</li>
</ul>
<p>This isn't a compute-bound workload where faster clocks and more cores help. It's dominated by overhead, and emulation multiplies that cost.</p>
<p>As a result, even a much more powerful x86 system can lose badly to a modest ARM system when the workload is OS-heavy.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-part-that-really-matters-cost-vs-time">The part that really matters: cost vs time<a href="https://www.mauromorales.com/posts/raspberry-pi-5-arm-builds/#the-part-that-really-matters-cost-vs-time" class="hash-link" aria-label="Direct link to The part that really matters: cost vs time" title="Direct link to The part that really matters: cost vs time" translate="no">​</a></h2>
<p>A Raspberry Pi 5 costs roughly <strong>EUR 80-100</strong>, depending on the RAM configuration and availability.</p>
<p>That's not "cheap" in an absolute sense, but in practice it's a bargain.</p>
<p>If that machine saves me even a few minutes per build -- multiplied across many iterations while working on images -- it pays for itself very quickly. Not in hardware terms, but in <strong>focus, iteration speed, and reduced friction</strong>.</p>
<p>Instead of waiting 15 minutes for a build to finish on my laptop under emulation, I can let a small ARM box do the work in half the time, quietly, in the background.</p>
<p>For the type of work I'm doing, that trade-off is an easy decision.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="this-applies-even-more-to-ci-pipelines">This applies even more to CI pipelines<a href="https://www.mauromorales.com/posts/raspberry-pi-5-arm-builds/#this-applies-even-more-to-ci-pipelines" class="hash-link" aria-label="Direct link to This applies even more to CI pipelines" title="Direct link to This applies even more to CI pipelines" translate="no">​</a></h2>
<p>The same logic applies to pipelines, arguably even more so.</p>
<p>For Kairos, we run our ARM builds on <strong>native ARM runners</strong>, which GitHub provides for free. Moving away from emulated ARM builds has significantly reduced how long our pipelines run.</p>
<p>The impact is very noticeable:</p>
<ul>
<li class="">faster feedback loops</li>
<li class="">less wasted CI time</li>
<li class="">fewer flaky or timing-sensitive failures</li>
<li class="">lower overall pipeline cost</li>
</ul>
<p>When your builds are dominated by package installation, system initialization, and OS-level steps, native execution isn't an optimization, it's the correct architectural choice.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="side-note-about-docker-cache-confusion">Side note: about Docker "cache" confusion<a href="https://www.mauromorales.com/posts/raspberry-pi-5-arm-builds/#side-note-about-docker-cache-confusion" class="hash-link" aria-label="Direct link to Side note: about Docker &quot;cache&quot; confusion" title="Direct link to Side note: about Docker &quot;cache&quot; confusion" translate="no">​</a></h2>
<p>While running these experiments, I repeatedly saw output like:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">CACHED FROM ubuntu:22.04@sha256:...</span><br></span></code></pre></div></div>
<p>even when using <code>--no-cache</code> and <code>--pull</code>.</p>
<p>What's happening here is subtle:</p>
<ul>
<li class=""><code>--no-cache</code> disables Docker build step caching</li>
<li class="">it does not clear BuildKit's internal content store</li>
<li class="">base image layers and remote <code>ADD</code> blobs can still be reused by digest</li>
</ul>
<p>If you really want to start from a clean slate, you need to clear BuildKit's cache explicitly:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">docker builder prune -a --force</span><br></span></code></pre></div></div>
<p>This removes cached content stored by BuildKit itself, not just images visible via <code>docker images</code>.</p>
<p>It's a blunt tool, but useful when you're trying to reason about cold-build performance.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-took-away-from-this">What I took away from this<a href="https://www.mauromorales.com/posts/raspberry-pi-5-arm-builds/#what-i-took-away-from-this" class="hash-link" aria-label="Direct link to What I took away from this" title="Direct link to What I took away from this" translate="no">​</a></h2>
<p>I didn't learn that native ARM is faster -- I already knew that.</p>
<p>What I learned is that architecture alignment matters far more than raw hardware specs for certain workloads.</p>
<p>When your build process is dominated by package managers, system initialization, and distribution-level tooling, native execution can easily outperform much stronger hardware running under emulation.</p>
<p>The Raspberry Pi 5 didn't win because it's fast.</p>
<p>It won because it speaks the right language.</p>]]></content:encoded>
            <category>ARM</category>
            <category>Raspberry Pi</category>
            <category>Homelab</category>
            <category>Linux</category>
            <category>Build Systems</category>
        </item>
        <item>
            <title><![CDATA[Introducing Hadron at FOSDEM 2026]]></title>
            <link>https://www.mauromorales.com/posts/introducing-hadron-at-fosdem-2026/</link>
            <guid>https://www.mauromorales.com/posts/introducing-hadron-at-fosdem-2026/</guid>
            <pubDate>Wed, 07 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Announcing my FOSDEM 2026 talk on Hadron, the path from configuration drift to image-based systems, and why we built a new minimal, upstream-first Linux distribution.]]></description>
            <content:encoded><![CDATA[<p>Ten years ago I gave my first talk at FOSDEM. Back then I was pushing for better tooling to inspect systems, to avoid configuration drift, and for universal system descriptions (USD), a way to consistently define Linux systems (see <a href="https://youtu.be/JlFyf80sNCQ" target="_blank" rel="noopener noreferrer" class="">this talk</a>).</p>
<p>Three years ago, I came back with a completely different paradigm: make existing systems immutable and image-based, remove drift at the root, and do it the same way across major Linux distros to avoid lock-in (see <a href="https://youtu.be/P8voTbQGmP8" target="_blank" rel="noopener noreferrer" class="">this talk</a>).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tldr">TL;DR<a href="https://www.mauromorales.com/posts/introducing-hadron-at-fosdem-2026/#tldr" class="hash-link" aria-label="Direct link to TL;DR" title="Direct link to TL;DR" translate="no">​</a></h2>
<p>This year, on February 1st, I'll be back to present Hadron, our new image-based Linux distribution, at <a href="https://fosdem.org/2026/schedule/event/YQRYB7-hadron-linux/" target="_blank" rel="noopener noreferrer" class="">FOSDEM</a>. If you want the backstory, read on.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="disclaimer">Disclaimer<a href="https://www.mauromorales.com/posts/introducing-hadron-at-fosdem-2026/#disclaimer" class="hash-link" aria-label="Direct link to Disclaimer" title="Direct link to Disclaimer" translate="no">​</a></h2>
<p>These ideas and projects are the work of many people. I’m not the visionary here; I’m just sharing a path I believe in and the lessons I’ve picked up along the way. The credit belongs to the communities and teams behind them.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="configuration-drift">Configuration Drift<a href="https://www.mauromorales.com/posts/introducing-hadron-at-fosdem-2026/#configuration-drift" class="hash-link" aria-label="Direct link to Configuration Drift" title="Direct link to Configuration Drift" translate="no">​</a></h2>
<p>In 2016 I was part of the <a href="https://github.com/SUSE/machinery" target="_blank" rel="noopener noreferrer" class="">Machinery</a> team at SUSE. Machinery is a tool for system introspection. It showed the current state of a system: which packages were installed and how their configuration differed from the defaults. That made it easy to spot a server drifting from the desired configuration. It felt powerful, because it made visible something that is usually invisible: how systems drift.</p>
<p>One of the ambitions of Machinery was to inspect multiple Linux distributions, but anyone who has worked with them knows how much they can differ. We thought we could tackle this by introducing USD. It didn’t pick up, and maybe that’s for the best.</p>
<p><a href="https://xkcd.com/927/" target="_blank" rel="noopener noreferrer" class=""><img decoding="async" loading="lazy" src="https://imgs.xkcd.com/comics/standards_2x.png" alt="XKCD 927" class="img_ev3q"></a></p>
<div class="shortcode-alert" role="note"><span class="shortcode-alert-icon" aria-hidden="true">i</span><div class="shortcode-alert-body"><p>Attribution: XKCD 927 “Standards” by Randall Munroe, licensed under CC BY-NC 2.5. <a href="https://xkcd.com/927/" target="_blank" rel="noopener noreferrer" class="">https://xkcd.com/927/</a> <a href="https://creativecommons.org/licenses/by-nc/2.5/" target="_blank" rel="noopener noreferrer" class="">https://creativecommons.org/licenses/by-nc/2.5/</a></p></div></div>
<p>What the industry did pick up was the use of configuration management systems (CMS). These introduced the idea of a single source of truth in the form of recipes, cookbooks, and playbooks. But CMS have limits. A full blog post is needed to explain all of that, so for now let’s focus on the main issue. If a system can be changed during runtime, then it’s only a matter of time before it happens in a way you did not predict and did not want.</p>
<p>And that was the recurring pain we kept running into: configuration drift. CMS designers understand this and introduced features to re-run configuration of their systems periodically. At best this leaves the system briefly disconfigured; at worst it fails to address changes that are not included in the system description. And for Machinery, well, introspection is nice to have, but not useful enough to get traction. So Machinery eventually reached its natural end, and I took a hiatus from this problem.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="image-based--immutable-systems">Image-Based / Immutable Systems<a href="https://www.mauromorales.com/posts/introducing-hadron-at-fosdem-2026/#image-based--immutable-systems" class="hash-link" aria-label="Direct link to Image-Based / Immutable Systems" title="Direct link to Image-Based / Immutable Systems" translate="no">​</a></h2>
<p>Fast forward to 2023, I joined the <a href="https://kairos.io/" target="_blank" rel="noopener noreferrer" class="">Kairos</a> team. Kairos turns an existing Linux distribution into an image-based one. You don’t mutate the base system at runtime; you swap the full image, or you don’t. Part of the system is mounted read-only (immutable), so package manager changes during runtime are off the table. No more drift.</p>
<p>As a bonus, this approach comes with side effects. Your bill of materials (SBOM) can be trusted because your system doesn’t change unexpectedly. You can use measured boot for critical parts like the kernel, cmdline, and initramfs, store the measurements in a TPM, and ensure only systems signed with your keys can boot.</p>
<p>Now Kairos is not the only project offering image-based systems out there. There's Google COS, Amazon Bottlerocket, Microsoft Flatcar, SUSE MicroOS, to name a few. And yes, I mention the ones backed by major platform vendors to make a point: this is a space getting serious investment and it will keep growing. But Kairos is unique in that it lets you choose your underlying Linux distro.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="enter-hadron">Enter Hadron<a href="https://www.mauromorales.com/posts/introducing-hadron-at-fosdem-2026/#enter-hadron" class="hash-link" aria-label="Direct link to Enter Hadron" title="Direct link to Enter Hadron" translate="no">​</a></h2>
<p>That flexibility Kairos gives is an asset. Within one organization you might be paying licenses for RHEL in your datacenter but running Ubuntu on your NVIDIA Orin devices; Kairos has you covered. Or maybe your team prefers Alpine? No problem. But that flexibility comes at a cost. After building hundreds of artifacts across distributions and options, we noticed two things: it can be confusing for newcomers, and some systems need just the bare minimum.</p>
<p>Our solution is a new Linux distribution, <a href="https://kairos.io/blog/2025/12/17/introducing-hadron-the-minimal-upstream-first-linux-base-for-kairos/" target="_blank" rel="noopener noreferrer" class="">Hadron</a>. It will not compete against all other distros; it will be one more flavor to use Kairos, but it will be the default and the only one we ship. This addresses both issues, gives us a minimal, upstream-first base to build on, and gives us a single baseline to test, document, and ship. Anyone who prefers to use one of the other base distros can keep doing so; they just need to build the images themselves, but we also provide all the tooling you may need with the <a href="https://github.com/kairos-io/AuroraBoot" target="_blank" rel="noopener noreferrer" class="">Kairos Factory</a>, which lets you build images from the CLI, web UI, or even a <a href="https://github.com/kairos-io/kairos-factory-action" target="_blank" rel="noopener noreferrer" class="">GitHub Action</a>.</p>
<p>Hadron is minimal and lean, upstream-first, and designed for the hard requirements of the edge while still running cloud-native workloads. It uses an uncommon combination of musl libc, systemd, vanilla kernel, and busybox to keep the base small and the attack surface lower while staying modern.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="fosdem-2026">FOSDEM 2026<a href="https://www.mauromorales.com/posts/introducing-hadron-at-fosdem-2026/#fosdem-2026" class="hash-link" aria-label="Direct link to FOSDEM 2026" title="Direct link to FOSDEM 2026" translate="no">​</a></h2>
<p>Join us on February 1st at <a href="https://fosdem.org/2026/schedule/event/YQRYB7-hadron-linux/" target="_blank" rel="noopener noreferrer" class="">FOSDEM</a> with my colleague <a href="https://dimitris.karakasilis.me/" target="_blank" rel="noopener noreferrer" class="">Dimitris Karakasilis</a>. We will share more insights into Hadron. We have a deep respect for this community, and that’s why we bring this talk to them. We want to present our work and gather feedback from those who have been building distributions for a while now.</p>]]></content:encoded>
            <category>Linux</category>
            <category>FOSDEM</category>
            <category>Kairos</category>
            <category>Hadron</category>
            <category>Cloud Native</category>
            <category>Edge</category>
            <category>Image-Based Systems</category>
            <category>Immutable Systems</category>
        </item>
        <item>
            <title><![CDATA[Wrapping Up 2025]]></title>
            <link>https://www.mauromorales.com/posts/wrapping-up-2025/</link>
            <guid>https://www.mauromorales.com/posts/wrapping-up-2025/</guid>
            <pubDate>Sun, 28 Dec 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>2025 felt like a continuation of the momentum I built <a class="" href="https://www.mauromorales.com/posts/wrapping-up-2024/">last year</a> with Kairos: more code, more people, and more proof that collaboration beats any solo sprint. I spent the year balancing hands-on engineering with developer relations, and I kept learning that the best outcomes happen when we build with others, not just for them.</p>
<p>I’ll never forget this year, less for the work and more for my son’s birth. While I want to keep that personal, it’s too big to ignore. This new role as a parent is teaching me different ways to prioritize and work, and I’m so excited about it.</p>
<blockquote>
<p>Update (2025-12-30): Added CNCF blog posts.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="20-years-working-in-tech">20 Years Working in Tech<a href="https://www.mauromorales.com/posts/wrapping-up-2025/#20-years-working-in-tech" class="hash-link" aria-label="Direct link to 20 Years Working in Tech" title="Direct link to 20 Years Working in Tech" translate="no">​</a></h2>
<p>This year marked my <strong>20th year</strong> in the tech industry. Over that time, I’ve worked on a lot of interesting projects and at some well-known tech companies. I earned a promotion to <strong>Staff Engineer</strong> at Spectro Cloud, and it feels like an important next step and a continuation of growth. It’s especially meaningful because I’m working on a project I find both interesting and impactful, alongside a team I truly admire, so I feel very proud of this accomplishment.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-cloud-native-os">The Cloud Native OS<a href="https://www.mauromorales.com/posts/wrapping-up-2025/#the-cloud-native-os" class="hash-link" aria-label="Direct link to The Cloud Native OS" title="Direct link to The Cloud Native OS" translate="no">​</a></h2>
<p>That project is Kairos. It’s the most challenging tech I’ve worked on because it demands programming knowledge and a deep understanding of Linux and its components: init systems, boot mechanisms, and more. It’s a lot of fun for my inner nerd, but it spans so many areas that are still new to me. And as if that wasn’t challenging enough, a big portion of my work is also about promoting the project and improving the user experience. I’m learning that on the go; I only started doing DevRel work last year. Here’s what mattered most:</p>
<ul>
<li class=""><strong>k0s integration</strong>: Kairos had first-class k3s support and a patchwork of community providers. I brought k0s in as a first-class option using the same Kairos-native cloud-init configuration, which means the same YAML-driven workflow, EdgeVPN-based P2P cluster formation, and a much smoother onboarding path. It turns Kairos into a Kubernetes meta-distribution, with native support for both k3s and k0s while keeping the immutable, secure-by-default experience intact.</li>
<li class=""><strong>Factory Web UI</strong>: To help people try Kairos and understand how the factory works, I made solid improvements to the Factory Web UI. That work led directly to my <strong>Pok8s</strong> talk on image-based systems and curated Kubernetes OS images, where I frame the problem space with playful metaphors and show why Kairos is a strong balance between convenience and control.</li>
<li class=""><strong>kairos-factory-action</strong>: Once the Factory Web UI makes the workflow tangible, the next step is to run it inside pipelines. I built the <a href="https://github.com/kairos-io/kairos-factory-action" target="_blank" rel="noopener noreferrer" class="">kairos-factory-action</a> so teams can generate images quickly by passing a few arguments in GitHub Actions. It’s now what we use to test, build, and release Kairos.</li>
</ul>
<p>My colleagues also shipped an amazing feature: a new Linux distribution specifically designed for image-based systems. On my side, I produced a <a href="https://kairos.io/quickstart/" target="_blank" rel="noopener noreferrer" class="">Quick Start series</a> for it. I’m proud of how it came out. It’s pushing me out of my comfort zone and giving me new ways to reach people and share Kairos, Hadron, and what I now see as the Cloud Native OS.</p>
<p>This year I delivered <strong>11 presentations</strong>, mostly at CNCF-related events, and every stage was a chance to share what we’re building and listen to what users need. I’m also excited that <strong>4 events are already scheduled for 2026</strong>. See the full list on my <a class="" href="https://www.mauromorales.com/public-speaking/">public speaking page</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cncf-blog-posts">CNCF Blog Posts<a href="https://www.mauromorales.com/posts/wrapping-up-2025/#cncf-blog-posts" class="hash-link" aria-label="Direct link to CNCF Blog Posts" title="Direct link to CNCF Blog Posts" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://www.cncf.io/blog/2025/03/25/building-secure-kubernetes-edge-images-with-kairos-and-k0s/" target="_blank" rel="noopener noreferrer" class="">Building secure Kubernetes Edge images with Kairos and k0s</a>: A joint post with William Rizzo on why Kairos + k0s make a secure, immutable, and low-ops edge stack, and how the integration simplifies real-world edge deployments.</li>
<li class=""><a href="https://www.cncf.io/blog/2025/12/29/how-to-integrate-kairos-architecturally-into-an-edge-ai-platform/" target="_blank" rel="noopener noreferrer" class="">How to integrate Kairos architecturally into an edge AI platform</a>: A field story with Jordan Karapanagiotis from Aurea Imaging on running K3s with Kairos on NVIDIA Jetson devices, using immutable upgrades and A/B rollbacks to keep fleets consistent and maintainable.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cncf-groups-and-meetups">CNCF Groups and Meetups<a href="https://www.mauromorales.com/posts/wrapping-up-2025/#cncf-groups-and-meetups" class="hash-link" aria-label="Direct link to CNCF Groups and Meetups" title="Direct link to CNCF Groups and Meetups" translate="no">​</a></h2>
<p>I continued <strong>co-hosting the <a href="https://community.cncf.io/cloud-native-brussels/" target="_blank" rel="noopener noreferrer" class="">CNCF Belgian Chapter</a></strong> and ran <strong>four well-attended meetups</strong>, around 30 people each, bringing companies and practitioners together. It’s been rewarding to see Belgium’s Cloud Native scene get stronger, and I’m excited to keep at it into 2026.</p>
<p>The <strong>CNCF removed working groups</strong>, which closed the <strong>Special-Purpose Operating System Work Group</strong>. I was bummed about not being a co-chair anymore, but it feels like a closing chapter. I’m now participating in the <strong><a href="https://contribute.cncf.io/community/tags/workloads-foundation/" target="_blank" rel="noopener noreferrer" class="">TAG Workloads Foundation</a></strong>, and I’m looking to reconnect with other OS makers to see if we can put something together.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="gratitude-and-acknowledgments">Gratitude and Acknowledgments<a href="https://www.mauromorales.com/posts/wrapping-up-2025/#gratitude-and-acknowledgments" class="hash-link" aria-label="Direct link to Gratitude and Acknowledgments" title="Direct link to Gratitude and Acknowledgments" translate="no">​</a></h2>
<p>Like last year, thanks to my team for being so supportive and hardworking, you guys rock! To the many people in the Cloud Native space who have been present, you know who you are, thank you. And a very special thanks to William Rizzo. Together we worked on the k0s integration, delivered 4 of those talks, and he even presented at the very first CNCF Community meetup I organized this year. William is very knowledgeable about Kubernetes and CNCF, but most importantly I’m very happy to have made a friend along the way.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-missed-and-why-thats-okay">What I Missed (And Why That’s Okay)<a href="https://www.mauromorales.com/posts/wrapping-up-2025/#what-i-missed-and-why-thats-okay" class="hash-link" aria-label="Direct link to What I Missed (And Why That’s Okay)" title="Direct link to What I Missed (And Why That’s Okay)" translate="no">​</a></h2>
<p>I didn’t hit everything I set out to do. I wanted deeper Kubernetes study (a homelab was supposed to help) and a content workflow that turns maintainer work into regular posts. I did level up my <strong>AI tooling</strong>, but the homelab is still on the backlog, and my content pipeline isn’t where I want it. That gap is a reminder to keep the goals a little out of reach so there’s room to grow.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="looking-ahead">Looking Ahead<a href="https://www.mauromorales.com/posts/wrapping-up-2025/#looking-ahead" class="hash-link" aria-label="Direct link to Looking Ahead" title="Direct link to Looking Ahead" translate="no">​</a></h2>
<p>Heading into 2026, I’m very excited about the possibilities with Kairos and Hadron. Talking with our users and learning about the different projects where they’re using Kairos, I see all the things we’re doing well. Seeing what other projects are doing, I feel like we have a very stable foundation and more flexibility than most. So if the stars align, my goal is to keep promoting the project and introduce it as the Cloud Native OS.</p>
<p>To achieve that, I plan to make Hadron the default in Kairos docs while still keeping all other flavors as first-class options. I also want to publish at least one technical video each month in 2026.</p>
<p>I plan to submit a CNCF Ambassador application and aim for acceptance or clear feedback. I believe in Cloud Native technology, and part of that is helping people see how relevant the OS layer still is in the Kubernetes world.</p>]]></content:encoded>
            <category>Developer Relations</category>
            <category>Cloud Native</category>
            <category>Community</category>
            <category>Open Source</category>
            <category>Reflections</category>
        </item>
        <item>
            <title><![CDATA[Fedora Silverblue: First Impressions from Someone Building an Immutable System]]></title>
            <link>https://www.mauromorales.com/posts/fedora-sivlerblue/</link>
            <guid>https://www.mauromorales.com/posts/fedora-sivlerblue/</guid>
            <pubDate>Mon, 27 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Ever since I started working on Kairos, I've wanted to test out Fedora Silverblue on the desktop. Not sure why I postponed this so much, but I finally pulled the trigger. Installation was fairly simple, though it got stuck every single time when trying to enable the third-party repositories. When this happened I was a bit scared that the whole experience was going to be the same. Fortunately, this hasn't been the case, quite the opposite.]]></description>
            <content:encoded><![CDATA[<p>Ever since I started working on Kairos, I've wanted to test out Fedora Silverblue on the desktop. Not sure why I postponed this so much, but I finally pulled the trigger. Installation was fairly simple, though it got stuck every single time when trying to enable the third-party repositories. When this happened I was a bit scared that the whole experience was going to be the same. Fortunately, this hasn't been the case, quite the opposite.</p>
<p>I've never used Fedora before, but I've used openSUSE, and to be honest, most major distros are so similar that for the majority of my needs all I have to do is understand which package manager they use and the underlying init system. In the case of Fedora Silverblue those are rpm-ostree and systemd. For the latter, there are no surprises; it's the same one I've used for many years now in openSUSE and Ubuntu, the two major distros I use. For the former, it's a bit less obvious because here we're comparing apples to pears: while openSUSE has zypper and Ubuntu has apt, Fedora has dnf, but we can't make use of it since the system is immutable. rpm-ostree is more of a hybrid between a package manager and an image manager. So yes, it knows about packages but it doesn't install them on your running system. Maybe first we need to quickly introduce what an immutable system is, and then we can dig into how you maintain such systems.</p>
<p>Disclaimer: I'm not an expert in Fedora Silverblue. If there's anything in this article that is wrong or misleading, please send me an email (bottom of the article) so I can correct it and learn from my mistake.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="immutable-os">Immutable OS<a href="https://www.mauromorales.com/posts/fedora-sivlerblue/#immutable-os" class="hash-link" aria-label="Direct link to Immutable OS" title="Direct link to Immutable OS" translate="no">​</a></h2>
<p>An immutable OS is not completely immutable, otherwise you wouldn't be able to run anything. What most operating systems out there mean when they say they are immutable is that there is a layer mounted read-only and therefore it cannot be modified. This is meant as a positive thing because it gives you a predictable system while reducing the attack surface, or at least making it harder to get hacked. There's a lot more under this term, but let's use this definition for this post. This means that by design you cannot install, remove, or upgrade packages in these systems.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="system-modifications">System Modifications<a href="https://www.mauromorales.com/posts/fedora-sivlerblue/#system-modifications" class="hash-link" aria-label="Direct link to System Modifications" title="Direct link to System Modifications" translate="no">​</a></h2>
<p>Obviously if you couldn't install, remove, or upgrade anything on your Linux desktop, it would be pretty useless. Hence we need mechanisms for modifying the system. There are two major approaches to achieve this. The first is to make changes in the areas of the system that aren't mounted read-only. That's partially true (there are more nuances), but for the sake of keeping things simple we're going to say that's the case. For example, within my home directory I can do whatever I want. This gives me lots of freedom without exposing the more sensitive areas of the system. We can achieve this via Flatpaks and toolboxes; you can think of them as two ways to containerize applications. There are obviously more ways to extend the system and I'll dig into them a bit later, but I'm mentioning these first because Fedora Silverblue seems to promote them.</p>
<p>The other way to modify the system is not to change the running system at all, but to create a new version of the immutable image based on the current image plus our desired changes. Fedora's solution for this is rpm-ostree. I haven't dug enough into Fedora's ecosystem to tell if there are more ways to generate images, so I'll focus on this one for now.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="flatpaks">Flatpaks<a href="https://www.mauromorales.com/posts/fedora-sivlerblue/#flatpaks" class="hash-link" aria-label="Direct link to Flatpaks" title="Direct link to Flatpaks" translate="no">​</a></h3>
<p>Flatpaks are very user-friendly. You can install them through the system's Store, which is not really a store because you don't pay for anything, but it's named like that for users coming from the Apple, Microsoft, or Android ecosystems. Installing applications is very simple: just look for what you want and click install.</p>
<p>Linux being Linux, you can also search, install, update, and remove Flatpaks via the command line. You can read more about it in their <a href="https://docs.flatpak.org/en/latest/using-flatpak.html" target="_blank" rel="noopener noreferrer" class="">documentation</a> but a quick example would be:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">flatpak install gimp</span><br></span></code></pre></div></div>
<p>This is how I've installed Slack and Visual Studio Code.</p>
<p>Not everything is perfect though. For example, Visual Studio Code throws a messages after installation about missing permissions, and giving me only a hint about how to solve the problem is IMO bad UX. Instead of just offering a close button, it should let me jump straight into the settings where I can make the needed changes.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="toolboxes">Toolboxes<a href="https://www.mauromorales.com/posts/fedora-sivlerblue/#toolboxes" class="hash-link" aria-label="Direct link to Toolboxes" title="Direct link to Toolboxes" translate="no">​</a></h3>
<p>From what I understand, a toolbox is like a sandbox. Imagine spinning up a container that already mounts local directories and the network, making it perfect to install everything you need for specific development projects. Normally I'd use tools like goenv or rbenv to have multiple versions of programming languages. With toolboxes I can do that for everything, not just the language runtime. Plus, it's better isolated! Not only are they cool, they're well integrated with the terminal. For example, if you're already inside a toolbox, opening a new tab drops you in the same toolbox.</p>
<p>These toolboxes run on top of Podman, Red Hat's container runtime. I'm more of a Docker user myself and while the experience of using Podman was ok for the most part, they aren't one-to-one compatible. I had to jump through some hoops to get some of the commands on the Kairos documentation running properly on Podman.</p>
<p>This is how I've installed Vim and npm.</p>
<p>So far the experience with toolboxes has been great. The only complaint I have is that my muscle memory keeps trying to open Vim on the host system when I know it's only installed in the toolbox. I'd like to make the toolbox the default for the terminal and switch to the host system, or a different toolbox, only when I need to. I still have to check if there's a way to configure that.</p>
<p>Update 2025-11-23</p>
<p>I've been pointed out to <a href="https://distrobox.it/" target="_blank" rel="noopener noreferrer" class="">Distrobox</a>, which seems to run on different hosts and allows you to spin up contianers of different distributions. This resonates better with me and it's also a good fit for Kairos.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="rpm-ostree">RPM-OStree<a href="https://www.mauromorales.com/posts/fedora-sivlerblue/#rpm-ostree" class="hash-link" aria-label="Direct link to RPM-OStree" title="Direct link to RPM-OStree" translate="no">​</a></h3>
<p>Okay, so far we've covered how to install software on the running system, but at some point you'll notice that there are packages you cannot easily install via Flatpaks or within a toolbox. In that case, you want to create a new image with those packages. In Fedora Silverblue land this process is called layering.</p>
<p>The tool used to layer your system is rpm-ostree. Think of it as a package manager that can peek across images. Within its database it keeps the currently installed packages and tracks the packages you want to add to your system. I say "tracks" because it doesn't apply the changes to the running system. To see the changes you must restart, and then you get a new image, most likely the original base with your layers on top, though I still need to read more about the exact architecture.</p>
<p>In the example below, you can see that my system's previous image included the cursor package, which has been removed in my current one, denoted by the ● symbol, and there's a new version of the base image with 43 upgraded packages.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">mauro@workstation:~$ rpm-ostree status</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">State: idle</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Deployments:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  fedora:fedora/42/x86_64/silverblue</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                  Version: 42.20251027.0 (2025-10-27T00:31:19Z)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">               BaseCommit: 220e7462ba4ef4389f88a7d1de72c266676ef317225f8d003e82a9c3b37dd45a</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             GPGSignature: Valid signature by B0F4950458F69E1150C6C5EDC8AC4916105EF944</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                     Diff: 43 upgraded</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          LayeredPackages: cockpit cockpit-machines libvirt-daemon-config-network libvirt-daemon-kvm qemu-kvm tailscale virt-install virt-viewer</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">● fedora:fedora/42/x86_64/silverblue</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                  Version: 42.20251023.0 (2025-10-23T00:38:32Z)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">               BaseCommit: 71f6dfcd9d4b76bb831f9e8a82cf244fc698d74e763de63c44cb20b0ca4d205a</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             GPGSignature: Valid signature by B0F4950458F69E1150C6C5EDC8AC4916105EF944</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          LayeredPackages: cockpit cockpit-machines libvirt-daemon-config-network libvirt-daemon-kvm qemu-kvm tailscale virt-install virt-viewer</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  fedora:fedora/42/x86_64/silverblue</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">                  Version: 42.20251023.0 (2025-10-23T00:38:32Z)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">               BaseCommit: 71f6dfcd9d4b76bb831f9e8a82cf244fc698d74e763de63c44cb20b0ca4d205a</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">             GPGSignature: Valid signature by B0F4950458F69E1150C6C5EDC8AC4916105EF944</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          LayeredPackages: cockpit cockpit-machines libvirt-daemon-config-network libvirt-daemon-kvm qemu-kvm tailscale virt-install virt-viewer</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            LocalPackages: cursor-1.7.33-1759462983.el8.x86_64</span><br></span></code></pre></div></div>
<p>I see this as a very interesting transition from traditional mutable systems, where you interact with the package manager to install, remove, and upgrade packages in your current system. rpm-ostree gives you a similar feeling, with the major difference that the system you're interacting with is the upcoming one. The other project that comes to mind is ABRoots, but I don't know enough about it to compare them.</p>
<p>I can also diff the database to see how those 43 packages upgrade (I'm only showing three here for the sake of readability):</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">mauro@workstation:~/Personal$ rpm-ostree db diff</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">ostree diff commit from: booted deployment (df21230f0d8c842212b5530a492eaec12d9735aa259a9da3f81abccc6d67d29a)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">ostree diff commit to:   pending deployment (ad6195067ab96b2b8f9d776b9b5b5346fa2575818efab36cbf6c4b314440303a)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Upgraded:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  intel-gpu-firmware 20251011-1.fc42 -&gt; 20251021-1.fc42</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ...</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  python3 3.13.7-1.fc42 -&gt; 3.13.9-1.fc42</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  ...</span><br></span></code></pre></div></div>
<p>This is how I've installed libvirt and Tailscale:</p>
<p>For libvirt it was as simple as running:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">sudo rpm-ostree install libvirt</span><br></span></code></pre></div></div>
<p>For Tailscale, you need to add its sources:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># add tailscale package to the root filesystem</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">$ sudo ostree remote add tailscale https://pkgs.tailscale.com/stable/fedora/tailscale.repo</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">$ sudo rpm-ostree install tailscale</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># reboot</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">$ sudo reboot</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># now register the device into the tailnet</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">$ sudo systemctl enable --now tailscaled</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">$ sudo tailscale up</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># there will be a URL displayed, use it to add the device to the tailnet.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># after you've registered it, it will automatically connect every time</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># you reboot.</span><br></span></code></pre></div></div>
<p>Taken from <a href="https://snikt.net/blog/2025/04/07/using-tailscale-on-fedora-silverblue/" target="_blank" rel="noopener noreferrer" class="">Andreas Happe's blog</a></p>
<p>So far I haven't encountered any issues layering my images, but I did notice on some videos and forums that people tend to avoid layering at all costs. I don't know if this is Fedora Silverblue's official suggestion or just a community thing, but I don't think it's good advice. Having built so many immutable systems now with Kairos, I don't see the problem with diverging from the upstream system as long as it's a deliberate design decision. In my example, the packages are meant to run in kernel space, so I don't even know if there would be a way to add them without layering, so there should be no shame in doing that.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="other-ways-to-extend-the-system">Other ways to extend the system<a href="https://www.mauromorales.com/posts/fedora-sivlerblue/#other-ways-to-extend-the-system" class="hash-link" aria-label="Direct link to Other ways to extend the system" title="Direct link to Other ways to extend the system" translate="no">​</a></h3>
<p>As I mentioned before, there are more ways to extend the system outside these possibilities; you can lean on systemd's system extensions, ship custom binaries, and what not. So far, I've used an AppImage and a binary to install Cursor's IDE and CLI respectively. The problem with this approach is that you're on your own, so it's up to you or the app developer to make sure you're getting updates with the necessary security patches. In the case of Cursor it does self-update, but that won't be the case for all apps.</p>
<p>Talking about Cursor, on their website they recommend installing their rpm/deb package instead of the AppImage, so first I tried installing it within the toolbox. That resulted in Cursor complaining about the keyring and what not, so I ended up installing it with rpm-ostree, which worked well but required me to repeat the process whenever I wanted to upgrade, and that's a bit too much for my personal taste. So I decided to test the AppImage, and it hasn't given me any trouble during the time I've been using it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://www.mauromorales.com/posts/fedora-sivlerblue/#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>I've tried to stay objective in the previous sections. My goal with this post is not to praise or trash Fedora Silverblue, but simply to share the thoughts that come to mind. These thoughts are obviously biased by my experience and personal preference.</p>
<p>Traditional and immutable systems are completely different approaches to managing operating systems, and both are valid. We just happen to be more familiar with traditional systems when we talk about desktop environments. If we look at smartphones, we'd realize we've been using "immutable" systems for a long time already.</p>
<p>Immutable systems are a different beast, and some aspects of their architecture might get you scratching your head and thinking, "why do they make things so difficult? I just want to install a package." I think that's the wrong way to look at them. While adding packages can feel harder, things like reproducibility and security often feel easier. That's exactly where I'm a bit confused about Silverblue's value proposition. It does make it easier to manage packages between images, but there's still a learning curve for doing everything in the immutable way. At the same time, I don't see it offering Trusted Boot for enhanced security or, more importantly, a workflow that lets me start from container images when I'm dealing with servers or appliances. Maybe that's why there's so much focus on bootc right now, but I'm not savvy enough with Fedora Silverblue, rpm-ostree, or bootc to say if that's correct. Bottom line, for me it's mostly useful to learn their technologies.</p>
<p>Let me elaborate on that a bit because I don't want to sound unfair. My main goal in running Linux on my workstation is development. There's nothing, IMO, that Fedora Silverblue offers me today that is crucial to get my work done; instead, I end up learning its quirks and being part of a niche community. And the only reason I'm using it is because I want to learn more about how other immutable systems operate. For example, I'd also like to test NixOS at some point. On that front, rpm-ostree, Podman, and toolboxes are really cool. On the other hand, for servers or appliances it's even a "downgrade" in terms of experience compared to Kairos.</p>
<p>Is there someone I think this is a good option for? Yes. If you're someone who does most things in the browser or apps you can find in the Store, this is actually a great system because you don't have to worry about the OS at all, just like you don't care about the OS on your phone.</p>
<p>Also, in case you're wondering why I try Fedora Silverblue instead of just running Kairos, there's a good reason for that. Kairos official images are aimed at "server" use cases, like when you download the server version of Ubuntu or Fedora, not the desktop one. And sure, I could build a desktop myself, like Jacques Landru does in his Polimorphic OS <a href="https://framagit.org/j-landru/abcd" target="_blank" rel="noopener noreferrer" class="">abcd</a>, which I might do at some point, but precisely because I haven't managed such a system, I'm interested in learning how other immutable systems do it to see what I'd have to deal with.</p>
<p>So will I keep using Fedora Silverblue as my daily driver? Yes, but I'm not sure for how long; unless something blocks my work at Kairos, or another distro tempts me, I'll stick with it. If you're already using Silverblue, send any tips my way. Would you try an immutable desktop like Silverblue yourself? What's still blocking you from putting immutable systems on your servers?</p>
<p>Update 2025-11-23</p>
<p>When working on Kairos, I need to be able to build a container on a different architecture than the host system. It's been a few weeks since I tried this and I don't have the issues in mind anymore, but let's just say it was a terrible experience. So, I'm not using Silverblue any more.</p>]]></content:encoded>
            <category>Immutable OS</category>
            <category>Fedora Silverblue</category>
            <category>rpm-ostree</category>
            <category>Flatpak</category>
            <category>Development Setup</category>
        </item>
        <item>
            <title><![CDATA[Principles for SOLID Systems]]></title>
            <link>https://www.mauromorales.com/posts/principles-for-solid-systems/</link>
            <guid>https://www.mauromorales.com/posts/principles-for-solid-systems/</guid>
            <pubDate>Thu, 16 Oct 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>We are the lessons we've learned along the way, and we try to apply them at different levels of our experience.</p>
<p>One of my favorite lessons as a software developer was the <a href="https://en.wikipedia.org/wiki/SOLID" target="_blank" rel="noopener noreferrer" class="">SOLID</a> principles from object-oriented programming. They were never canonical rules about how to write code, but they made me a better Rubyist, not because they told me <em>what</em> to do, but because they made me think <em>why</em> I was doing it.</p>
<p>Today, I find myself thinking in the same terms, but at a deeper layer of the stack: the operating system.</p>
<p><em>Some of the original SOLID names have been adapted to fit this context, and, just like before, these are not laws, only principles I personally find valuable when building robust, secure, and reproducible systems.</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="srp---single-responsibility-principle">SRP - Single Responsibility Principle<a href="https://www.mauromorales.com/posts/principles-for-solid-systems/#srp---single-responsibility-principle" class="hash-link" aria-label="Direct link to SRP - Single Responsibility Principle" title="Direct link to SRP - Single Responsibility Principle" translate="no">​</a></h2>
<p>A Single-Purpose Operating System (SPOS) has one responsibility, in contrast to general-purpose OSes like the one I'm using to write this post, which I also use for software development, video calls, entertainment, online shopping, and so on.</p>
<p>A SPOS is more like your ISP's router or a smart appliance. Historically, these systems only existed in embedded environments, but we no longer have to limit ourselves to that model thanks to the advancements in containerization.</p>
<p>To achieve this, we try to slim down the image to only that which is necessary, the benefits: the attack surface shrinks, audits become easeir, and updates risk less unintended side effects.</p>
<p><strong>Smell:</strong> You maintain a single generic image that gets heavily configured at runtime for different roles. Instead, build slim, purpose-built images or extend them cleanly (see OCP).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ocp---open-for-extension-closed-for-modification">OCP - Open for Extension, Closed for Modification<a href="https://www.mauromorales.com/posts/principles-for-solid-systems/#ocp---open-for-extension-closed-for-modification" class="hash-link" aria-label="Direct link to OCP - Open for Extension, Closed for Modification" title="Direct link to OCP - Open for Extension, Closed for Modification" translate="no">​</a></h2>
<p>Once your base system has a single responsibility, it should remain closed for modification but open for extension.</p>
<p>Avoid touching the system definition and instead, extend the system through mechanisms like <strong>system extensions</strong>, <strong>bundles</strong>, or other. Ideally pick those that can be verified or measured.</p>
<p><strong>Smell:</strong> Two machines serve the same purpose but require different firmware, so you bake all firmware into the base image. Instead, keep the base sealed and extend only the firmware required for that specific system.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="lsp---lifecycle-stability-principle">LSP - Lifecycle Stability Principle<a href="https://www.mauromorales.com/posts/principles-for-solid-systems/#lsp---lifecycle-stability-principle" class="hash-link" aria-label="Direct link to LSP - Lifecycle Stability Principle" title="Direct link to LSP - Lifecycle Stability Principle" translate="no">​</a></h2>
<p>Every SOLID system should maintain a <strong>stable lifecycle</strong>: creation, deployment, upgrade, and rollback should all be predictable and reversible. By no means, should the system fail to boot after a lifecycle operation, or if it does it should automatically try the last running system.</p>
<p>In addition to those, there should be a <strong>factory reset</strong> mechanism to restore the base image and clearing runtime state without re-provisioning the machine from scratch. This operation is not reversible by design. And a <strong>recovery mode</strong> to debug, but keep in mind that this last one should be used like an airplane's black box, it's something that you know is there just for the very worst case scenarios.</p>
<p>When those guarantees hold, you can trust the system even in an edge location where you cannot easily go in and investigate the state of the system.</p>
<p><strong>Smell:</strong> You constantly find yourself accessing the recovery mode of the system, because upgrades leave the system in a broken sate.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="isp---immutable-system-principle">ISP - Immutable System Principle<a href="https://www.mauromorales.com/posts/principles-for-solid-systems/#isp---immutable-system-principle" class="hash-link" aria-label="Direct link to ISP - Immutable System Principle" title="Direct link to ISP - Immutable System Principle" translate="no">​</a></h2>
<p>To achieve LSP, provisioning and upgrading need to be atomic operations, either they are done or not, there should not be an inbetween state. This is hard to achieve with GPOS where the system can fail finishig upgrading a list of packages and yet still boots. While it might sound like a positive thing, it is not because all the sudden you find yourself in an unknown state. Whether your applications work with this set of packages is a matter of luck and not something you have previously tested. You are likekly to have this kind of problem even if you're using a configuration management sytems.</p>
<p>Talking about CMS, we also need to address the elephant in the room. While these systems are meant to follow a recipe that will leave every system looking the same, they have a design flaw, they depend on the upstream source not changing, which we know it's not the case. In practical terms, this means that having defined that you want a certain package present in your system is not SOLID because if you run this process at different moments you might get different results. Even if you pin the version number, the dependencies could change at the source. The worst part of it is how they have been sold as the final solution for managing "cattle" leaving companies thinking they are secure and auditable, while these are just smoke and mirrors.</p>
<p>The only way out of this rat race is <strong>immutability</strong>.</p>
<p><strong>Smell:</strong> After provisioning a fleet, you find that at least one of the machines differes even if just slightly from the others</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dip---dependency-inversion-principle">DIP - Dependency Inversion Principle<a href="https://www.mauromorales.com/posts/principles-for-solid-systems/#dip---dependency-inversion-principle" class="hash-link" aria-label="Direct link to DIP - Dependency Inversion Principle" title="Direct link to DIP - Dependency Inversion Principle" translate="no">​</a></h2>
<p>The immutable core should depend on <strong>stable abstractions</strong>, not on external, changing details.<br>
<!-- -->A change at the outer layer, say, configuration, shouldn't require rebuilding the entire image.</p>
<p>Immutability isn't about freezing everything, it's about <strong>controlling where change is allowed to happen</strong>. When a system's base layer is immutable, changes are isolated to clearly defined areas (data partitions, overlays, or extensions). Overlay partitions can even reset drift automatically. Any unauthorized change disappears on reboot because the base image is reapplied. And because updates land through fresh images, frequent reboots become an asset: there is no prize for the longest-running, unpatched node when every restart brings you back to a known-good state. This separation keeps the core reproducible and secure while still allowing flexibility where it's needed.</p>
<p>The system layout should reflect that principle: rebuilds only for base components changes (kernel, security), while higher-level ones happen through configuration or declarative overlays.</p>
<p><strong>Smell:</strong> You tweak a config file and suddenly need to rebuild the whole OS image. That's an inversion failure, the dependency direction is wrong.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://www.mauromorales.com/posts/principles-for-solid-systems/#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>By reinterpreting SOLID through this lens, we can design systems that are stable at their core yet flexible at their edges, systems we can automate through our pipelines just like we already do with code, and that remain robust enough to trust in production, whether in a datacenter or at the edge.</p>
<p>To put the principles into practice:</p>
<ul>
<li class="">Start by isolating the single purpose of each image, even if it means producing more artifacts.</li>
<li class="">Lock the base layer, then expose controlled extension points for firmware, drivers, or workloads.</li>
<li class="">Make lifecycle guarantees explicit: document the upgrade path, rollback trigger, and recovery tool.</li>
<li class="">Keep mutability quarantined to known surfaces so audits and drift detection stay tractable.</li>
<li class="">Route environment-specific details through declarative inputs instead of image rebuilds.</li>
</ul>
<p>Just like SOLID once made our codebases easier to maintain, these principles can do the same for our systems, helping us build a world of software and infrastructure that's truly <strong>SOLID</strong>, in name and execution.</p>]]></content:encoded>
            <category>operating-systems</category>
            <category>immutable-infrastructure</category>
            <category>systems-architecture</category>
            <category>devops</category>
            <category>security</category>
        </item>
        <item>
            <title><![CDATA[The Birth of My Homelab]]></title>
            <link>https://www.mauromorales.com/posts/the-birth-of-my-homelab/</link>
            <guid>https://www.mauromorales.com/posts/the-birth-of-my-homelab/</guid>
            <pubDate>Mon, 04 Aug 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Last week, the open-source team at Spectro Cloud held a <strong>Hackweek</strong>. If you haven’t heard of the format, it’s basically a week where we don’t focus on the highest-priority tickets. Instead, we work on things we <em>want</em> to work on, whether it’s for fun, exploration, or finally giving some love to that neglected project in the corner. I find Hackweeks incredibly valuable. In fact, I’ve written about them before, <a href="https://www.mauromorales.com/2020/09/28/using-a-hackathon-to-stress-test-your-development-process/" target="_blank" rel="noopener noreferrer" class="">once in 2020</a> and even earlier <a href="https://www.mauromorales.com/2015/12/19/yes-ship-it/" target="_blank" rel="noopener noreferrer" class="">back in 2015</a>. They spark ideas, rekindle curiosity, and sometimes result in something genuinely useful.</p>
<p>One thing I’ve been meaning to do for a while is level up my Kubernetes game.</p>
<p>That might sound odd. I work on <a href="https://kairos.io/" target="_blank" rel="noopener noreferrer" class="">Kairos</a>, a Linux-based OS for running Kubernetes, but in practice, most of my day-to-day work is in Linux, containers, and Go. I rarely <em>need</em> to interact with Kubernetes itself. It reminds me of Matz, the creator of Ruby, saying he’s actually a C developer. I guess it's not that uncommon in our field. Still, I want to get better at Kubernetes because the better I understand it, the better I can support Kairos users. And the best way I learn? By building stuff.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-setup">The setup<a href="https://www.mauromorales.com/posts/the-birth-of-my-homelab/#the-setup" class="hash-link" aria-label="Direct link to The setup" title="Direct link to The setup" translate="no">​</a></h2>
<p>The week before Hackweek, I went on eBay to buy some used machines for a home cluster. That plan flopped. The seller completely botched the delivery, and I had to request a refund. Thankfully, I found a local shop instead: <a href="https://www.tweedehandslaptop.com/" target="_blank" rel="noopener noreferrer" class="">tweedehandslaptop.com</a>. Not the cheapest option, but totally worth it. Fast service, even during the Belgian summer slowdown. For now, I only bought one node to start small and grow organically.</p>
<p>The node is an HP ProDesk 600 G4 Mini with an Intel Core i5 8500T (8th gen, 2.1 GHz), Intel UHD Graphics 630, 16 GB of RAM, and a 256 GB SSD. I got it for €275. I can already hear the fan even when it’s running without a load, so I’m wondering how bad that will get in the future, but my inner geek thinks it’s worth the trouble. The plan is to eventually add an amd64 worker node that also handles storage, plus a Raspberry Pi 4 to run arm64 workloads and connect to physical peripherals like a doorbell and a camera.</p>
<p>I also picked up a copy of <em><a href="https://nostarch.com/book-kubernetes" target="_blank" rel="noopener noreferrer" class="">The Book of Kubernetes</a></em>, which I’ve been enjoying. The exercises are hands-on and solid, but it reminded me why I enjoy working on Kairos.</p>
<p>Configuration management systems like Ansible feel so much heavier compared to working with immutable systems, where you start with the final image. And it’s not just about complexity. It’s kind of wild that everyone has to rerun the configuration of their system from scratch. With an immutable image, you save time <em>and</em> resources by building it once and distributing it. It's more ecological too, you’re not pointlessly running the same setup hundreds of times across different machines and users. Imagine if we didn’t use caching in other areas of computing. It’d be madness! And yet, somehow, our industry is still stuck in the CMS world. But we’re changing that. 😉</p>
<p>Special shoutout to <a href="https://framagit.org/j-landru/abcd" target="_blank" rel="noopener noreferrer" class="">abcd</a> by Jacques Landru, who uses Kairos to teach computer networking. That’s a great example of how infrastructure should be built for learning environments.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="building-the-image">Building the image<a href="https://www.mauromorales.com/posts/the-birth-of-my-homelab/#building-the-image" class="hash-link" aria-label="Direct link to Building the image" title="Direct link to Building the image" translate="no">​</a></h2>
<p>One of the first things you need to get used to with immutable systems is the need to rebuild the image for any change you make. While you're still learning, it's tempting to rely on a GUI, but sooner or later, that workflow needs to move into a pipeline.</p>
<p>At Kairos, we’ve worked hard to make that pipeline as smooth as possible. Tools like <a href="https://github.com/kairos-io/AuroraBoot" target="_blank" rel="noopener noreferrer" class="">AuroraBoot</a> offer both a web UI and a CLI, and you can plug the latter straight into your automation. Still, I wanted something <em>even simpler</em>, especially for day-one users.</p>
<p>That’s why my main Hackweek project was creating the <a href="https://github.com/kairos-io/kairos-factory-action" target="_blank" rel="noopener noreferrer" class="">kairos-factory-action</a>: a GitHub Action that makes it ridiculously easy to produce Kairos images from a repository. You don’t need to write a Dockerfile or understand the internals of AuroraBoot to get started. Just drop in a workflow like this:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> Docker Build</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">uses</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> kairos</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">io/kairos</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">factory</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">action/.github/workflows/reusable</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">factory.yaml@main</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">secrets</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">registry_username</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> secrets.REGISTRY_USERNAME </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">registry_password</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> secrets.REGISTRY_PASSWORD </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">with</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">base_image</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ubuntu</span><span class="token punctuation" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">24.04</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">model</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"generic"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">arch</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"amd64"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">version</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"auto"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">iso</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">grype</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">registry_domain</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"quay.io"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">registry_namespace</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"mauromorales"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">summary_artifacts</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">kubernetes_distro</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"k0s"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">kubernetes_version</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"v1.33.3+k0s.0"</span><br></span></code></pre></div></div>
<p>This snippet declares the base image, Kubernetes distribution, version, and output format (an ISO, in my case). No boilerplate needed.</p>
<p>Boot, browser, boom</p>
<p>Once the GitHub Action finishes, you download the ISO, flash it to a USB stick, and boot up the machine. Open a browser at IP:8080, paste your config, and boom, single-node cluster running.</p>
<p>The <a href="https://github.com/mauromorales/homelab/blob/e9826e5bad773dba65d9a4bf615522a791db395a/nodes/protos.yaml" target="_blank" rel="noopener noreferrer" class="">configuration at this point is very simple</a>, I mostly followed <a href="https://kairos.io/docs/examples/single-node/" target="_blank" rel="noopener noreferrer" class="">Kairos' example for setting up a manual single-node cluster with k0s</a> with one small tweak: instead of using the <code>--single</code> flag, I passed <code>--enable-worker</code> and <code>--no-taints</code>. This lets me run it as a single-node cluster for now, while keeping the door open to add more nodes later.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">k0s:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  enabled: true</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  args:</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    - --enable-worker</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    - --no-taints</span><br></span></code></pre></div></div>
<p>If I want to change something, I just commit it to my repo. The action builds a new image, I point the node to it, and upgrade. No manual builds. It honestly feels a bit like updating an Apple device: the OS just shows up ready to go.</p>
<p>It’s a small cluster (so far), but seeing it boot for the first time felt like flipping the switch on a personal cloud. Can’t wait to see what else I can build with it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-steps">Next steps<a href="https://www.mauromorales.com/posts/the-birth-of-my-homelab/#next-steps" class="hash-link" aria-label="Direct link to Next steps" title="Direct link to Next steps" translate="no">​</a></h2>
<p>First thing I wanted to deploy? My doorbell app. But then I realized I wanted to tweak a couple things.</p>
<p>That led to a new project: Mowa, short for MacOS Web API. It’ll let my cluster talk to my Mac Mini programmatically. More on that in a follow-up post.</p>]]></content:encoded>
            <category>Kubernetes</category>
            <category>Homelab</category>
        </item>
        <item>
            <title><![CDATA[Coding with AI: From Blank Canvas to Working App in a Day]]></title>
            <link>https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/</link>
            <guid>https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/</guid>
            <pubDate>Sun, 01 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[An honest look at building a minimalist web app with AI, starting from a single prompt and iterating through the highs, lows, and surprises of coding with ChatGPT and Cursor.]]></description>
            <content:encoded><![CDATA[<p>This post documents what it felt like to build software with AI, from a clean slate, minimal tooling, and a curious mindset. The goal isn’t to teach, because I’m not an expert on the topic of Coding with AI. I’ve been coding professionally for years, but working with AI like this is still new territory. These notes capture my experiments, prompts, roadblocks, and small wins along the way.</p>
<p>If you’ve got tips or stories of your own, I’d love to hear them.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-experiment">The experiment<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#the-experiment" class="hash-link" aria-label="Direct link to The experiment" title="Direct link to The experiment" translate="no">​</a></h2>
<p>I wanted to build something simple: a web app that visualizes your life in weeks. Each week is a box. Past weeks are dark, future ones are light. You choose your birthdate, and it instantly shows you how many weeks have passed, how many are left, and what percentage of your (approximate) 4000-week lifespan you've lived.</p>
<p>I used ChatGPT to help craft the initial prompt, iterate on mockups, and step in for debugging when Cursor got stuck in a loop. Most of the development happened in the Cursor IDE.</p>
<p>You can try it here: <a href="https://4000.mauromorales.com/" target="_blank" rel="noopener noreferrer" class="">4000.mauromorales.com</a>. Code’s on <a href="https://github.com/mauromorales/4000" target="_blank" rel="noopener noreferrer" class="">GitHub</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-i-start-with-a-prompt-for-the-prompt">Why I Start with a Prompt for the Prompt<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#why-i-start-with-a-prompt-for-the-prompt" class="hash-link" aria-label="Direct link to Why I Start with a Prompt for the Prompt" title="Direct link to Why I Start with a Prompt for the Prompt" translate="no">​</a></h2>
<p>In my experience, I get better results when I first ask ChatGPT to generate a prompt based on my own rough notes or ideas, rather than jumping straight into writing one myself. You might wonder: if I already know what I want, why not just write the full prompt directly?</p>
<p>For me, it’s a bit like brainstorming. I might have a clear goal in mind, or just a few scattered thoughts, and by handing those to ChatGPT, I get back a structured version of what I’m trying to say. Reading that output helps me spot gaps, clarify assumptions, and refine the ask. It’s like having someone else paraphrase your request so you can check: “Did you get what I meant?” That loop has helped me avoid misunderstandings and sharpen the initial direction before any code gets written.</p>
<p>Here’s the initial prompt I gave ChatGPT:</p>
<blockquote>
<p>Help me write a prompt to build one single page app that has 4000 boxes each representing a week of a persons life. Every week that has passed will be painted in black or a dark color every coming week in white or a light color. Ideally all black and white. There is a date picker to select your birthdate, once elected it will automatically calculate how many weeks from the 4000 have passed and paint them, and how many are pending and keep them clear. A percentage is also displayed next to the row where the black and white boxes meet.</p>
</blockquote>
<p>From there, we riffed on mockups (see image below) and iterated until it felt right. I asked ChatGPT to generate a prompt for Cursor (my AI coding assistant), restricting it to Tailwind only, no other dependencies. In hindsight, that constraint backfired: it kept defaulting to raw CSS, which didn’t play well with Tailwind’s utility-first mindset.</p>
<p><img decoding="async" loading="lazy" alt="mockups generated by ChatGPT" src="https://www.mauromorales.com/assets/images/your-life-in-weeks-mockups-a2418c5788f7f58103c8c017eb4f1ee6.png" width="1024" height="1024" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="coding-with-ai-what-it-was-like">Coding with AI: What it was like<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#coding-with-ai-what-it-was-like" class="hash-link" aria-label="Direct link to Coding with AI: What it was like" title="Direct link to Coding with AI: What it was like" translate="no">​</a></h2>
<p>The first version worked. It looked close enough to the mockup. But it didn’t hold up on mobile, and dark/light mode was glitchy. I replaced the native date picker with one from Flowbite, which broke everything. The AI couldn’t fix it, so I had to step in and debug manually.</p>
<p>Other rough edges:</p>
<ul>
<li class="">The static app needed fingerprinting for CSS/JS assets to avoid caching issues. When I asked, the AI only did CSS. I had to request JS separately. It worked in the end, but required hand-holding.</li>
<li class="">I requested a small feature to load a date via the URL: nailed on the first try.</li>
<li class="">I asked for a randomizer mode (simulate life expectancy between 0 and 5000 weeks): almost worked, just needed tweaks.</li>
</ul>
<p>All in all, it took a couple of hours from idea to delivery.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pairing-with-ai-isnt-like-pairing-with-a-person-and-that-matters">Pairing with AI isn’t like pairing with a person, and that matters<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#pairing-with-ai-isnt-like-pairing-with-a-person-and-that-matters" class="hash-link" aria-label="Direct link to Pairing with AI isn’t like pairing with a person, and that matters" title="Direct link to Pairing with AI isn’t like pairing with a person, and that matters" translate="no">​</a></h2>
<p>So far, I see AI like a pairing partner. It’s good at some things, but not at everything.</p>
<p>One big difference: it’s always confident. A human pair might say “I’m not sure,” or you’d pick up on hesitation in their tone or body language. AI doesn’t do that. I’d love if these tools could express uncertainty in their language, using phrases like “I think”, “maybe”, or “this tends to work” when the solution isn’t guaranteed.</p>
<p>Another loss: speed. The AI writes code instantly, which leaves no room for the natural pauses that happen when pairing with a human. You miss the chance to say: “What command did you use just now?”, “Why that approach?”, or “Oh, this reminds me of…” Those micro-reflections spark understanding, and sometimes, new ideas. Without them, the process becomes transactional, and I think that’s a missed opportunity.</p>
<p>Maybe it’s my fault for expecting it to feel like real pairing. Pairing isn’t just about building. It’s about learning together, spreading context, and sharing reasoning. Maybe I need a better analogy. And maybe finding that analogy will help me use AI more effectively.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-i-learned">What I learned<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#what-i-learned" class="hash-link" aria-label="Direct link to What I learned" title="Direct link to What I learned" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-clean-code-isnt-the-goal-anymore">1. Clean code isn’t the goal anymore<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#1-clean-code-isnt-the-goal-anymore" class="hash-link" aria-label="Direct link to 1. Clean code isn’t the goal anymore" title="Direct link to 1. Clean code isn’t the goal anymore" translate="no">​</a></h3>
<p>I was talking to a friend at a big tech company, and we ended up discussing how clean code isn’t always a priority, especially at scale. In a monolith, you care about maintainability. You can’t just throw everything away each time you want to make a change. But with microservices, it’s different. They’re small, isolated, and often disposable. Sometimes, rewriting is cheaper than refactoring.</p>
<p>That’s where AI starts to shine. It’s often faster to generate a scoped, clean version of a feature than to surgically patch bloated legacy code.</p>
<p>This app wasn’t meant to be a microservice, but it’s small enough that the same principle applies. I haven’t read the full codebase, and honestly, I don’t feel the need to. Some decisions along the way already feel like prototype-quality work, not something I’d want to maintain long-term. And maybe that’s okay. If the cost of rewriting is near-zero, maybe “clean” doesn’t matter in the same way it used to.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-debugging-feels-like-context-switching-into-someone-elses-project">2. Debugging feels like context-switching into someone else’s project<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#2-debugging-feels-like-context-switching-into-someone-elses-project" class="hash-link" aria-label="Direct link to 2. Debugging feels like context-switching into someone else’s project" title="Direct link to 2. Debugging feels like context-switching into someone else’s project" translate="no">​</a></h3>
<p>Even when I technically wrote the code, the AI rewrote pieces so often that I’d revisit "done" sections to make sure nothing silently broke. It felt like jumping into a fresh repo each time.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-ai-doesnt-validate-its-own-work">3. AI doesn’t validate its own work<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#3-ai-doesnt-validate-its-own-work" class="hash-link" aria-label="Direct link to 3. AI doesn’t validate its own work" title="Direct link to 3. AI doesn’t validate its own work" translate="no">​</a></h3>
<p>It happily rewrote working code without checking whether the change actually functioned. For a simple app, I could play QA. But for anything more complex, this is risky, especially for people who don’t know what testing even looks like. At a minimum, AI tools should surface test coverage, suggest test cases, or provide validation options.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-the-right-words-matter">4. The right words matter<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#4-the-right-words-matter" class="hash-link" aria-label="Direct link to 4. The right words matter" title="Direct link to 4. The right words matter" translate="no">​</a></h3>
<p>I wasted time with vague phrasing like “the styles aren’t updating.” When I got specific, e.g., asking <em>how to fingerprint the style.css to avoid cache issues</em>, the AI nailed it.</p>
<p>Lesson: technical vocabulary = better results.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-screenshots-helped-but-were-not-enough">5. Screenshots helped but were not enough.<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#5-screenshots-helped-but-were-not-enough" class="hash-link" aria-label="Direct link to 5. Screenshots helped but were not enough." title="Direct link to 5. Screenshots helped but were not enough." translate="no">​</a></h3>
<p>Combining screenshots with text worked better than text alone, especially when describing visual bugs or layout issues. But layout was a challenge. Sometimes it wasn’t enough to describe what I wanted visually, even with screenshots. In some cases, I had to explicitly tell the AI: “Put this <code>&lt;div&gt;</code> inside that one, add this class, give it this attribute.” The specificity mattered. The vaguer I was, the more it hallucinated or improvised in the wrong direction.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="6-it-never-pushed-back">6. It never pushed back<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#6-it-never-pushed-back" class="hash-link" aria-label="Direct link to 6. It never pushed back" title="Direct link to 6. It never pushed back" translate="no">​</a></h3>
<p>The AI treated my prompts as gospel, even when better options existed. It never suggested fingerprinting early, or warned that it preferred raw CSS over Tailwind. It didn’t offer alternatives, opinions, or guardrails. I want tools that challenge my assumptions, especially when it comes to things like security.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="7-ai-generated-git-commits-hit-or-miss">7. AI-generated Git commits: hit or miss<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#7-ai-generated-git-commits-hit-or-miss" class="hash-link" aria-label="Direct link to 7. AI-generated Git commits: hit or miss" title="Direct link to 7. AI-generated Git commits: hit or miss" translate="no">​</a></h3>
<p>I experimented with letting AI write my Git commit messages. Sometimes they were great. Other times they just echoed obvious file changes, which I’d rather avoid.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="8-delegating-front-end-work-felt-great">8. Delegating front-end work felt great<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#8-delegating-front-end-work-felt-great" class="hash-link" aria-label="Direct link to 8. Delegating front-end work felt great" title="Direct link to 8. Delegating front-end work felt great" translate="no">​</a></h3>
<p>I’m more of a back-end dev, so I was happy to hand off the UI. I still reviewed and tweaked things, but I didn’t get bogged down in unfamiliar territory. If tools like these keep improving, maybe we’ll all become full-stack by default, not because we master every layer, but because we can delegate what we don’t specialize in.</p>
<p>That said, deep expertise still matters. Especially for architecture, scaling, or debugging hard problems.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="9-abstractions-raise-the-floor-but-also-the-ceiling">9. Abstractions raise the floor, but also the ceiling<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#9-abstractions-raise-the-floor-but-also-the-ceiling" class="hash-link" aria-label="Direct link to 9. Abstractions raise the floor, but also the ceiling" title="Direct link to 9. Abstractions raise the floor, but also the ceiling" translate="no">​</a></h3>
<p>Some worry that using AI makes your coding muscles weak. I get that. But it feels more like moving to a higher-level language. You still need to understand what’s happening under the hood to use it well. It’s not a shortcut past skill. It’s a shift in where skill gets applied.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wrapping-up">Wrapping up<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#wrapping-up" class="hash-link" aria-label="Direct link to Wrapping up" title="Direct link to Wrapping up" translate="no">​</a></h2>
<p>This project reminded me that good prompts matter, but so does good judgment. AI helped me ship something I might’ve abandoned otherwise. It’s not perfect, but it’s done. That’s a win.</p>
<p>If you’ve built something with AI, or you’re experimenting with it in your workflow, I’d love to hear how it went. I’m especially curious about how others are navigating the mix of delegation, debugging, and discovery. What did it help with, and where did it fall short?</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="update-what-if-the-app-didnt-need-to-be-an-app">Update: What if the app didn’t need to be an app?<a href="https://www.mauromorales.com/posts/coding-with-ai-from-blank-canvas-to-working-app-in-a-day/#update-what-if-the-app-didnt-need-to-be-an-app" class="hash-link" aria-label="Direct link to Update: What if the app didn’t need to be an app?" title="Direct link to Update: What if the app didn’t need to be an app?" translate="no">​</a></h2>
<p>After building and deploying the site, a funny thought hit me: this thing is so small. Do I even need a web app?</p>
<p>So I asked ChatGPT to generate a GPT that <em>is</em> the app.</p>
<p>Functionality-wise, it nailed it on the first try. The visual output? Awful. No matter how much we tweaked it, the graphical layout never felt right. But then we landed on a workaround: a Python script that generates an ASCII representation of the graph. Surprisingly satisfying.</p>
<p>What makes this version more interesting is the interaction model. You don’t need to pick a date from a UI, you can just say “I’m 22 years old,” or “I was born in Ecuador in 1990,” or “I’m of Asian descent.” It adapts the calculation accordingly. You can also configure the GPT to only respond about the life-weeks concept and nothing else.</p>
<p>Is it perfect? No. But for 30 minutes of experimentation, I think it’s a success, and maybe even more accessible than the web version for some people.</p>
<p>You can try it here: <a href="https://chatgpt.com/g/g-683d7eb68b708191b6701b26b638bd01-your-life-in-weeks" target="_blank" rel="noopener noreferrer" class="">Your Life in Weeks GPT</a></p>]]></content:encoded>
            <category>AI</category>
            <category>Programming</category>
            <category>Web Development</category>
            <category>Prompt Engineering</category>
        </item>
        <item>
            <title><![CDATA[Wrapping Up 2024: A Year of Growth and Connection]]></title>
            <link>https://www.mauromorales.com/posts/wrapping-up-2024/</link>
            <guid>https://www.mauromorales.com/posts/wrapping-up-2024/</guid>
            <pubDate>Wed, 11 Dec 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>As we approach the end of 2024, I find it important to reflect on the milestones achieved this year and how they've shaped my journey as a software developer, technologist, and contributor to the Cloud Native ecosystem. This post is as much about sharing insights as it is about expressing gratitude for the opportunities and support I've received.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="embracing-community-at-kairos">Embracing Community at Kairos<a href="https://www.mauromorales.com/posts/wrapping-up-2024/#embracing-community-at-kairos" class="hash-link" aria-label="Direct link to Embracing Community at Kairos" title="Direct link to Embracing Community at Kairos" translate="no">​</a></h2>
<p>Since 2023, I've been contributing to <a href="https://kairos.io/" target="_blank" rel="noopener noreferrer" class=""><strong>Kairos</strong></a> as a maintainer. This role has been a mix of technical problem-solving and building something larger than code: a community. In mid-2024, I decided to focus more on developer relations to strengthen Kairos' ecosystem.</p>
<p>Here's what this shift looked like:</p>
<ul>
<li class=""><strong>Conference Engagement</strong>: Over the year, I participated in 10 conferences across Europe and the US, including delivering a workshop and speaking on a panel. These opportunities allowed me to share Kairos' story, introduce its features, and, most importantly, connect with the Cloud Native community.</li>
<li class=""><strong>Community Growth</strong>: By fostering conversations at these events, I witnessed an increase in community engagement, feedback, and contributions to Kairos. It's a reminder that great technology thrives when supported by an active and inclusive community.</li>
</ul>
<p>This work reaffirmed my belief that building developer relationships is not just about promoting a product; it's about creating an ecosystem where developers feel heard, valued, and empowered.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="progressing-in-the-cloud-native-space">Progressing in the Cloud Native Space<a href="https://www.mauromorales.com/posts/wrapping-up-2024/#progressing-in-the-cloud-native-space" class="hash-link" aria-label="Direct link to Progressing in the Cloud Native Space" title="Direct link to Progressing in the Cloud Native Space" translate="no">​</a></h2>
<p>2024 has also been a pivotal year for my involvement in the <strong>Cloud Native Computing Foundation (CNCF)</strong>:</p>
<ul>
<li class="">I became a <strong>co-host of the <a href="https://community.cncf.io/cloud-native-brussels/" target="_blank" rel="noopener noreferrer" class="">CNCF Belgian Chapter</a></strong>, collaborating with local developers to strengthen the Cloud Native community in Belgium.</li>
<li class="">I was honored to be appointed <strong>co-chair of the <a href="https://tag-runtime.cncf.io/wgs/spos/charter/" target="_blank" rel="noopener noreferrer" class="">CNCF Special-Purpose Operating System Work Group</a></strong>. This role allows me to help shape the future of operating systems in cloud-native environments, ensuring they are secure, efficient, and innovative.</li>
</ul>
<p>These roles represent an evolution in my career, not just as a contributor but as a leader who helps drive meaningful change in the ecosystem.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="gratitude-and-acknowledgments">Gratitude and Acknowledgments<a href="https://www.mauromorales.com/posts/wrapping-up-2024/#gratitude-and-acknowledgments" class="hash-link" aria-label="Direct link to Gratitude and Acknowledgments" title="Direct link to Gratitude and Acknowledgments" translate="no">​</a></h2>
<p>None of these achievements would have been possible without the incredible support system I'm fortunate to have. I want to express my heartfelt thanks to <strong>my boss and my team</strong>. Your hard work and dedication make it possible for me to focus on community-building and technical advocacy. I'm equally grateful to my employer for supporting my growth and enabling me to take on these opportunities.</p>
<p>A special thank-you also goes out to the amazing people I've connected with this year, you know who you are. Whether through conferences, collaborations, or chance encounters, I've been genuinely delighted to meet and learn from so many talented individuals. These connections remind me that the Cloud Native community is built on the collective passion and generosity of its members.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-these-achievements-matter-to-me">Why These Achievements Matter To Me<a href="https://www.mauromorales.com/posts/wrapping-up-2024/#why-these-achievements-matter-to-me" class="hash-link" aria-label="Direct link to Why These Achievements Matter To Me" title="Direct link to Why These Achievements Matter To Me" translate="no">​</a></h2>
<p>For most of my career, I saw myself as a tech person rather than a people person. I was comfortable focusing on code, systems, and solutions, but the idea of engaging with communities or building relationships felt outside my realm. This year, by stepping out of my comfort zone and putting in the hard work, I've realized that this is not only an important area but also one I genuinely enjoy.</p>
<p>Through these experiences, I've discovered that fostering connections and growing communities is just as rewarding as solving technical challenges. It's helped me break my own paradigms and embrace the fact that building relationships is a skill worth cultivating, one that complements and enhances my work as a technologist.</p>
<hr>
<p>Looking ahead, I aim to build on this foundation by continuing to contribute to open-source projects, share knowledge, and connect with like-minded technologists. If there's one takeaway from 2024, it's that growth happens when you step out of your comfort zone and embrace collaboration.</p>
<p>Here's to another year of learning, building, and connecting!</p>]]></content:encoded>
            <category>Developer Relations</category>
            <category>Cloud Native</category>
            <category>Community</category>
            <category>Open Source</category>
            <category>Reflections</category>
        </item>
        <item>
            <title><![CDATA[How to Sync a Headless Server with Your Nextcloud Files]]></title>
            <link>https://www.mauromorales.com/posts/headless-sync-nextcloud/</link>
            <guid>https://www.mauromorales.com/posts/headless-sync-nextcloud/</guid>
            <pubDate>Fri, 08 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[When it comes to keeping your headless server synchronized with your Nextcloud server, setting up a reliable automated process is crucial. This guide will walk you through syncing top-level directories on an Ubuntu server. While this explanation uses Ubuntu, be aware that some commands, such as package installation, may vary on other Linux distributions.]]></description>
            <content:encoded><![CDATA[<p>When it comes to keeping your headless server synchronized with your Nextcloud server, setting up a reliable automated process is crucial. This guide will walk you through syncing top-level directories on an Ubuntu server. While this explanation uses Ubuntu, be aware that some commands, such as package installation, may vary on other Linux distributions.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1-install-the-nextcloud-command-line-client">Step 1: Install the Nextcloud Command-Line Client<a href="https://www.mauromorales.com/posts/headless-sync-nextcloud/#step-1-install-the-nextcloud-command-line-client" class="hash-link" aria-label="Direct link to Step 1: Install the Nextcloud Command-Line Client" title="Direct link to Step 1: Install the Nextcloud Command-Line Client" translate="no">​</a></h2>
<p>First, you'll need to install the Nextcloud command-line client to manage the synchronization:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">sudo apt install nextcloud-desktop-cmd</span><br></span></code></pre></div></div>
<p>Ensure the package installs successfully so that you can proceed with setting up automated sync tasks.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="configure-credentials">Configure Credentials<a href="https://www.mauromorales.com/posts/headless-sync-nextcloud/#configure-credentials" class="hash-link" aria-label="Direct link to Configure Credentials" title="Direct link to Configure Credentials" translate="no">​</a></h3>
<p>To enable authentication for <code>nextcloudcmd</code>, add your credentials to the <code>~/.netrc</code> file with the following format:</p>
<div class="language-plaintext codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-plaintext codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">machine your-next-cloud-domain.org</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">login your-username</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">password your-password</span><br></span></code></pre></div></div>
<p>Replace <code>your-username</code> and <code>your-password</code> with your actual Nextcloud login details. This file ensures that the command-line client can authenticate without manual input.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2-choose-directories-to-sync">Step 2: Choose Directories to Sync<a href="https://www.mauromorales.com/posts/headless-sync-nextcloud/#step-2-choose-directories-to-sync" class="hash-link" aria-label="Direct link to Step 2: Choose Directories to Sync" title="Direct link to Step 2: Choose Directories to Sync" translate="no">​</a></h2>
<p>For simplicity, I prefer syncing only the top-level directories that are important to me, such as <code>src</code> and similar folders. This approach keeps the sync process efficient and focused.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3-create-the-systemd-service-file">Step 3: Create the <code>systemd</code> Service File<a href="https://www.mauromorales.com/posts/headless-sync-nextcloud/#step-3-create-the-systemd-service-file" class="hash-link" aria-label="Direct link to step-3-create-the-systemd-service-file" title="Direct link to step-3-create-the-systemd-service-file" translate="no">​</a></h2>
<p>To automate the syncing process, create a <code>systemd</code> service file that defines how the sync should be executed. Save the following content as <code>~/.config/systemd/user/nextcloud-sync-src.service</code>:</p>
<div class="language-ini codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ini codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">[Unit]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Description=Nextcloud Sync Src</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[Service]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Type=simple</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">ExecStart=/usr/bin/nextcloudcmd -n --non-interactive --path /src /home/mauro/src https://your-next-cloud-domain.org</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">TimeoutStartSec=300</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Slice=nextcloud-sync.slice</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[Install]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">WantedBy=default.target</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4-create-the-systemd-timer-file">Step 4: Create the <code>systemd</code> Timer File<a href="https://www.mauromorales.com/posts/headless-sync-nextcloud/#step-4-create-the-systemd-timer-file" class="hash-link" aria-label="Direct link to step-4-create-the-systemd-timer-file" title="Direct link to step-4-create-the-systemd-timer-file" translate="no">​</a></h2>
<p>The <code>systemd</code> timer will trigger the service at regular intervals. Save the following as <code>~/.config/systemd/user/nextcloud-sync-src.timer</code>:</p>
<div class="language-ini codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ini codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">[Unit]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Description=Run Nextcloud Sync Src every 5 minutes</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Requires=nextcloud-sync-src.service</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[Timer]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Unit=nextcloud-sync-src.service</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">OnUnitInactiveSec=5m</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">RandomizedDelaySec=5m</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">AccuracySec=1s</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[Install]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">WantedBy=timers.target</span><br></span></code></pre></div></div>
<p>This configuration ensures that the service runs every 5 minutes with a randomized delay for slight variability.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-5-create-a-systemd-slice-file">Step 5: Create a <code>systemd</code> Slice File<a href="https://www.mauromorales.com/posts/headless-sync-nextcloud/#step-5-create-a-systemd-slice-file" class="hash-link" aria-label="Direct link to step-5-create-a-systemd-slice-file" title="Direct link to step-5-create-a-systemd-slice-file" translate="no">​</a></h2>
<p>To limit resource usage, create a slice file to manage CPU and memory allocation. Save this content as <code>~/.config/systemd/user/nextcloud-sync.slice</code>:</p>
<div class="language-ini codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ini codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">[Unit]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Description=Nextcloud Sync Slice</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">DefaultDependencies=no</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">Before=slices.target</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[Slice]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">CPUQuota=30%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">MemoryMax=2G</span><br></span></code></pre></div></div>
<p>This configuration will limit the sync process to 30% of the CPU and a maximum of 2 GB of memory.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-6-load-and-enable-the-timer">Step 6: Load and Enable the Timer<a href="https://www.mauromorales.com/posts/headless-sync-nextcloud/#step-6-load-and-enable-the-timer" class="hash-link" aria-label="Direct link to Step 6: Load and Enable the Timer" title="Direct link to Step 6: Load and Enable the Timer" translate="no">​</a></h2>
<p>To activate the service and timer, reload the <code>systemd</code> user daemon and enable the timer:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">systemctl --user daemon-reload</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">systemctl --user enable nextcloud-sync-src.timer</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">systemctl --user start nextcloud-sync-src.timer</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-7-monitor-the-service-and-timer">Step 7: Monitor the Service and Timer<a href="https://www.mauromorales.com/posts/headless-sync-nextcloud/#step-7-monitor-the-service-and-timer" class="hash-link" aria-label="Direct link to Step 7: Monitor the Service and Timer" title="Direct link to Step 7: Monitor the Service and Timer" translate="no">​</a></h2>
<p>To check the logs for the service, use:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">journalctl --user -u nextcloud-sync-src</span><br></span></code></pre></div></div>
<p>To list active timers and see when they last ran or are scheduled to run next, use:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">systemctl --user list-timers</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://www.mauromorales.com/posts/headless-sync-nextcloud/#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>With this setup, your headless server will automatically sync the specified directories with your Nextcloud server every 5 minutes. This approach keeps your essential files up-to-date without manual intervention, optimizing the workflow for your headless environment.</p>]]></content:encoded>
            <category>System Administration</category>
            <category>Linux</category>
            <category>Automation</category>
            <category>Nextcloud</category>
            <category>Small-Tech</category>
        </item>
        <item>
            <title><![CDATA[How Does a Raspberry Pi 5 Boot an image?]]></title>
            <link>https://www.mauromorales.com/posts/raspberrypi-boot/</link>
            <guid>https://www.mauromorales.com/posts/raspberrypi-boot/</guid>
            <pubDate>Mon, 12 Aug 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[When the Raspberry Pi5 is turned on, it will check on which device it is configured to boot. By default, this is the SD card, but you can change it to boot from an NVMe or USB drive while still fallback to SD. In my case, I’m using a USB SSD. Let’s take a look at how the disk is partitioned.]]></description>
            <content:encoded><![CDATA[<p>When the Raspberry Pi5 is turned on, it will check on which device it is configured to boot. By default, this is the SD card, but you can change it to boot from an NVMe or USB drive while still fallback to SD. In my case, I’m using a USB SSD. Let’s take a look at how the disk is partitioned.</p>
<p>For this article, I will be making reference to the <a href="https://ubuntu.com/download/raspberry-pi/thank-you?version=24.04&amp;architecture=server-arm64+raspi" target="_blank" rel="noopener noreferrer" class="">Ubuntu 24.04 server image</a> because its configuration is easier to understand than the Raspbian one, which uses implicit defaults. I mounted the image as a loop device, hence the <code>dev/loop44</code> in the examples, but if you burned it to an SSD or SD card, you could get the same results from <code>/dev/sdX</code> and <code>/dev/mmcblkY</code>.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">root@zeno:~# lsblk -f /dev/loop44</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">NAME       FSTYPE FSVER LABEL       UUID                                 FSAVAIL FSUSE% MOUNTPOINTS</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">loop44</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">├─loop44p1 vfat   FAT32 system-boot F526-0340                             419.3M    17% /media/mauro/system-boot</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">└─loop44p2 ext4   1.0   writable    1305c13b-200a-49e8-8083-80cd01552617  781.9M    66% /media/mauro/writable</span><br></span></code></pre></div></div>
<p>From the labels, we can assume the <code>system-boot</code> partition will be the one booting the system, but how does the system know this is the case. From the <a href="http://rptl.io/configtxt" target="_blank" rel="noopener noreferrer" class="">documentation</a>, I was able to find this:</p>
<blockquote>
<p>Partition numbers start at 1 and the MBR partitions are 1 to 4. Specifying partition 0 means boot from the default partition which is the first bootable FAT partition.</p>
<p>Bootable partitions must be formatted as FAT12, FAT16 or FAT32 and contain a start.elf file (or config.txt file on Raspberry Pi 5) in order to be classed as be bootable by the bootloader</p>
</blockquote>
<p>Looking at the output of the previous command, only the system-boot partition has the right format, so let’s look into that one first.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># ls -1 /media/mauro/system-boot/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">README</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2710-rpi-2-b.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2710-rpi-3-b-plus.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2710-rpi-3-b.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2710-rpi-cm3.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2710-rpi-zero-2-w.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2710-rpi-zero-2.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2711-rpi-4-b.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2711-rpi-400.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2711-rpi-cm4.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2711-rpi-cm4s.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2712-rpi-5-b.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2712-rpi-cm5-cm4io.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2712-rpi-cm5-cm5io.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bcm2712d0-rpi-5-b.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">boot.scr</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bootcode.bin</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">cmdline.txt</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">config.txt</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fixup.dat</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fixup4.dat</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fixup4cd.dat</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fixup4db.dat</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fixup4x.dat</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fixup_cd.dat</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fixup_db.dat</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fixup_x.dat</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">hat_map.dtb</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">initrd.img</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">meta-data</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">network-config</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">overlays</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">start.elf</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">start4.elf</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">start4cd.elf</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">start4db.elf</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">start4x.elf</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">start_cd.elf</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">start_db.elf</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">start_x.elf</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">uboot_rpi_3.bin</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">uboot_rpi_4.bin</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">uboot_rpi_arm64.bin</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">user-data</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">vmlinuz</span><br></span></code></pre></div></div>
<p>We can see that the expected config.txt. Let’s take a look at its contents.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">root@zeno:~# cat /media/mauro/system-boot/config.txt</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[all]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">kernel=vmlinuz</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">cmdline=cmdline.txt</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">initramfs initrd.img followkernel</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[pi4]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">max_framebuffers=2</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">arm_boost=1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[all]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># Enable the audio output, I2C and SPI interfaces on the GPIO header. As these</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># parameters related to the base device-tree they must appear *before* any</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># other dtoverlay= specification</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">dtparam=audio=on</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">dtparam=i2c_arm=on</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">dtparam=spi=on</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># Comment out the following line if the edges of the desktop appear outside</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># the edges of your display</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">disable_overscan=1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># If you have issues with audio, you may try uncommenting the following line</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># which forces the HDMI output into HDMI mode instead of DVI (which doesn't</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># support audio output)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">#hdmi_drive=2</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># Enable the serial pins</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">enable_uart=1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># Autoload overlays for any recognized cameras or displays that are attached</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># to the CSI/DSI ports. Please note this is for libcamera support, *not* for</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># the legacy camera stack</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">camera_auto_detect=1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">display_auto_detect=1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># Config settings specific to arm64</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">arm_64bit=1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">dtoverlay=dwc2</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># Enable the KMS ("full" KMS) graphics overlay, leaving GPU memory as the</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># default (the kernel is in control of graphics memory with full KMS)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">dtoverlay=vc4-kms-v3d</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">disable_fw_kms_setup=1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[pi3+]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># Use a smaller contiguous memory area, specifically on the 3A+ to avoid an</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># OOM oops on boot. The 3B+ is also affected by this section, but it shouldn't</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># cause any issues on that board</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">dtoverlay=vc4-kms-v3d,cma-128</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[pi02]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># The Zero 2W is another 512MB board which is occasionally affected by the same</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># OOM oops on boot.</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">dtoverlay=vc4-kms-v3d,cma-128</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[all]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[cm4]</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># Enable the USB2 outputs on the IO board (assuming your CM4 is plugged into</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"># such a board)</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">dtoverlay=dwc2,dr_mode=host</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[all]</span><br></span></code></pre></div></div>
<p>I’m only interested in the first 5 lines.</p>
<ul>
<li class=""><strong>[all]</strong>: makes reference to the board, or in this case, all boards</li>
<li class=""><strong>kernel</strong>: defines the kernel file to be read, in this case <code>vmlinuz</code> which was present on the files list</li>
<li class=""><strong>cmdline</strong>: defines the file with the cmdline used to boot the kernel. In this case <code>cmdline.txt</code> which is also there</li>
<li class=""><strong>initramfs</strong>: defines the initrd file to be loaded, in this case <code>initrd.img</code>, also there. And the <code>followkernel</code> stanza loads the initrd file in memory right after the kernel. Pay attention that this instruction, different from all others, doesn't use the assignment =.</li>
</ul>
<p>Now we can take a look into <code>cmdline.txt</code></p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># cat /media/mauro/system-boot/cmdline.txt</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">console=serial0,115200 multipath=off dwc_otg.lpm_enable=0 console=tty1 root=LABEL=writable rootfstype=ext4 rootwait fixrtc</span><br></span></code></pre></div></div>
<p>This tells us that the root of the system is a partition with the label writable. This matches with the output of our very first command. Listing everything in <code>writable</code> we find:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">root@zeno:~# ls -1 /media/mauro/writable/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bin</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">bin.usr-is-merged</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">boot</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">dev</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">etc</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">home</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">lib</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">lib.usr-is-merged</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">lost+found</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">media</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">mnt</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">opt</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">proc</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">root</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">run</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">sbin</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">sbin.usr-is-merged</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">snap</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">srv</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">sys</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">tmp</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">usr</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">var</span><br></span></code></pre></div></div>
<p>This looks like a common root directory for an Ubuntu system, so I will not go deeper into it.</p>
<p>From a Linux installation on a PC the bootloader, but it turns out that for the Pi 5 it is already part of the EEPROM. So we can trust that it’s present and following the instructions from <code>config.txt</code>.</p>
<p>An important part of this process is the Device Tree (<code>.dtb</code> files), which is also read by the bootloader. The Device Tree describes the hardware present on the board, ensuring that the kernel knows how to interact with all connected peripherals.</p>
<p>To summarize it all. When the Raspberry Pi 5 powers up, the EEPROM will look for the first bootable partition, where it will read the config.txt file. The configuration file tells the bootloader which kernel, initramfs and cmdline params to load. After that, it’s the kernel’s job to decide how to proceed, in this case after the kernel and initramfs are running in memory, it will pivot to the system living in the writable partition. Last and out of the scope of this article, the init system will finalize the system.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="raspbian">Raspbian<a href="https://www.mauromorales.com/posts/raspberrypi-boot/#raspbian" class="hash-link" aria-label="Direct link to Raspbian" title="Direct link to Raspbian" translate="no">​</a></h2>
<p>Keep in mind that the Raspbian image doesn't define all these details, since it uses defaults:</p>
<blockquote>
<p>The Raspberry Pi 5 firmware defaults to loading kernel_2712.img because this image contains optimizations specific to Raspberry Pi 5 (e.g. 16K page-size). If this file is not present, then the common 64-bit kernel (kernel8.img) will be loaded instead.</p>
</blockquote>
<p>And I assume that for initramfs, if not defined, it will also look within the directory and load either <code>initramfs8</code> or <code>initramfs_2712</code> by default since those files are present in the raspbian image.</p>]]></content:encoded>
            <category>Linux</category>
            <category>Boot Process</category>
            <category>Raspberry Pi</category>
            <category>System Architecture</category>
            <category>Embedded Systems</category>
        </item>
        <item>
            <title><![CDATA[Epomaker Tide75 Keyboard Review]]></title>
            <link>https://www.mauromorales.com/posts/epomaker-tide75-keyboard-review/</link>
            <guid>https://www.mauromorales.com/posts/epomaker-tide75-keyboard-review/</guid>
            <pubDate>Wed, 10 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[A review of the Epomaker Tide75 Keyboard. A mechanical keyboard with a 75% layout, RGB LEDs, and a knob. It can be connected via cable, dongle, or Bluetooth. It has a 4000mAh battery and can be customized using the VIA app. It is a solid keyboard, with a great sound, and a good-looking design. It is a great choice for anyone looking for a mechanical keyboard.
]]></description>
            <content:encoded><![CDATA[<p>You know that feeling when you are trying not to buy new mechanical keyboards and still one manages to find its way to your desk? Well, that’s precisely how I felt last week when a package from Epomaker arrived! For about a fortnight, I’ve been putting the Tide75 to the test, let me tell you what I found!</p>
<figure class="shortcode-figure"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-box.jpeg" alt="Epomaker Tide75 Keyboard Box"></figure>
<p><em>Disclaimer: Epomaker contacted me to write this review as an exchange for keeping the keyboard. They didn’t have a say on what I wrote, except for the two links to their web and Amazon shop.</em></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="first-impressions">First Impressions<a href="https://www.mauromorales.com/posts/epomaker-tide75-keyboard-review/#first-impressions" class="hash-link" aria-label="Direct link to First Impressions" title="Direct link to First Impressions" translate="no">​</a></h2>
<div class="shortcode-gallery"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-1.jpeg" class="grid-w33"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-2.jpeg" class="grid-w33"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-3.jpeg" class="grid-w33"></div>
<p>The first thing I noticed is the weight of this keyboard, it is thick! Immediately then, I knew this was a solid product (pun intended 🥁). The sound is a delight. And overall, it is a very good-looking keyboard. On the other hand, the knob feels a bit loose, and I’m not a fan of the font type they used for the caps.</p>
<p>But let’s not judge this book only by the cover! I spent almost two weeks with it as my daily driver to find out if this one is a keeper. My main interest in such a device is to write code and prose, so sorry gamers, I don’t have much for you. Here are the 4 areas which I assessed this keyboard on:</p>
<ol>
<li class=""><strong>Comfort</strong> with extended use</li>
<li class="">Base <strong>functionality</strong>. I don’t want to be interrupted while I’m working because something is not connecting properly or other similar issues</li>
<li class="">Extra <strong>customizations</strong>. Being able to adapt the keyboard to my needs is a great plus for me</li>
<li class=""><strong>Aesthetics</strong>. I will be looking at this piece of equipment for at least 8 hours every day</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="comfort">Comfort<a href="https://www.mauromorales.com/posts/epomaker-tide75-keyboard-review/#comfort" class="hash-link" aria-label="Direct link to Comfort" title="Direct link to Comfort" translate="no">​</a></h2>
<p>The distribution of the keys is superb. I have small hands, but it doesn’t feel like an effort to do any key combination. I’ve experienced RSI in the past, and I will invest in a good keyboard rather than experience pain. I spend hours at a time typing, and so far, I haven’t felt any discomfort with the Tide75.</p>
<p>As a macOS user, the main issue I have with the layout is that it is missing the Cmd key on the right. Thankfully, this can be customized (more on this later)!</p>
<p>I really enjoy how the keyboard will not move a millimeter while I’m typing. I compared this to my Reform keyboard, which is the closest one in size I have, and the Epomaker Tide75 is significantly better. This boy is so heavy that even my DasKeyboard 4 Professional, which is a full-sized keyboard, couldn’t compete. Now this, of course, is a double-sided sword because it means I would never consider carrying it around in a backpack.</p>
<p>One thing I still have to get used to, is the minimum touch the keys require to react, but I like it, it just means I have to change some old bad habits, like resting my thumb on the space bar.</p>
<p>As for the sound, I’m a big fan of loud keyboards (Cherry MX Blue Switches), but you cannot take such a keyboard to an office unless you want to become an outcast, this is why, on my daily drivers I have (Cherry MX Silent Red Switches) with dampers. The sound of the Epomaker Lemon Switch, sits somewhere in between these two, which I would recommend for a single person office, or one that is ok with this level of noise. I tried putting dampers on the keycaps, but it didn’t really have an effect.</p>
<div class="shortcode-video"><iframe src="https://www.youtube.com/embed/J0bHw2mKCSQ" title="YouTube video" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="functionality">Functionality<a href="https://www.mauromorales.com/posts/epomaker-tide75-keyboard-review/#functionality" class="hash-link" aria-label="Direct link to Functionality" title="Direct link to Functionality" translate="no">​</a></h2>
<p>The keyboard can be connected using a cable, a dongle, or via Bluetooth. When connected via cable, the keyboard also charges the 4000mAh battery. I attest to the quality of the charge since I had to use the cable during the different times that I configured the keyboard, I’ll make sure to report that in a later update.</p>
<p>Connecting with the dongle worked like a charm. The only complaint I have is that the little housing where it stays put feels a bit loose, maybe it’s not so bad but for me, it’s another reason not to put it inside a bag pack.</p>
<div class="shortcode-gallery"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-4.jpeg" class="grid-w50"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-5.jpeg" class="grid-w50"></div>
<p>Connecting via Bluetooth is super simple. You have 3 presets when pressing <code>Fn</code> + <code>Q/W/R</code>. Switching between them works just fine, and the right preset wakes up my computer as expected. This was very handy since I have multiple devices on my desk and I could switch between them without having to move around the desk as I used to.
At some point when testing, I started experiencing disconnections, and it turned out to be caused by low battery. A battery indicator could be a great addition. It could even be done with the existing RGB LEDs, I think that would be pretty sick!</p>
<p>Media keys for macOS can be accessed with the <code>Fn</code> + <code>digit</code>, they are even in the same position as the Apple keyboard, which is a great design choice so you don’t have to learn a new layout. The only minor issue I found was in the manual where it says that <code>Fn</code> + <code>S</code> is Mac mode and <code>Fn</code> + <code>A</code> is Win mode, but for me is the other way around (at least for mac, I don’t have a Windows machine to test).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="customizations">Customizations<a href="https://www.mauromorales.com/posts/epomaker-tide75-keyboard-review/#customizations" class="hash-link" aria-label="Direct link to Customizations" title="Direct link to Customizations" translate="no">​</a></h2>
<p>If I have a keyboard that is comfortable and the basic functionality fits my needs, then I can be happy with it, but being able to customize it, is what can make it a joy to type with a certain keyboard.</p>
<p>The manual says I can configure the layout by downloading an app from <a href="https://github.com/the-via/releases" target="_blank" rel="noopener noreferrer" class="">this repository</a>, and there is a <a href="https://formulae.brew.sh/cask/via#default" target="_blank" rel="noopener noreferrer" class="">homebrew formula</a> for it too. Unfortunately, I wasn't able to make it work. The app loads, but it never finds the keyboard. Thankfully, the <a href="https://usevia.app/" target="_blank" rel="noopener noreferrer" class="">online</a> version did work. In addition to this app, you also need to download the keyboard’s definition file. The link on the manual doesn't work, but <a href="https://epomaker.com/blogs/via-json/epomaker-tide75-qmk-via-json-file" target="_blank" rel="noopener noreferrer" class="">this one</a> does.</p>
<p>Ok, with everything ready, you can go to the “design” tab and, unlike me, make sure to turn OFF “Use V2 definitions (deprecated)” despite it being a step in the manual 🤦‍♂️. Hit the load button, and select the definition file. If things work correctly, you will see the keyboard’s layout on the page. I have the “Long Spaces” version, which is nice, but the “Split Spaces” must be better in my opinion. Now you can move to the “configure” tab, and you should see the layout with all key definitions.</p>
<div class="shortcode-gallery"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/usevia-1.png" class="grid-w50"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/usevia-2.png" class="grid-w50"></div>
<p>All changes you do to the keyboard, take effect in real time, which is pretty cool for playing with the RGB colors and such configurations. For configuring the layout, I’m still not sure if I like it because I like to make all my changes and change them at once. But it’s just a different way of working, and maybe I won’t even notice it after using it a couple of times.</p>
<p>There are 5 customizable layouts, which means lots of room for making this keyboard your own. For now, I will customize the default layer to have the right <code>Cmd</code> key, and layer 2 for having easier access to the arrow keys on the <code>hjkl</code> keys (yes, Vim has messed up this bad with my head 🙈) and the numbers. This is what it looks like:</p>
<figure class="shortcode-figure"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/layout.png" alt="My custom layout for the Epomaker Tide75 Keyboard"></figure>
<p>If you like my setup, then feel free to grab it and play with it</p>
<div class="shortcode-gist"></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="aesthetics">Aesthetics<a href="https://www.mauromorales.com/posts/epomaker-tide75-keyboard-review/#aesthetics" class="hash-link" aria-label="Direct link to Aesthetics" title="Direct link to Aesthetics" translate="no">​</a></h2>
<p>Overall, I think the keyboard looks great. The black pitch of the aluminum mixed with the copper borders and the RGB LEDs on look fantastic! Even the bottom of the keyboard looks pretty nice. This is a product that you enjoy touching, I get a similar vibe when interacting with my <a href="https://mauromorales.com/2021/04/30/mnt-reform-2-diy-kit-review/" target="_blank" rel="noopener noreferrer" class="">MNT Reform 2 Laptop</a>.</p>
<div class="shortcode-gallery"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-6.jpeg" class="grid-w50"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-7.jpeg" class="grid-w50"></div>
<p>There is just one mistake Epomaker made here. The knob border is platinized, which in my opinion doesn't mix well with the copper. I’m also not a fan of the type used, for most letters it is ok, but <code>4$</code> reads more like <code>45</code>. Words like <code>PGDN</code> give the impression that the painting went wrong, but it's just the font type they chose for it. Not a deal-breaker, but hopefully Epomaker will move away from this font in the future, check the difference with the font used in the MNT Keyboard, which is such a delight to read.</p>
<figure class="shortcode-figure"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-8.jpeg" alt="Epomaker Tide75 Keyboard Font"></figure>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="size-comparison">Size Comparison<a href="https://www.mauromorales.com/posts/epomaker-tide75-keyboard-review/#size-comparison" class="hash-link" aria-label="Direct link to Size Comparison" title="Direct link to Size Comparison" translate="no">​</a></h2>
<div class="shortcode-gallery"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-9.jpeg" class="grid-w33"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-10.jpeg" class="grid-w33"><img src="https://www.mauromorales.com/img/posts/epomaker-tide75-keyboard-review/tide75-11.jpeg" class="grid-w33"></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pricing">Pricing<a href="https://www.mauromorales.com/posts/epomaker-tide75-keyboard-review/#pricing" class="hash-link" aria-label="Direct link to Pricing" title="Direct link to Pricing" translate="no">​</a></h2>
<p>The Epomaker Tide75 has a $139.99 price tag, plus taxes. Since I didn’t pay for it, I asked <a href="https://chatgpt.com/share/2658d866-17da-4d89-b982-b8f999bbc410" target="_blank" rel="noopener noreferrer" class="">ChatGPT to calculate</a> it for me, and it thinks it would be an extra 26.65 EUR for taxes, making it a total of approximately 153 EUR.</p>
<p>Deciding whether a piece of equipment is worth buying is for you to decide, but compared to these other devices, the price seems just about right to me:</p>
<ul>
<li class="">Magic keyboard is 149 EUR. It comes with the T chip, but it doesn’t have the mechanical keyboard experience, replaceable switches, and neither the extensible configuration.</li>
<li class="">DasKeyboard Pro is 179 EUR. It is a mechanical keyboard including the Numpad, but lacks RGB, replaceable switches and it’s not configurable.</li>
<li class=""><a href="https://mauromorales.com/2020/06/21/vortex-core-mechanical-keyboard/" target="_blank" rel="noopener noreferrer" class="">Vortex Core</a> is 99 EUR. It comes with the RGBs, and it is configurable, but it’s only a 40% which might be challenging to adapt for many people.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://www.mauromorales.com/posts/epomaker-tide75-keyboard-review/#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>Overall, I think this is a great product and anyone looking for a mechanical keyboard will not be disappointed. It happens to be that my daily driver, the Ergodox EZ just broke a few months back, and the Tide75 will take its place as my number 1.</p>
<p>The issues I have with the keyboard are minor, but in my opinion, could take the Epomaker from a good choice, to a great one. Having an up-to-date manual, including extra blank keycaps for customization and most important, choosing a more readable font and matching the knob color with the rest of the keyboard.</p>
<p>Thanks Epomaker for this remarkable gift, I hope that they feel proud to see their little boy is helping build <a class="" href="https://www.mauromorales.com/open-source/">open-source</a>.</p>
<p><em>The following links were given to me by Epomaker. I don’t make any commission on them. If you’d rather not use them, type Epomaker Tide75 in your preferred search engine, and you should find them, but you might get a different product than the one I reviewed here.</em></p>
<p>Interested in grabbing yours? Check out the <a href="https://epomaker.com/products/epomaker-tide75-qmk-via?_pos=1&amp;_sid=19b85c7dc&amp;_ss=r" target="_blank" rel="noopener noreferrer" class="">Epomaker Online Store</a>, or the <a href="https://amzn.to/3KAWVmT" target="_blank" rel="noopener noreferrer" class="">Epomaker Amazon Store</a>.</p>]]></content:encoded>
            <category>Hardware</category>
            <category>Mechanical Keyboards</category>
            <category>Reviews</category>
        </item>
        <item>
            <title><![CDATA[A New Dawn for Secure Linux in Untrusted Environments]]></title>
            <link>https://www.mauromorales.com/posts/a-new-down-for-secure-linux/</link>
            <guid>https://www.mauromorales.com/posts/a-new-down-for-secure-linux/</guid>
            <pubDate>Thu, 27 Jun 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Linux has become the default operating system for running web applications. However, like any system connected to the internet, it is exposed to remote attacks. While public cloud environments and private datacenters offer some security from physical tampering, edge computing presents unique challenges.</p>
<p>For this article, an edge device refers to a headless computer system (without direct human interface) deployed in remote locations like coffee shops, gas stations, or warehouses.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-security-challenge-at-the-edge">The Security Challenge at the Edge<a href="https://www.mauromorales.com/posts/a-new-down-for-secure-linux/#the-security-challenge-at-the-edge" class="hash-link" aria-label="Direct link to The Security Challenge at the Edge" title="Direct link to The Security Challenge at the Edge" translate="no">​</a></h2>
<p>Contrary to popular belief, Linux systems lack certain critical security features found in Windows (Trusted Boot) and macOS (Startup Security). While Linux supports Secure Boot and full-disk encryption, these measures alone are insufficient for edge environments where devices are physically accessible to untrusted parties.</p>
<p>The primary security goals for edge devices are:</p>
<ol>
<li class="">Preventing unauthorized access to data if the device is stolen.</li>
<li class="">Ensuring the device does not boot if tampered with.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="protecting-your-data-with-encryption">Protecting Your Data with Encryption<a href="https://www.mauromorales.com/posts/a-new-down-for-secure-linux/#protecting-your-data-with-encryption" class="hash-link" aria-label="Direct link to Protecting Your Data with Encryption" title="Direct link to Protecting Your Data with Encryption" translate="no">​</a></h2>
<p>Encrypting the disk keeps your data safe when the device is powered off, addressing the first security goal. However, this protection is compromised if the device is tampered with, leading us to the second goal.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="protecting-your-device-from-tampering">Protecting Your Device from Tampering<a href="https://www.mauromorales.com/posts/a-new-down-for-secure-linux/#protecting-your-device-from-tampering" class="hash-link" aria-label="Direct link to Protecting Your Device from Tampering" title="Direct link to Protecting Your Device from Tampering" translate="no">​</a></h2>
<p>Understanding the Linux boot process is crucial for securing a device against tampering. Upon powering on, a modern computer runs the UEFI firmware, which hands control to a bootloader. The bootloader initiates the operating system, which then decrypts your data and starts your application.</p>
<p>Secure Boot helps secure the initial stage by only allowing execution of digitally signed bootloaders. However, the problem lies in the next stage: most Linux distributions’ bootloaders do not verify the signatures of the Kernel or Initrd, nor do they measure the integrity of these components. This oversight allows potential tampering to go unnoticed.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="measuring-for-integrity">Measuring for Integrity<a href="https://www.mauromorales.com/posts/a-new-down-for-secure-linux/#measuring-for-integrity" class="hash-link" aria-label="Direct link to Measuring for Integrity" title="Direct link to Measuring for Integrity" translate="no">​</a></h2>
<p>Measuring involves calculating a hash for artifacts like the Linux Kernel. Any change in these artifacts alters the hash. Utilizing Trusted Platform Module (TPM) chips, we can establish a validation system that only proceeds with booting if the measurements match the expected values.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-unified-system-image-usi">The Unified System Image (USI)<a href="https://www.mauromorales.com/posts/a-new-down-for-secure-linux/#the-unified-system-image-usi" class="hash-link" aria-label="Direct link to The Unified System Image (USI)" title="Direct link to The Unified System Image (USI)" translate="no">​</a></h2>
<p>One effective solution is creating a Unified System Image (USI). This combines the Kernel, cmdline parameters, and Initrd into a single, immutable image. By measuring this single image, we ensure the integrity of the entire system. There’s no need to encrypt this image since it contains no sensitive data, which resides in the encrypted area. The system configuration and valuable data remain secure, and the image is mounted read-only to prevent changes.</p>
<p>For more detailed information on this process, refer to the <a href="https://uapi-group.org/specifications/" target="_blank" rel="noopener noreferrer" class="">UAPI Group’s page</a> and Lennart Poettering’s article, <a href="https://0pointer.net/blog/brave-new-trusted-boot-world.html" target="_blank" rel="noopener noreferrer" class="">Brave New Trusted Boot World</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kairos-simplifying-trusted-boot">Kairos: Simplifying Trusted Boot<a href="https://www.mauromorales.com/posts/a-new-down-for-secure-linux/#kairos-simplifying-trusted-boot" class="hash-link" aria-label="Direct link to Kairos: Simplifying Trusted Boot" title="Direct link to Kairos: Simplifying Trusted Boot" translate="no">​</a></h2>
<p>Implementing a USI with Trusted Boot can be complex. Kairos aims to simplify this process. Visit the <a href="https://kairos.io/docs/installation/trustedboot/" target="_blank" rel="noopener noreferrer" class="">Trusted Boot Installation instructions</a> to try it out, or delve into the <a href="https://kairos.io/docs/architecture/trustedboot/" target="_blank" rel="noopener noreferrer" class="">Trusted Boot Architecture documentation</a> for a deeper understanding of how Kairos enhances security in untrusted environments.</p>]]></content:encoded>
            <category>Linux</category>
            <category>Edge Computing</category>
            <category>Security</category>
            <category>Trusted Boot</category>
            <category>System Architecture</category>
        </item>
    </channel>
</rss>