<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged opensource</title>
<link>https://coderlegion.com/tag/opensource</link>
<description>Powered by Question2Answer</description>
<item>
<title>Trick or Type Guard? Surviving `unknown` with is-kit 🎃</title>
<link>https://coderlegion.com/33235/trick-or-type-guard-surviving-unknown-with-is-kit</link>
<description>&lt;p&gt;Hoi hoi! 👋&lt;/p&gt;
&lt;p&gt;I'm &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru&quot;&gt;@nyaomaru&lt;/a&gt;, a frontend engineer who is scared of horror things 🙀&lt;/p&gt;
&lt;p&gt;Happy Halloween! 🎃👻&lt;/p&gt;
&lt;p&gt;Tonight, we're exploring a haunted TypeScript project.&lt;/p&gt;
&lt;p&gt;There are zombies 🧟, ghosts 👻, mysterious pumpkins 🎃, and a terrifying vampire 🧛 waiting for us.&lt;/p&gt;
&lt;p&gt;And they all have something to do with TypeScript types!&lt;/p&gt;
&lt;p&gt;Our mission?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Survive the night without losing type safety!&lt;/strong&gt; 😸&lt;/p&gt;
&lt;p&gt;Let's go!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/6q34apw47hkutthxpbdk.gif&quot; alt=&quot;halloween is-kit&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-chapter-1-the-unknown-living-dead&quot;&gt;🧟 Chapter 1: The Unknown Living Dead&lt;/h3&gt;
&lt;p&gt;It's a dark and spooky night.&lt;/p&gt;
&lt;p&gt;Suddenly, something crawls out of the grave! 🧟&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const creature: unknown = JSON.parse('{&quot;message&quot;:&quot;BRAAAINS!&quot;}');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oh! An &lt;code&gt;unknown&lt;/code&gt; creature!&lt;/p&gt;
&lt;p&gt;Let's see what it wants.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;console.log(creature.message);
// ❌ 'creature' is of type 'unknown'.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WHAT?! 🙀&lt;/p&gt;
&lt;p&gt;TypeScript won't let us!&lt;/p&gt;
&lt;p&gt;But why?&lt;/p&gt;
&lt;p&gt;Well, because we don't actually know what &lt;code&gt;creature&lt;/code&gt; is.&lt;/p&gt;
&lt;p&gt;It might be a zombie.&lt;br&gt;
It might be a ghost.&lt;br&gt;
It might even be a cute little cat. 😸&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;unknown&lt;/code&gt; means we haven't verified the value yet.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Unlike &lt;code&gt;any&lt;/code&gt;, TypeScript won't let us access arbitrary properties on an &lt;code&gt;unknown&lt;/code&gt; value.&lt;/p&gt;
&lt;p&gt;And that's actually a good thing!&lt;/p&gt;
&lt;p&gt;Let's try something simpler.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { isString } from &quot;is-kit&quot;;

const voice: unknown = JSON.parse('&quot;BRAAAINS!&quot;');

if (isString(voice)) {
  console.log(voice.toUpperCase());
  // BRAAAINS!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;YAY! 🎉&lt;/p&gt;
&lt;p&gt;We survived our first encounter!&lt;/p&gt;
&lt;p&gt;Of course, we could also use &lt;code&gt;typeof voice === 'string'&lt;/code&gt; here.&lt;/p&gt;
&lt;p&gt;For a single check, that's perfectly fine!&lt;/p&gt;
&lt;p&gt;But what happens when our monsters become bigger and more complicated?&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-chapter-2-the-big-pumpkin-of-unknown&quot;&gt;🎃 Chapter 2: The Big Pumpkin of Unknown&lt;/h3&gt;
&lt;p&gt;Look!&lt;/p&gt;
&lt;p&gt;A GIGANTIC PUMPKIN! 🎃&lt;/p&gt;
&lt;p&gt;And apparently, it's full of candy.&lt;/p&gt;
&lt;p&gt;But nobody knows what's really inside.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const pumpkin: unknown = JSON.parse(`{
  &quot;name&quot;: &quot;Big Pumpkin&quot;,
  &quot;candies&quot;: [&quot;Chocolate&quot;, &quot;Lollipop&quot;, &quot;Cookie&quot;],
  &quot;spookyLevel&quot;: 99,
  &quot;keeper&quot;: {
    &quot;name&quot;: &quot;cat&quot;,
    &quot;friendly&quot;: true
  },
  &quot;secret&quot;: &quot;Meow!&quot;
}`);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's a pretty big object.&lt;/p&gt;
&lt;p&gt;Can we just grab the candy?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;console.log(pumpkin.candies);
// ❌ 'pumpkin' is of type 'unknown'.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not again! 😿&lt;/p&gt;
&lt;p&gt;We need to check what's inside before touching anything.&lt;/p&gt;
&lt;p&gt;Fortunately, we have &lt;code&gt;struct&lt;/code&gt;!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  arrayOf,
  isBoolean,
  isNumber,
  isString,
  optionalKey,
  struct,
} from &quot;is-kit&quot;;

const isPumpkin = struct({
  name: isString,
  candies: arrayOf(isString),
  spookyLevel: isNumber,
  keeper: struct({
    name: isString,
    friendly: isBoolean,
  }),
  secret: optionalKey(isString),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We composed several small guards into one big pumpkin guard.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;isString&lt;/code&gt; checks strings.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isNumber&lt;/code&gt; checks finite numbers.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arrayOf(isString)&lt;/code&gt; checks an array of strings.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;struct&lt;/code&gt; checks nested object shapes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;optionalKey&lt;/code&gt; allows a property to be missing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, let's open the pumpkin!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { safeParse } from &quot;is-kit&quot;;

const result = safeParse(isPumpkin, pumpkin);

if (result.valid) {
  console.log(result.value.candies);
  // ['Chocolate', 'Lollipop', 'Cookie']

  console.log(result.value.keeper.name);
  // cat 😸
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We found the candy!!! 🍬🎉&lt;/p&gt;
&lt;p&gt;When validation succeeds, &lt;code&gt;result.value&lt;/code&gt; has the type inferred from our guards.&lt;/p&gt;
&lt;p&gt;And when validation fails?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const cursedPumpkin = {
  name: &quot;Cursed Pumpkin&quot;,
  candies: &quot;100 candies&quot;,
  spookyLevel: 99,
  keeper: {
    name: &quot;cat&quot;,
    friendly: true,
  },
};

const result = safeParse(isPumpkin, cursedPumpkin);

console.log(result);
// { valid: false }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The candies were a string, not an array!&lt;/p&gt;
&lt;p&gt;Our guard rejected the cursed pumpkin.&lt;/p&gt;
&lt;p&gt;And here's the important part.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;safeParse&lt;/code&gt; doesn't magically fix or convert invalid values.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It simply tells us whether the value passed our guard.&lt;/p&gt;
&lt;p&gt;Also, &lt;code&gt;struct&lt;/code&gt; allows extra properties by default. If you need to reject additional own enumerable string keys, you can use &lt;code&gt;{ exact: true }&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Nice and simple!&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-chapter-3-trick-or-type-guard-&quot;&gt;🍬 Chapter 3: Trick or Type Guard!&lt;/h3&gt;
&lt;p&gt;Ding dong! 🔔&lt;/p&gt;
&lt;p&gt;TRICK OR TREAT!!! 😸&lt;/p&gt;
&lt;p&gt;Someone hands us a mysterious gift.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function receiveGift(): unknown {
  return Math.random() &amp;gt; 0.5 ? &quot;Chocolate&quot; : { spiders: 100 };
}

const gift: unknown = receiveGift();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wait.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Is it candy?&lt;br&gt;
Or is it a box full of spiders?! 🕷️😱&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We need a way to check!&lt;/p&gt;
&lt;p&gt;And this time, we want to accept either a string or a number.&lt;/p&gt;
&lt;p&gt;Let's combine our guards with &lt;code&gt;or&lt;/code&gt;!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { isNumber, isString, or } from &quot;is-kit&quot;;

const isTreat = or(isString, isNumber);

function trickOrTreat(gift: unknown) {
  if (isTreat(gift)) {
    // gift: string | number
    return `🍬 Treat: ${gift}`;
  }

  return &quot;👻 Trick!&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let's try it!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;trickOrTreat(&quot;Chocolate&quot;);
// 🍬 Treat: Chocolate

trickOrTreat(100);
// 🍬 Treat: 100

trickOrTreat({ spiders: 100 });
// 👻 Trick!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;YAY!!! 🍬&lt;/p&gt;
&lt;p&gt;No spiders for us today!&lt;/p&gt;
&lt;p&gt;This is one of my favorite things about &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru/is-kit&quot;&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We don't need to write one giant validator.&lt;/p&gt;
&lt;p&gt;We can build small guards, combine them, and reuse them wherever we need.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Small guards. Simple composition. Useful TypeScript narrowing.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That's the idea!&lt;/p&gt;
&lt;p&gt;But wait,&lt;br&gt;
Something is coming.&lt;br&gt;
Something TERRIFYING.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-final-boss-the-vampire-called-any&quot;&gt;🧛 Final Boss: The Vampire Called &lt;code&gt;any&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Suddenly, the lights go out.&lt;/p&gt;
&lt;p&gt;The door opens.&lt;/p&gt;
&lt;p&gt;A mysterious figure appears in the darkness.&lt;/p&gt;
&lt;p&gt;It's... &lt;strong&gt;COUNT ANY!!! 🧛💀&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The most terrifying vampire in our TypeScript project! And he has a very dangerous power.&lt;/p&gt;
&lt;p&gt;He can make TypeScript stop checking our operations!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const visitor: unknown = JSON.parse('{&quot;name&quot;:666}');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A mysterious visitor has arrived.&lt;/p&gt;
&lt;p&gt;Normally, TypeScript protects us from accessing properties before we verify them.&lt;/p&gt;
&lt;p&gt;But Count Any has a trick.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;THE CURSE OF &lt;code&gt;as any&lt;/code&gt;! 🧛&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const vampire = visitor as any;

// The vampire ignores type safety!
console.log(vampire.name.toUpperCase());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No TypeScript error!&lt;/p&gt;
&lt;p&gt;Wait, What?! 🙀&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Does that mean the code is safe?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;At runtime&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TypeError: vampire.name.toUpperCase is not a function
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💀 GAME OVER 💀&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Count Any didn't turn the number into a string.&lt;/p&gt;
&lt;p&gt;He just convinced TypeScript to stop checking it!&lt;/p&gt;
&lt;p&gt;That's what makes &lt;code&gt;any&lt;/code&gt; dangerous.&lt;/p&gt;
&lt;p&gt;It allows operations without requiring proof that they'll work.&lt;/p&gt;
&lt;p&gt;But &lt;code&gt;unknown&lt;/code&gt; behaves differently.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const visitor: unknown = JSON.parse('{&quot;name&quot;:666}');

console.log(visitor.name.toUpperCase());
// ❌ TypeScript stops us!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TypeScript refuses to continue until we check the value.&lt;/p&gt;
&lt;p&gt;So how do we defeat Count Any?&lt;/p&gt;
&lt;p&gt;With a Type Guard, of course! 😸&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { isString, struct } from &quot;is-kit&quot;;

const isFriendlyGuest = struct({
  name: isString,
});

if (isFriendlyGuest(visitor)) {
  console.log(visitor.name.toUpperCase());
} else {
  console.log(&quot;🧄 Not tonight, Count Any!&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And just like that&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;THE VAMPIRE HAS BEEN DEFEATED!&lt;/strong&gt; 🧄🎉&lt;/p&gt;
&lt;p&gt;Well, at least this particular cursed value has been rejected.&lt;/p&gt;
&lt;p&gt;A Type Guard checks the actual runtime value, while a type assertion only changes what TypeScript believes about it.&lt;/p&gt;
&lt;p&gt;One important detail, &lt;strong&gt;neither &lt;code&gt;unknown&lt;/code&gt; nor &lt;code&gt;any&lt;/code&gt; changes the value at runtime&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;They are TypeScript types.&lt;/p&gt;
&lt;p&gt;The difference is what the compiler allows us to do before verifying the value.&lt;/p&gt;
&lt;p&gt;And that's why &lt;code&gt;unknown&lt;/code&gt; is usually the safer choice for untrusted data.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-epilogue-we-survived-halloween-&quot;&gt;😸 Epilogue: We Survived Halloween!&lt;/h3&gt;
&lt;p&gt;What a night!&lt;/p&gt;
&lt;p&gt;We met the Unknown Living Dead, opened a gigantic pumpkin, collected some candy, and defeated Count Any!&lt;/p&gt;
&lt;p&gt;So, what did we learn?&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Monster                &lt;/th&gt;&lt;th&gt;TypeScript lesson&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;🧟 Unknown Living Dead &lt;/td&gt;&lt;td&gt;&lt;code&gt;unknown&lt;/code&gt; prevents unchecked operations&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;🎃 Big Pumpkin         &lt;/td&gt;&lt;td&gt;&lt;code&gt;struct&lt;/code&gt; validates object shapes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;🍬 Trick or Treat      &lt;/td&gt;&lt;td&gt;&lt;code&gt;or&lt;/code&gt; combines reusable type guards&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;🧛 Count Any           &lt;/td&gt;&lt;td&gt;&lt;code&gt;any&lt;/code&gt; bypasses normal type checking&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Of course, &lt;code&gt;any&lt;/code&gt; isn't always evil.&lt;/p&gt;
&lt;p&gt;Sometimes it's useful when dealing with legacy code or difficult integrations.&lt;/p&gt;
&lt;p&gt;But when working with API responses, parsed JSON, and other untrusted values, keeping them as &lt;code&gt;unknown&lt;/code&gt; until validation is usually a much safer approach.&lt;/p&gt;
&lt;p&gt;And &lt;code&gt;is-kit&lt;/code&gt; makes those checks easy to compose and reuse!&lt;/p&gt;
&lt;p&gt;If you want to explore more, check out the official guide 👇&lt;/p&gt;
&lt;p&gt;📖 &lt;a rel=&quot;nofollow&quot; href=&quot;https://is-kit.dev/guides/validate-unknown-without-schema-library&quot;&gt;How to Validate unknown in TypeScript Without a Schema Library&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And remember&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The real monster isn't &lt;code&gt;unknown&lt;/code&gt;. It's unchecked &lt;code&gt;any&lt;/code&gt;.&lt;/strong&gt; 🧛&lt;/p&gt;
&lt;p&gt;Happy Halloween!!! 🎃👻&lt;/p&gt;
&lt;p&gt;If you enjoyed this little TypeScript adventure, check out is-kit!&lt;/p&gt;
&lt;p&gt;{% embed &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru/is-kit&quot;&gt;https://github.com/nyaomaru/is-kit&lt;/a&gt; %}&lt;/p&gt;
&lt;p&gt;And if you like the project, a ⭐ would make this little cat very happy! 😸&lt;/p&gt;
&lt;p&gt;Thank you for reading! 🤲&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33235/trick-or-type-guard-surviving-unknown-with-is-kit</guid>
<pubDate>Sat, 31 Oct 2026 15:54:29 +0000</pubDate>
</item>
<item>
<title>Embeds: Putting a Live NitroIDE Playground Inside Your Docs</title>
<link>https://coderlegion.com/33224/embeds-putting-a-live-nitroide-playground-inside-your-docs</link>
<description>&lt;p&gt;Every tutorial has the same weak spot: the code sample. A static block shows what the code looks like. It cannot show what the code does. The reader has to copy it, open an editor, paste it, and run it — and every step in that chain is a step where a reader leaves.&lt;/p&gt;
&lt;p&gt;A month ago I wrote about NitroIDE's share engine: the Share button compresses an entire workspace into a ?code= URL parameter, and opening that link rebuilds the workspace locally in the reader's browser. Nothing is uploaded to a server. That post was about sending code to one person. This one is about the layer on top of it: embedding that workspace inside a page, so the reader never has to leave the tutorial at all.&lt;/p&gt;
&lt;h3 id=&quot;one-parameter-turns-a-share-link-into-an-embed&quot;&gt;One parameter turns a share link into an embed&lt;/h3&gt;
&lt;p&gt;Take any NitroIDE share URL and add ?embed=1 to it. That is the whole API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;iframe src=&quot;https://nitroide.com/tools/codebox.html?embed=1&amp;amp;code=YOUR_SHARE_CODE&quot; width=&quot;100%&quot; height=&quot;500&quot; frameborder=&quot;0&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The embed loads a stripped-down, read-only viewer. The full IDE chrome is gone — no file explorer, no console panel, no settings menus. What remains is the two things a reader actually needs: the editor and the live preview, side by side. The reader sees the code and its output in the same frame, on the same page, with nothing to install and no account to create.&lt;/p&gt;
&lt;p&gt;Read-only is a deliberate choice, not a missing feature. A tutorial embed is not a workspace; it is a demonstration. If the reader could edit the code in place, every edit would be a fork of the author's example with no way back — and the next reader would see someone else's experiment, not the tutorial. So the embed shows, and the escape hatch handles editing: every embed carries a footer link reading &quot;Built with NitroIDE — open &amp;amp; edit&quot;. One click opens the exact workspace in the full editor, where the reader can change anything. Show first, edit second, and never confuse the two.&lt;/p&gt;
&lt;h3 id=&quot;where-this-actually-belongs&quot;&gt;Where this actually belongs&lt;/h3&gt;
&lt;p&gt;The honest answer is: anywhere that renders an iframe and where the reader is there to learn. Blog posts, documentation pages, tutorial sites, course material — anywhere a code sample currently sits as a dead block.&lt;/p&gt;
&lt;p&gt;It does not belong in a GitHub README. GitHub strips iframes from rendered Markdown, so an embed pasted into a README renders as nothing. That is what the &quot;Open in NitroIDE&quot; badge is for — a link, not a frame — and knowing which of the two a surface accepts is the difference between an embed that works and one that silently vanishes. If the page cannot render an iframe, do not force the embed in; link out instead.&lt;/p&gt;
&lt;p&gt;The second place it belongs is less obvious: your own changelog and release notes. A new feature described in prose is a claim; the same feature embedded as a running demo is evidence. When a release note says &quot;the preview now updates without flicker,&quot; an embedded playground running the new behaviour lets the reader verify it in ten seconds instead of trusting the sentence.&lt;/p&gt;
&lt;h3 id=&quot;the-details-that-make-an-embed-feel-native-not-bolted-on&quot;&gt;The details that make an embed feel native, not bolted on&lt;/h3&gt;
&lt;p&gt;A bare iframe works. A few attributes make it feel like part of the page:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;width=&quot;100%&quot; and a fixed height around 500px. The embed is a window into a workspace, and a window needs a predictable frame. Let it fill the content column and keep the height tall enough that the preview is readable without scrolling inside a scrolling page.&lt;/li&gt;
&lt;li&gt;loading=&quot;lazy&quot; on embeds below the fold. A tutorial with five demos should not load five editors before the reader reaches the first one. Lazy loading defers each embed until it scrolls into view.&lt;/li&gt;
&lt;li&gt;A title attribute on the iframe. It costs nothing and it is what a screen reader announces; &quot;Live demo: responsive card layout&quot; is a better experience than an unnamed frame.&lt;/li&gt;
&lt;li&gt;One demo per concept. An embed is a heavyweight element — a whole editor and a live preview. Three embeds proving the same point are two embeds too many; one well-chosen demo per idea keeps the page fast and the argument sharp.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these are NitroIDE features. They are the hosting page's job, and they are the difference between an embed that feels like part of the tutorial and one that feels pasted in.&lt;/p&gt;
&lt;h3 id=&quot;the-limits-stated-plainly&quot;&gt;The limits, stated plainly&lt;/h3&gt;
&lt;p&gt;An embed inherits every limit of the share link it is built on. The workspace travels inside the URL itself, compressed into the ?code= parameter, so a very large project makes a very long URL. For a tutorial demo — a handful of files, a single concept — that is never a problem. For a full application, it is: use ZIP export for the project and embed a small, focused demo of the idea instead. The embed is for the example, not the app.&lt;/p&gt;
&lt;p&gt;The second limit comes from the preview's sandbox. NitroIDE's preview runs in a sandboxed iframe with an opaque origin, which is what keeps code in a shared link from touching the reader's saved projects. The tradeoff is that localStorage, sessionStorage, and cookies are unavailable inside the preview — and in Safari, touching them throws a SecurityError. Demo code that reads localStorage unguarded will crash inside the embed while working fine in a normal tab. Wrap storage access in try/catch in any code you plan to embed, and the same demo runs everywhere.&lt;/p&gt;
&lt;p&gt;The third limit is the read-only viewer itself. It hides the console, so a demo whose point is console output — logging, errors, debugging — loses its punchline inside an embed. That is a real constraint, and the honest response is to pick the right surface: embed the demos whose output is visual, and link out to the full editor for the ones whose output lives in the console.&lt;/p&gt;
&lt;h3 id=&quot;why-this-matters-more-than-a-nicer-code-block&quot;&gt;Why this matters more than a nicer code block&lt;/h3&gt;
&lt;p&gt;Syntax highlighting made code readable. Embeds make it runnable, and that is a different category of change. A readable sample asks the reader to trust the author. A runnable sample lets the reader check the author — change a value, watch the preview move, break it, fix it. That is the feedback loop NitroIDE is built around, except now it happens inside someone else's docs, for a reader who never opened the IDE at all.&lt;/p&gt;
&lt;p&gt;The embed is also the quietest distribution NitroIDE has. Every embed carries its own attribution in the footer, and every footer click is a reader who arrived at the full editor from inside a tutorial they were already reading — not from a post, not from an ad, from a working demo. That is the growth loop that does not look like a growth loop: make the tool useful inside other people's pages, and let the pages do the talking.&lt;/p&gt;
&lt;p&gt;One parameter, one iframe, one running demo where a dead code block used to be. Try it with the docs example above — paste a share code into the snippet, drop it into a page, and the tutorial stops describing the code and starts running it.&lt;/p&gt;
&lt;p&gt;I build NitroIDE — a browser-based IDE where your code never leaves your machine: &lt;a rel=&quot;nofollow&quot; href=&quot;https://nitroide.com?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=daily-2026-10-31&quot;&gt;https://nitroide.com?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=daily-2026-10-31&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33224/embeds-putting-a-live-nitroide-playground-inside-your-docs</guid>
<pubDate>Sat, 31 Oct 2026 12:32:01 +0000</pubDate>
</item>
<item>
<title>The Maintainer Who Wrote &quot;Sorry, I Missed the Request!&quot;</title>
<link>https://coderlegion.com/33148/the-maintainer-who-wrote-sorry-i-missed-the-request</link>
<description>&lt;p&gt;&lt;em&gt;Sixteen days of silence, one polite reminder, and a surprisingly good Friday in open source.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;My favorite GitHub notification this month wasn't a new star, a detailed code review, or someone telling me my project had changed their life.&lt;/p&gt;
&lt;p&gt;It was a maintainer saying:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Sorry, I missed the request!&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That's it.&lt;/p&gt;
&lt;p&gt;No technical discussion. No complicated review. No debate about architecture. Just five words, an exclamation mark, and a merged pull request.&lt;/p&gt;
&lt;p&gt;And somehow, that little interaction made my Friday.&lt;/p&gt;
&lt;p&gt;Let me explain.&lt;/p&gt;
&lt;h3 id=&quot;the-world-s-least-dramatic-pull-request&quot;&gt;The world's least dramatic pull request&lt;/h3&gt;
&lt;p&gt;On October 14, I opened a pull request to &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/in-toto/friends&quot;&gt;in-toto/friends&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you're unfamiliar with in-toto, it's an open-source framework focused on software supply-chain security. The &lt;em&gt;friends&lt;/em&gt; repository maintains a list of projects that integrate with it.&lt;/p&gt;
&lt;p&gt;I maintain a tool called &lt;strong&gt;workproof&lt;/strong&gt;, which generates verifiable engineering reports from private repositories.&lt;/p&gt;
&lt;p&gt;I wanted to add it to the list.&lt;/p&gt;
&lt;p&gt;The pull request was about as exciting as it sounds.&lt;/p&gt;
&lt;p&gt;One row in a Markdown table.&lt;/p&gt;
&lt;p&gt;Not a new feature. Not a performance improvement. Not a security patch that would save thousands of production servers from catching fire.&lt;/p&gt;
&lt;p&gt;A row.&lt;/p&gt;
&lt;p&gt;If GitHub had an award for the least dramatic contribution of the week, I might have been a serious contender.&lt;/p&gt;
&lt;p&gt;I opened the PR, checked that everything looked right, and moved on to other work.&lt;/p&gt;
&lt;p&gt;Then I waited.&lt;/p&gt;
&lt;p&gt;And waited.&lt;/p&gt;
&lt;p&gt;Not in the cinematic sense. There was no staring at the screen, refreshing GitHub every seven seconds while dramatic music played in the background.&lt;/p&gt;
&lt;p&gt;I had other things to build.&lt;/p&gt;
&lt;p&gt;But every so often, I'd check the pull request.&lt;/p&gt;
&lt;p&gt;Still open.&lt;/p&gt;
&lt;p&gt;No comments. No review. No labels.&lt;/p&gt;
&lt;p&gt;Nothing.&lt;/p&gt;
&lt;p&gt;Sixteen days passed.&lt;/p&gt;
&lt;h3 id=&quot;the-strange-psychology-of-an-unanswered-pr&quot;&gt;The strange psychology of an unanswered PR&lt;/h3&gt;
&lt;p&gt;Here's something I've noticed about contributing to open-source projects.&lt;/p&gt;
&lt;p&gt;When you submit a pull request, you know intellectually that nobody owes you an immediate response.&lt;/p&gt;
&lt;p&gt;Maintainers have jobs, families, other repositories, notifications, issues, bugs, releases, and probably an impressive collection of browser tabs they'll never close.&lt;/p&gt;
&lt;p&gt;I understand that.&lt;/p&gt;
&lt;p&gt;But there's still a small part of your brain that starts inventing explanations when a PR sits unanswered.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Did I submit it incorrectly?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Is this repository still maintained?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Was the contribution inappropriate?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Should I say something?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Would saying something make me annoying?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The last question is particularly interesting.&lt;/p&gt;
&lt;p&gt;As contributors, we often worry about bothering maintainers. We don't want to appear impatient or entitled, especially when the change is small.&lt;/p&gt;
&lt;p&gt;So we wait.&lt;/p&gt;
&lt;p&gt;Sometimes a week. Sometimes a month.&lt;/p&gt;
&lt;p&gt;Sometimes until the repository becomes a historical artifact and someone discovers our pull request while researching ancient civilizations.&lt;/p&gt;
&lt;p&gt;Meanwhile, the maintainer might simply have missed the notification.&lt;/p&gt;
&lt;p&gt;That's the funny part.&lt;/p&gt;
&lt;p&gt;We can spend days interpreting silence that contains absolutely no information.&lt;/p&gt;
&lt;h3 id=&quot;thirty-five-minutes&quot;&gt;Thirty-five minutes&lt;/h3&gt;
&lt;p&gt;On October 30, I decided to write a follow-up.&lt;/p&gt;
&lt;p&gt;Nothing elaborate.&lt;/p&gt;
&lt;p&gt;I mentioned that the pull request had been open since October 14, that the change still merged cleanly, and that a quick yes or no would help me plan.&lt;/p&gt;
&lt;p&gt;That was the whole point.&lt;/p&gt;
&lt;p&gt;I wasn't asking the maintainer to prioritize my contribution. I wasn't explaining why my project was important. I wasn't trying to turn a Markdown table update into a philosophical argument about the future of software supply chains.&lt;/p&gt;
&lt;p&gt;I just wanted to know whether the contribution made sense.&lt;/p&gt;
&lt;p&gt;Thirty-five minutes later, the maintainer approved it.&lt;/p&gt;
&lt;p&gt;The response:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Sorry, I missed the request!&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At 11:02 UTC, the PR was merged.&lt;/p&gt;
&lt;p&gt;Sixteen days of silence.&lt;/p&gt;
&lt;p&gt;Thirty-five minutes after a reminder, done.&lt;/p&gt;
&lt;p&gt;I laughed a little when I saw it.&lt;/p&gt;
&lt;p&gt;Not because the maintainer had done anything wrong. Quite the opposite.&lt;/p&gt;
&lt;p&gt;The answer was so ordinary and so human that it immediately made the entire situation feel smaller.&lt;/p&gt;
&lt;p&gt;There had been no hidden disagreement.&lt;/p&gt;
&lt;p&gt;No mysterious review process.&lt;/p&gt;
&lt;p&gt;No elaborate technical objection waiting to emerge.&lt;/p&gt;
&lt;p&gt;The request had simply been missed.&lt;/p&gt;
&lt;p&gt;And now it wasn't.&lt;/p&gt;
&lt;h3 id=&quot;we-talk-a-lot-about-automation-we-talk-less-about-attention-&quot;&gt;We talk a lot about automation. We talk less about attention.&lt;/h3&gt;
&lt;p&gt;I spend a lot of time around software engineering, open-source tooling, and AI-assisted development.&lt;/p&gt;
&lt;p&gt;Every week, there's a new discussion about agents writing code, agents reviewing code, agents fixing bugs, and eventually agents doing practically everything except remembering where we left our coffee.&lt;/p&gt;
&lt;p&gt;Some of those developments are genuinely exciting.&lt;/p&gt;
&lt;p&gt;But this little experience reminded me of something that doesn't get nearly as much attention.&lt;/p&gt;
&lt;p&gt;Open source isn't just a collection of repositories.&lt;/p&gt;
&lt;p&gt;It's also a collection of people trying to coordinate their limited time and attention.&lt;/p&gt;
&lt;p&gt;A pull request can be technically correct, perfectly formatted, and ready to merge.&lt;/p&gt;
&lt;p&gt;That doesn't mean someone has seen it.&lt;/p&gt;
&lt;p&gt;And even when someone has seen it, that doesn't mean they've had the time or context to respond.&lt;/p&gt;
&lt;p&gt;Those are different problems.&lt;/p&gt;
&lt;p&gt;As engineers, we're very good at building systems that track code changes. We have statuses, checks, notifications, dashboards, review requests, and activity histories.&lt;/p&gt;
&lt;p&gt;Yet a perfectly valid contribution can still disappear into the noise.&lt;/p&gt;
&lt;p&gt;The software is doing its job.&lt;/p&gt;
&lt;p&gt;The humans are just busy being humans.&lt;/p&gt;
&lt;h3 id=&quot;a-reminder-doesn-t-have-to-be-a-demand&quot;&gt;A reminder doesn't have to be a demand&lt;/h3&gt;
&lt;p&gt;I've seen contributors handle unanswered pull requests in very different ways.&lt;/p&gt;
&lt;p&gt;Some wait indefinitely.&lt;/p&gt;
&lt;p&gt;Some send repeated messages.&lt;/p&gt;
&lt;p&gt;Some become frustrated and start questioning whether the project is actively maintained.&lt;/p&gt;
&lt;p&gt;And occasionally, a simple follow-up turns into an argument that nobody really wanted to have.&lt;/p&gt;
&lt;p&gt;I'm not convinced any of those extremes help.&lt;/p&gt;
&lt;p&gt;What worked for me was making the request easy to answer.&lt;/p&gt;
&lt;p&gt;Not:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&quot;Why hasn't anyone reviewed my PR?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Not:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&quot;This is a very important contribution and deserves your attention.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;And definitely not:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&quot;Hello??? @maintainer @maintainer @maintainer&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Just a little context, a specific question, and room for the answer to be no.&lt;/p&gt;
&lt;p&gt;That last part matters.&lt;/p&gt;
&lt;p&gt;When you ask for a review, you're asking another person to spend time on something you care about.&lt;/p&gt;
&lt;p&gt;They might agree.&lt;/p&gt;
&lt;p&gt;They might disagree.&lt;/p&gt;
&lt;p&gt;They might need another week.&lt;/p&gt;
&lt;p&gt;They might decide the contribution doesn't belong in the project.&lt;/p&gt;
&lt;p&gt;All of those are legitimate outcomes.&lt;/p&gt;
&lt;p&gt;A polite reminder doesn't guarantee a merge, and it shouldn't.&lt;/p&gt;
&lt;p&gt;It just gives the conversation another chance to happen.&lt;/p&gt;
&lt;p&gt;Of course, every project has its own contribution guidelines and review expectations. Some maintainers explicitly ask contributors not to ping them. Those boundaries deserve respect.&lt;/p&gt;
&lt;p&gt;But where follow-ups are welcome, one clear message can be more useful than two weeks of silent speculation.&lt;/p&gt;
&lt;p&gt;At least, it was in this case.&lt;/p&gt;
&lt;h3 id=&quot;small-contributions-are-still-real-contributions&quot;&gt;Small contributions are still real contributions&lt;/h3&gt;
&lt;p&gt;I also want to avoid exaggerating what happened here.&lt;/p&gt;
&lt;p&gt;My PR added one row to a README.&lt;/p&gt;
&lt;p&gt;It wasn't a significant engineering breakthrough. It didn't prove the quality or security of workproof. Being listed in a repository isn't the same thing as receiving an independent endorsement.&lt;/p&gt;
&lt;p&gt;And I don't think we should pretend otherwise.&lt;/p&gt;
&lt;p&gt;There's already enough inflated language in technology.&lt;/p&gt;
&lt;p&gt;Not every commit is revolutionary.&lt;/p&gt;
&lt;p&gt;Not every feature is groundbreaking.&lt;/p&gt;
&lt;p&gt;And not every merged pull request needs a LinkedIn post explaining how it transformed the future of engineering.&lt;/p&gt;
&lt;p&gt;Although, apparently, one Markdown row can inspire an entire article.&lt;/p&gt;
&lt;p&gt;So perhaps I'm not completely innocent.&lt;/p&gt;
&lt;p&gt;But small contributions do matter in a different way.&lt;/p&gt;
&lt;p&gt;They help projects stay connected.&lt;/p&gt;
&lt;p&gt;They improve documentation, update integrations, correct outdated information, and sometimes simply make it easier for the next person to discover something useful.&lt;/p&gt;
&lt;p&gt;They also give contributors opportunities to interact with communities outside their own projects.&lt;/p&gt;
&lt;p&gt;That last part matters to me.&lt;/p&gt;
&lt;p&gt;I maintain my own tools, but I don't want all my engineering work to happen inside repositories where I'm the person making every decision.&lt;/p&gt;
&lt;p&gt;Contributing elsewhere means learning how other projects work, respecting their processes, and occasionally discovering that the person on the other side of the pull request is just as human as you are.&lt;/p&gt;
&lt;p&gt;It's a modest kind of connection.&lt;/p&gt;
&lt;p&gt;But I like it.&lt;/p&gt;
&lt;h3 id=&quot;the-other-side-of-the-notification&quot;&gt;The other side of the notification&lt;/h3&gt;
&lt;p&gt;There's another perspective worth considering.&lt;/p&gt;
&lt;p&gt;Imagine maintaining a popular open-source project.&lt;/p&gt;
&lt;p&gt;You've spent months or years building something useful, often without compensation proportional to the work involved.&lt;/p&gt;
&lt;p&gt;Your inbox is full.&lt;/p&gt;
&lt;p&gt;Someone reports a bug.&lt;/p&gt;
&lt;p&gt;Someone requests a feature.&lt;/p&gt;
&lt;p&gt;Someone opens a PR that breaks three tests.&lt;/p&gt;
&lt;p&gt;Someone asks whether the project supports a configuration you documented two paragraphs above their question.&lt;/p&gt;
&lt;p&gt;And somewhere among all of that is a perfectly reasonable one-line contribution.&lt;/p&gt;
&lt;p&gt;You miss it.&lt;/p&gt;
&lt;p&gt;Sixteen days later, the contributor sends a polite reminder.&lt;/p&gt;
&lt;p&gt;You take a look, realize it's fine, and merge it.&lt;/p&gt;
&lt;p&gt;What do you say?&lt;/p&gt;
&lt;p&gt;In this case:&lt;/p&gt;
&lt;p&gt;&quot;Sorry, I missed the request!&quot;&lt;/p&gt;
&lt;p&gt;I appreciated that response because it didn't try to turn a small oversight into something complicated.&lt;/p&gt;
&lt;p&gt;No excuses.&lt;/p&gt;
&lt;p&gt;No defensiveness.&lt;/p&gt;
&lt;p&gt;Just an acknowledgment and an action.&lt;/p&gt;
&lt;p&gt;That's a pretty good way to handle many situations, not only pull requests.&lt;/p&gt;
&lt;p&gt;And if you're a maintainer reading this, I don't think the lesson is that you need to respond to everything immediately.&lt;/p&gt;
&lt;p&gt;That would be a terrible expectation.&lt;/p&gt;
&lt;p&gt;It's that even a short answer can be valuable.&lt;/p&gt;
&lt;p&gt;&quot;Not right now.&quot;&lt;/p&gt;
&lt;p&gt;&quot;Needs changes.&quot;&lt;/p&gt;
&lt;p&gt;&quot;This doesn't fit the project.&quot;&lt;/p&gt;
&lt;p&gt;&quot;Sorry, I missed this.&quot;&lt;/p&gt;
&lt;p&gt;A response doesn't have to be positive to be useful.&lt;/p&gt;
&lt;p&gt;Sometimes it just needs to exist.&lt;/p&gt;
&lt;h3 id=&quot;one-more-thing-before-i-close-github&quot;&gt;One more thing before I close GitHub&lt;/h3&gt;
&lt;p&gt;I have a couple of other pull requests waiting in different projects.&lt;/p&gt;
&lt;p&gt;After this experience, I'm going to check their statuses and see whether a follow-up makes sense.&lt;/p&gt;
&lt;p&gt;Not because I suddenly believe every unanswered PR is one message away from being merged.&lt;/p&gt;
&lt;p&gt;Some changes need discussion. Some need substantial review. Some may never be accepted.&lt;/p&gt;
&lt;p&gt;That's part of contributing to projects you don't control.&lt;/p&gt;
&lt;p&gt;But I'm less inclined to interpret silence as a decision.&lt;/p&gt;
&lt;p&gt;And I'm a little more comfortable asking a reasonable question when enough time has passed.&lt;/p&gt;
&lt;p&gt;If you've contributed to open source, you've probably experienced that strange waiting period where you can't tell whether your PR is being considered, forgotten, or quietly ignored.&lt;/p&gt;
&lt;p&gt;My experience isn't proof that reminders always work.&lt;/p&gt;
&lt;p&gt;It's just one example of why they sometimes do.&lt;/p&gt;
&lt;p&gt;Sixteen days of silence turned out to mean exactly what the maintainer said:&lt;/p&gt;
&lt;p&gt;They missed the request.&lt;/p&gt;
&lt;p&gt;Nothing more dramatic than that.&lt;/p&gt;
&lt;p&gt;So thank you to the maintainer who took the time to respond and merge the change.&lt;/p&gt;
&lt;p&gt;You probably spent less than a minute writing that sentence.&lt;/p&gt;
&lt;p&gt;I ended up writing an entire article about it.&lt;/p&gt;
&lt;p&gt;That's a pretty impressive return on five words.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I'm Efe Genc, an independent software engineer building open-source developer tools. I write about engineering, experiments, and the things that happen when software meets actual human beings.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;First published on my site: &lt;a rel=&quot;nofollow&quot; href=&quot;https://efegenc.me/writing/the-maintainer-who-wrote-sorry-i-missed-the-request/&quot;&gt;https://efegenc.me/writing/the-maintainer-who-wrote-sorry-i-missed-the-request/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33148/the-maintainer-who-wrote-sorry-i-missed-the-request</guid>
<pubDate>Fri, 30 Oct 2026 20:21:47 +0000</pubDate>
</item>
<item>
<title>Building Knemora: An Open-source, Local-first AI (SI) Second Brain</title>
<link>https://coderlegion.com/33114/building-knemora-an-open-source-local-first-ai-si-second-brain</link>
<description>&lt;p&gt;I’m Federico, a visual designer and full-stack developer.&lt;/p&gt;
&lt;p&gt;About six months ago, I had an idea I kept coming back to: could I turn documents and datasets into connected knowledge, explore the relationships visually, and ask questions without sending my material to a cloud inference service?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I built Knemora?&lt;/strong&gt;&lt;br&gt;
After a lot of research, architecture decisions, late nights, and interface iterations, that idea became Knemora.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works?&lt;/strong&gt;&lt;br&gt;
It’s an open-source, local-first AI (SI) Second Brain. You bring your documents and datasets; Knemora compiles knowledge into a portable Markdown wiki, connects pages, and makes that knowledge available through search, a native graph, and chat with source citations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Architecture:&lt;/strong&gt;&lt;br&gt;
The diagrams below show the system architecture and the knowledge/inference workflow.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/luuyx8m950opmqn57egz.png&quot; alt=&quot;System architecture&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/pa4tcxcrntaoaacx19ai.png&quot; alt=&quot;Core AI architecture&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A few choices behind the architecture:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Portable knowledge: Markdown pages with frontmatter and explicit links. The graph is a rebuildable view of those connections.&lt;/li&gt;
&lt;li&gt;Local inference: llama.cpp with Qwen3-8B Q3_K_M as the default model.&lt;/li&gt;
&lt;li&gt;Retrieval: BM25 search and bounded graph navigation, without a vector database.&lt;/li&gt;
&lt;li&gt;Traceability: original documents are preserved, and generated claims carry source references.&lt;/li&gt;
&lt;li&gt;Dataset analysis: Polars performs structured calculations, keeping numerical results grounded in computation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The stack includes Vue 3, FastAPI, Google ADK, SQLite, and Sigma.js/Graphology.&lt;/p&gt;
&lt;p&gt;The initial alpha targets &lt;strong&gt;macOS Apple Silicon&lt;/strong&gt;. Node.js 24 with npm is required; the launcher manages the remaining runtimes and model downloads. Initial setup needs internet access, while the core knowledge and inference workflow runs locally afterward. There’s also an isolated demo with interconnected sample knowledge.&lt;/p&gt;
&lt;p&gt;As a designer, I care about making the interface clear and the graph useful. As a developer, I care about keeping the underlying knowledge portable and understandable.&lt;/p&gt;
&lt;p&gt;Open-source software has helped me throughout my career, and I’m proud to contribute something of my own. This is an early alpha, with plenty left to improve.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code, documentation, and setup:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/federicolaterra/knemora&quot;&gt;github.com/federicolaterra/knemora&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://knemora.space&quot;&gt;knemora.space&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33114/building-knemora-an-open-source-local-first-ai-si-second-brain</guid>
<pubDate>Fri, 30 Oct 2026 15:23:16 +0000</pubDate>
</item>
<item>
<title>[OC] I built a free, open-source HIPAA DICOM anonymizer for medical imaging</title>
<link>https://coderlegion.com/33082/oc-i-built-a-free-open-source-hipaa-dicom-anonymizer-for-medical-imaging</link>
<description>&lt;p&gt;I just released &lt;strong&gt;dicom-clean&lt;/strong&gt; — an open-source CLI tool for HIPAA-compliant DICOM de-identification. Here's how it works and why I built it.&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Medical imaging files (CT scans, MRIs, X-rays) use a format called &lt;strong&gt;DICOM&lt;/strong&gt;, which contains two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Metadata&lt;/strong&gt; — patient name, birth date, MRN, hospital, referring physician, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pixel data&lt;/strong&gt; — the actual image&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When hospitals want to share DICOM data for research, AI training, or vendor evaluation, they must remove the patient identifiers first — this is required by HIPAA Safe Harbor (45 CFR §164.514(b)(2)).&lt;/p&gt;
&lt;p&gt;But existing options are problematic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Commercial tools&lt;/strong&gt; (RSNA CTP, DicomCleaner) cost $5,000+/year&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud solutions&lt;/strong&gt; upload patient data → HIPAA violation risk&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DCMTK / GDCM&lt;/strong&gt; work but have steep learning curves (C++ toolchain, complex syntax)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I built &lt;strong&gt;dicom-clean&lt;/strong&gt; to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;100% offline&lt;/strong&gt; (air-gapped compatible)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Simple CLI&lt;/strong&gt; (one command)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Zero-install&lt;/strong&gt; (standalone &lt;code&gt;.exe&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Free and open source&lt;/strong&gt; (AGPL-3.0)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-it-does&quot;&gt;What It Does&lt;/h3&gt;
&lt;h4 id=&quot;1-strips-all-18-hipaa-patient-identifiers&quot;&gt;1. Strips All 18 HIPAA Patient Identifiers&lt;/h4&gt;
&lt;p&gt;The tool blanks out 40+ scalar PHI tags and deletes 8 sequences that may carry nested PHI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;HIPAA_TAGS_TO_BLANK = (
    (0x0010, 0x0010),  # PatientName
    (0x0010, 0x0020),  # PatientID
    (0x0010, 0x0030),  # PatientBirthDate
    (0x0010, 0x1040),  # PatientAddress
    (0x0008, 0x0080),  # InstitutionName
    (0x0008, 0x0090),  # ReferringPhysicianName
    # ... 40+ more
)

HIPAA_TAGS_TO_REMOVE = (
    (0x0008, 0x1110),  # ReferencedStudySequence
    (0x0008, 0x1115),  # ReferencedSeriesSequence
    # ... 6 more
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Design decision:&lt;/strong&gt; I &lt;strong&gt;blank&lt;/strong&gt; the scalar tags (preserving tag + VR) rather than deleting them, so downstream PACS and DICOM viewers don't break when they expect those fields.&lt;/p&gt;
&lt;h4 id=&quot;2-deterministic-uid-pseudonymization&quot;&gt;2. Deterministic UID Pseudonymization&lt;/h4&gt;
&lt;p&gt;DICOM uses &lt;strong&gt;UIDs&lt;/strong&gt; (Unique Identifiers) to link files together. A study has a &lt;code&gt;StudyInstanceUID&lt;/code&gt;, each series has a &lt;code&gt;SeriesInstanceUID&lt;/code&gt;, and each image has a &lt;code&gt;SOPInstanceUID&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you just replace these with random UUIDs, you lose the relationships between files — a PACS server won't be able to group images from the same study.&lt;/p&gt;
&lt;p&gt;My solution: &lt;strong&gt;SHA-256 with an optional salt&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def pseudonymize_uid(original_uid: str, salt: str = &quot;&quot;) -&amp;gt; str:
    &quot;&quot;&quot;Deterministically pseudonymize a DICOM UID.&quot;&quot;&quot;
    if not original_uid:
        return original_uid
    digest = hashlib.sha256(
        (salt + str(original_uid)).encode(&quot;utf-8&quot;)
    ).hexdigest()
    number = int(digest[:32], 16)  # 128 bits
    return f&quot;2.25.{number}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deterministic&lt;/strong&gt; — Same input + salt → same output, always&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reversible relationships&lt;/strong&gt; — All files from one study get consistent pseudonyms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-reversible&lt;/strong&gt; — Without the salt, you can't reverse it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valid format&lt;/strong&gt; — Output is a proper DICOM UID (&lt;code&gt;2.25.&amp;lt;128-bit-int&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-image-integrity&quot;&gt;3. Image Integrity&lt;/h4&gt;
&lt;p&gt;The pixel data is &lt;strong&gt;never touched&lt;/strong&gt;. Same for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Modality&lt;/code&gt; (CT, MR, US, etc.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PixelData&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TransferSyntaxUID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PhotometricInterpretation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Rows&lt;/code&gt; / &lt;code&gt;Columns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BitsAllocated&lt;/code&gt; / &lt;code&gt;BitsStored&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PixelSpacing&lt;/code&gt; / &lt;code&gt;SliceThickness&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Window/Level tags&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the image looks identical in any DICOM viewer — only the metadata changes.&lt;/p&gt;
&lt;h4 id=&quot;4-date-reduction&quot;&gt;4. Date Reduction&lt;/h4&gt;
&lt;p&gt;HIPAA permits keeping the year but not the month/day. So:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;Before: StudyDate = &quot;20250315&quot;
After:  StudyDate = &quot;20250101&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Component &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;pydicom&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;DICOM parsing &amp;amp; writing&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Typer&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;CLI framework&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Rich&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Terminal UI (panels, progress bars, tables)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PyInstaller&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Standalone &lt;code&gt;.exe&lt;/code&gt; build&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;what-it-looks-like&quot;&gt;What It Looks Like&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ dicom-clean sanitize patient.dcm output\ --salt &quot;my-secret&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output shows a live progress bar and a colored summary table:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Total Files Scanned: 1,250&lt;/li&gt;
&lt;li&gt;Successfully Anonymized: 1,248&lt;/li&gt;
&lt;li&gt;Skipped (Non-DICOM): 2&lt;/li&gt;
&lt;li&gt;Errors / Failed: 0&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;directory-batch-processing&quot;&gt;Directory Batch Processing&lt;/h3&gt;
&lt;p&gt;The tool recursively processes directories and preserves the original subfolder structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ dicom-clean sanitize C:\Hospital_Data C:\Clean_Output --salt &quot;hospital-2025&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;robustness&quot;&gt;Robustness&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Non-DICOM files are &lt;strong&gt;skipped with a warning&lt;/strong&gt; instead of crashing&lt;/li&gt;
&lt;li&gt;Per-file errors are &lt;strong&gt;collected into a summary&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;try/except&lt;/code&gt; for &lt;code&gt;InvalidDicomError&lt;/code&gt;, &lt;code&gt;OSError&lt;/code&gt;, &lt;code&gt;IOError&lt;/code&gt;, and generic exceptions&lt;/li&gt;
&lt;li&gt;Exit code &lt;code&gt;1&lt;/code&gt; if any file failed, &lt;code&gt;0&lt;/code&gt; otherwise&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;h4 id=&quot;windows-standalone-no-python-required-&quot;&gt;Windows Standalone (No Python Required)&lt;/h4&gt;
&lt;p&gt;Download &lt;code&gt;dicom-clean.exe&lt;/code&gt; from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kaveen-ai/dicom-clean/releases&quot;&gt;Releases page&lt;/a&gt; and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;dicom-clean.exe sanitize input.dcm output\ --salt &quot;my-secret&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;from-source&quot;&gt;From Source&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/kaveen-ai/dicom-clean.git
cd dicom-clean
pip install -r requirements.txt
python anonymizer.py --help
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;design-tradeoffs&quot;&gt;Design Tradeoffs&lt;/h3&gt;
&lt;p&gt;Some decisions I made and why:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. SHA-256 hashing vs random UUIDs for UIDs&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Hash preserves relationships across files&lt;/li&gt;
&lt;li&gt;✅ Deterministic (reproducible)&lt;/li&gt;
&lt;li&gt;❌ Requires salt management for security&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Blank tags vs delete tags&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Blanking preserves tag structure (PACS-safe)&lt;/li&gt;
&lt;li&gt;✅ Downstream tools don't break&lt;/li&gt;
&lt;li&gt;❌ Leaves an empty tag with metadata footprint&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. AGPL-3.0 + Commercial dual-license&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Free for open-source use&lt;/li&gt;
&lt;li&gt;✅ Commercial license funds development&lt;/li&gt;
&lt;li&gt;❌ AGPL can be a dealbreaker for some hospitals&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. Single-file CLI vs library-first&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Standalone &lt;code&gt;.exe&lt;/code&gt; for non-technical users&lt;/li&gt;
&lt;li&gt;✅ The core &lt;code&gt;DicomAnonymizer&lt;/code&gt; class is importable as a library&lt;/li&gt;
&lt;li&gt;✅ CLI is just a thin Typer wrapper&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-dicomanonymizer-class&quot;&gt;The &lt;code&gt;DicomAnonymizer&lt;/code&gt; Class&lt;/h3&gt;
&lt;p&gt;If you want to use the engine programmatically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from anonymizer import DicomAnonymizer

anonymizer = DicomAnonymizer(
    salt=&quot;my-hospital-secret&quot;,
    remove_private_tags=True,
    reduce_dates_to_year=True,
)

summary = anonymizer.anonymize(
    input_path=&quot;patient.dcm&quot;,
    output_path=&quot;output/&quot;,
)

print(f&quot;Anonymized {summary.anonymized} files&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The class handles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UID pseudonymization&lt;/li&gt;
&lt;li&gt;HIPAA tag blanking&lt;/li&gt;
&lt;li&gt;Private tag removal&lt;/li&gt;
&lt;li&gt;Date reduction&lt;/li&gt;
&lt;li&gt;File meta sanitization&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's Next&lt;/h3&gt;
&lt;p&gt;I'm planning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub Actions CI&lt;/strong&gt; — test matrix across Python 3.10-3.13&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PyPI publication&lt;/strong&gt; — &lt;code&gt;pip install dicom-clean&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker image&lt;/strong&gt; — for hospital IT deployments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pixel OCR redaction&lt;/strong&gt; — for burned-in PHI in images (commercial edition)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;feedback-welcome&quot;&gt;Feedback Welcome&lt;/h3&gt;
&lt;p&gt;I'd love feedback from anyone working with DICOM data, healthcare IT, or medical imaging.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Star on GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kaveen-ai/dicom-clean&quot;&gt;github.com/kaveen-ai/dicom-clean&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Report bugs:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kaveen-ai/dicom-clean/issues&quot;&gt;Issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussions:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kaveen-ai/dicom-clean/discussions&quot;&gt;GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What would you like to see next?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;python&lt;/code&gt; &lt;code&gt;healthcare&lt;/code&gt; &lt;code&gt;opensource&lt;/code&gt; &lt;code&gt;dicom&lt;/code&gt; &lt;code&gt;privacy&lt;/code&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33082/oc-i-built-a-free-open-source-hipaa-dicom-anonymizer-for-medical-imaging</guid>
<pubDate>Fri, 30 Oct 2026 10:49:57 +0000</pubDate>
</item>
<item>
<title>A JSON-RPC Library With No Method Registration: How Does It Decide What Can Be Called?</title>
<link>https://coderlegion.com/32964/a-json-rpc-library-with-no-method-registration-how-does-it-decide-what-can-be-called</link>
<description>&lt;p&gt;JSON-RPC 2.0 is a small specification: the client sends &lt;code&gt;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;...&quot;,&quot;params&quot;:{...},&quot;id&quot;:1}&lt;/code&gt;, and the server answers with a &lt;code&gt;result&lt;/code&gt; or an &lt;code&gt;error&lt;/code&gt;. REST makes you decide on routes, HTTP verbs and status codes; JSON-RPC only cares about which method to call and with what parameters. For an API where you own both the front end and the back end, that is pleasant to work with.&lt;/p&gt;
&lt;p&gt;Polhem.JsonRpc is a set of .NET packages for JSON-RPC 2.0: a transport-independent server, an ASP.NET Core endpoint and a client, built on System.Text.Json alone. It started as the API layer of the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/polhem-dev/polhem&quot;&gt;Polhem framework&lt;/a&gt;. I have now extracted it into standalone packages that do not depend on the framework, so other projects can reference them directly. The source is on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/polhem-dev/polhem-jsonrpc&quot;&gt;GitHub&lt;/a&gt; under the MIT license, and the packages are on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages?q=Polhem.JsonRpc&quot;&gt;NuGet&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-what-it-covers&quot;&gt;1️⃣ What it covers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Full JSON-RPC 2.0: ordinary calls, notifications (calls that expect no response), and batches (several calls in one message). An id can be a string, an integer or &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A transport-independent server: the dispatcher (the part that finds and runs the method) knows nothing about HTTP. The ASP.NET Core endpoint is a separate package.&lt;/li&gt;
&lt;li&gt;A client: an HTTP transport, batches, notifications, and interceptors that can rewrite the parameters and result of each call.&lt;/li&gt;
&lt;li&gt;Optional payload encryption: the parameters and result go into an envelope that can be compressed and encrypted, and replayed calls can be rejected.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is split into several NuGet packages, so an application references only what it needs:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Package &lt;/th&gt;&lt;th&gt;Use&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc&quot;&gt;Polhem.JsonRpc&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Message types, error codes and the transport abstraction shared by the server and the client&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.Server&quot;&gt;Polhem.JsonRpc.Server&lt;/a&gt; &lt;/td&gt;&lt;td&gt;The dispatcher: method resolution, parameter binding and filters&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.AspNetCore&quot;&gt;Polhem.JsonRpc.AspNetCore&lt;/a&gt; &lt;/td&gt;&lt;td&gt;The ASP.NET Core endpoint, &lt;code&gt;MapJsonRpc&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.Client&quot;&gt;Polhem.JsonRpc.Client&lt;/a&gt; &lt;/td&gt;&lt;td&gt;The client, &lt;code&gt;JsonRpcConnector&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.Payload&quot;&gt;Polhem.JsonRpc.Payload&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Optional: the payload envelope, compression, encryption and replay protection&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.Payload.Client&quot;&gt;Polhem.JsonRpc.Payload.Client&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Optional: a client that seals the parameters and opens the result of every call&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/packages/Polhem.JsonRpc.Payload.Server&quot;&gt;Polhem.JsonRpc.Payload.Server&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Optional: opens the envelope and checks for replays on the server&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The server references &lt;code&gt;Polhem.JsonRpc.AspNetCore&lt;/code&gt;, and the client references only &lt;code&gt;Polhem.JsonRpc.Client&lt;/code&gt;. The split keeps a client, whether a desktop app or a mobile app, from pulling in ASP.NET Core and the reflection-based dispatcher.&lt;/p&gt;
&lt;p&gt;Native AOT compiles an app to native code at publish time, so nothing can rely on reflection at run time; iOS and WebAssembly both need it. The shared package, the client and the payload packages are marked AOT-compatible, and CI publishes a test program with Native AOT and runs it. The server package finds methods by reflection, so it does not support AOT. The target framework is &lt;code&gt;net10.0&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-getting-started&quot;&gt;2️⃣ Getting started&lt;/h3&gt;
&lt;h4 id=&quot;asp-net-core-server-minimal-api-&quot;&gt;ASP.NET Core server (Minimal API)&lt;/h4&gt;
&lt;p&gt;The server uses ASP.NET Core Minimal APIs (endpoints mapped directly in &lt;code&gt;Program.cs&lt;/code&gt; with &lt;code&gt;MapXxx&lt;/code&gt;, without MVC controllers). &lt;code&gt;MapJsonRpc&lt;/code&gt; creates a POST endpoint at the given path, and every JSON-RPC call comes in through it. &lt;code&gt;AddControllers&lt;/code&gt; is not needed.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Program.cs&lt;/code&gt; needs three things: register the object factory, register the JSON-RPC services, and map the endpoint.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using Polhem.JsonRpc.AspNetCore;
using Polhem.JsonRpc.Server;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton&amp;lt;IJsonRpcObjectFactory, AppObjectFactory&amp;gt;();
builder.Services.AddJsonRpcServer();

var app = builder.Build();
app.MapJsonRpc(&quot;/api&quot;);
await app.RunAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AddJsonRpcServer&lt;/code&gt; takes two sets of options. The first configures the server itself, such as filters, the method policy, the batch limit and serializer options. The second configures the HTTP endpoint, such as the request body size limit and which HTTP status code an error returns. Leave them out and the defaults apply.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;builder.Services.AddJsonRpcServer(
    options =&amp;gt;
    {
        options.MaxBatchSize = 50;
        options.Filters.Add(new ApiKeyFilter(apiKey)); // see section 5
    },
    http =&amp;gt; http.MaxRequestBodySize = 1024 * 1024);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MapJsonRpc&lt;/code&gt; returns the same thing as any other Minimal API endpoint, so authorization, CORS and rate limiting chain on the usual way, for example &lt;code&gt;app.MapJsonRpc(&quot;/api&quot;).RequireAuthorization();&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The object factory and the class that handles the calls:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class AppObjectFactory : IJsonRpcObjectFactory
{
    public object? CreateObject(string progId, JsonRpcRequestContext context) =&amp;gt; progId switch
    {
        &quot;Calculator&quot; =&amp;gt; new Calculator(),
        _ =&amp;gt; null,
    };
}

public sealed class Calculator
{
    public AddResponse Add(AddRequest request) =&amp;gt; new() { Sum = request.A + request.B };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;client&quot;&gt;Client&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using var http = new HttpClient { BaseAddress = new Uri(&quot;http://localhost:5080/api&quot;) };
var rpc = new JsonRpcConnector(new HttpTransport(http));

var added = await rpc.InvokeAsync&amp;lt;AddResponse&amp;gt;(&quot;Calculator.Add&quot;, new AddRequest { A = 1, B = 2 });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is enough to call &lt;code&gt;Calculator.Add&lt;/code&gt;. Note that the server has no &lt;code&gt;AddMethod&lt;/code&gt; or similar registration, and &lt;code&gt;Calculator&lt;/code&gt; carries no attribute.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-with-no-registration-which-methods-can-be-called-&quot;&gt;3️⃣ With no registration, which methods can be called?&lt;/h3&gt;
&lt;p&gt;Skipping registration is convenient, but it raises a worry: if one day I add a public method to the class for internal use, can it be called from the network too? When the server receives &lt;code&gt;Calculator.Add&lt;/code&gt;, it runs the following checks in order.&lt;/p&gt;
&lt;p&gt;Name format. The method name must be &lt;code&gt;ProgId.Action&lt;/code&gt;. A ProgId (program identifier) may contain letters, digits, underscores and hyphens; an action letters, digits and underscores; each at most 64 characters. A malformed name is rejected before any lookup.&lt;/p&gt;
&lt;p&gt;The application decides the object. The ProgId is passed to your implementation of &lt;code&gt;IJsonRpcObjectFactory.CreateObject&lt;/code&gt;, which decides which object to create; returning &lt;code&gt;null&lt;/code&gt; means the ProgId is unknown. The packages keep no registry of names, so which ProgIds exist is entirely up to the factory. After the call, the object is passed to &lt;code&gt;ReleaseObjectAsync&lt;/code&gt; to be released.&lt;/p&gt;
&lt;p&gt;Method signature. An action maps only to a public, non-generic instance method with exactly one parameter. Static methods, property accessors and methods declared by &lt;code&gt;object&lt;/code&gt; do not count. If two or more methods with the same name qualify, the server does not pick one; it treats the method as not found.&lt;/p&gt;
&lt;p&gt;Naming convention. The parameter type must be named &lt;code&gt;{Action}Request&lt;/code&gt;, and the return type (or the result type of &lt;code&gt;Task&amp;lt;T&amp;gt;&lt;/code&gt; or &lt;code&gt;ValueTask&amp;lt;T&amp;gt;&lt;/code&gt;) must be named &lt;code&gt;{Action}Response&lt;/code&gt;. &lt;code&gt;Add&lt;/code&gt; takes an &lt;code&gt;AddRequest&lt;/code&gt; and returns an &lt;code&gt;AddResponse&lt;/code&gt;. This is the default method policy; any public method that does not follow it is treated as nonexistent and answered with &lt;code&gt;-32601 Method not found&lt;/code&gt;. An internal helper method that does not follow this naming cannot be called from outside.&lt;/p&gt;
&lt;p&gt;The rule can be replaced through &lt;code&gt;JsonRpcServerOptions.MethodPolicy&lt;/code&gt;. The Polhem framework replaces it with one that reads its own access-control attribute.&lt;/p&gt;
&lt;p&gt;Policy before filters. The method policy runs before every filter, so filters such as authorization checks or decryption only ever see calls that are allowed to run.&lt;/p&gt;
&lt;p&gt;Only after these checks are the &lt;code&gt;params&lt;/code&gt; bound. &lt;code&gt;params&lt;/code&gt; must be a JSON object, deserialized into the request class with camelCase names by default; missing &lt;code&gt;params&lt;/code&gt; or an array (positional parameters) are answered with &lt;code&gt;-32602 Invalid params&lt;/code&gt;. To return a custom error, a method throws &lt;code&gt;JsonRpcErrorException&lt;/code&gt;; any other exception is answered with &lt;code&gt;-32603 Internal error&lt;/code&gt;, and the exception message is not sent to the client.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-the-client&quot;&gt;4️⃣ The client&lt;/h3&gt;
&lt;p&gt;An error returned by the server becomes a &lt;code&gt;JsonRpcErrorException&lt;/code&gt;, which carries the error code and message:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;try
{
    await rpc.InvokeAsync&amp;lt;DivideResponse&amp;gt;(&quot;Calculator.Divide&quot;, new DivideRequest { Dividend = 1, Divisor = 0 });
}
catch (JsonRpcErrorException ex)
{
    Console.WriteLine($&quot;Error {ex.Code}: {ex.Message}&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notifications and batches:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// The server runs it but does not respond.
await rpc.NotifyAsync(&quot;Calculator.Log&quot;, new LogRequest { Message = &quot;Hello&quot; });

// Two calls sent in the same message.
var batch = rpc.CreateBatch();
var first = batch.Add&amp;lt;AddResponse&amp;gt;(&quot;Calculator.Add&quot;, new AddRequest { A = 2, B = 3 });
var second = batch.Add&amp;lt;AddResponse&amp;gt;(&quot;Calculator.Add&quot;, new AddRequest { A = 4, B = 5 });
await batch.SendAsync();
Console.WriteLine($&quot;{(await first)!.Sum}, {(await second)!.Sum}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The request and response classes usually live in a project shared by both sides, which is how the samples in the repo are arranged.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-extension-points-filters-and-interceptors&quot;&gt;5️⃣ Extension points: filters and interceptors&lt;/h3&gt;
&lt;p&gt;A server filter wraps every call. It can reject the call, rewrite the parameters before the method runs, and rewrite the result afterwards. For example, checking an API key:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class ApiKeyFilter(string expectedKey) : IJsonRpcFilter
{
    public ValueTask InvokeAsync(JsonRpcRequestContext context, JsonRpcFilterDelegate next)
    {
        context.Transport.Headers.TryGetValue(&quot;X-Api-Key&quot;, out var key);
        if (!CryptographicOperations.FixedTimeEquals(
                Encoding.UTF8.GetBytes(key ?? &quot;&quot;), Encoding.UTF8.GetBytes(expectedKey)))
        {
            throw new JsonRpcErrorException(-32001, &quot;Unauthorized&quot;);
        }
        return next(context);
    }
}

builder.Services.AddJsonRpcServer(options =&amp;gt; options.Filters.Add(new ApiKeyFilter(apiKey)));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To add an HTTP header on the client, use &lt;code&gt;HttpClient&lt;/code&gt;'s own &lt;code&gt;DelegatingHandler&lt;/code&gt;; the packages add no layer of their own. To rewrite the parameters and result of every call, add an &lt;code&gt;IJsonRpcClientInterceptor&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The Polhem framework plugs its sessions and authorization checks into these extension points (the object factory, the method policy and filters). None of that logic is in the packages themselves.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-encrypted-payloads&quot;&gt;6️⃣ Encrypted payloads&lt;/h3&gt;
&lt;p&gt;HTTPS protects the connection. If the parameters and results also need end-to-end protection, or a call must not run a second time when it is resent, add the optional payload packages.&lt;/p&gt;
&lt;p&gt;They put &lt;code&gt;params&lt;/code&gt; and &lt;code&gt;result&lt;/code&gt; into an envelope with three formats: plain JSON, encoded (serialized, then gzip-compressed), and encrypted (AES-256-CBC with HMAC-SHA256, encrypt-then-MAC). A frame carrying a timestamp and a sequence number can also be put in front of the content, so the server can reject expired or repeated requests; that is the replay protection.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Server: the application's policy decides the keys and which methods must be encrypted or replay-protected.
builder.Services.AddJsonRpcServer(options =&amp;gt; options.UsePayload(payloadOptions, new MyPayloadPolicy()));

// Client: called the same way as an ordinary call; each call seals its parameters and opens its result.
var rpc = new PayloadConnector(connector, new PayloadProcessor(payloadOptions),
    new PayloadConnectorOptions { KeyProvider = () =&amp;gt; sessionKey });
var added = await rpc.InvokeAsync&amp;lt;AddResponse&amp;gt;(&quot;Calculator.Add&quot;, request);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few more security details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The HMAC is compared in constant time and verified before anything is decrypted.&lt;/li&gt;
&lt;li&gt;The HMAC covers the method name and the direction, so an intercepted call cannot be redirected to another method, and a response cannot be sent back as a request.&lt;/li&gt;
&lt;li&gt;A method can require a minimum format; a request in a lower format is rejected before any key is asked for.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How keys are negotiated (exchanged after sign-in, provisioned in advance, and so on) is outside the packages and left to the application.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-how-it-differs-from-common-libraries&quot;&gt;7️⃣ How it differs from common libraries&lt;/h3&gt;
&lt;p&gt;There are quite a few JSON-RPC libraries for .NET; here are two common ones for comparison. The version in each column header is the latest stable release on NuGet as of October 2026, and the details come from that release's documentation and NuGet dependency list. Later releases may differ.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;  &lt;/th&gt;&lt;th&gt;StreamJsonRpc 2.25.29 &lt;/th&gt;&lt;th&gt;Tochka.JsonRpc 7.4.1 &lt;/th&gt;&lt;th&gt;Polhem.JsonRpc 1.2.0&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;JSON library &lt;/td&gt;&lt;td&gt;Newtonsoft.Json by default, switchable to System.Text.Json; the package still depends on Newtonsoft.Json and MessagePack &lt;/td&gt;&lt;td&gt;System.Text.Json &lt;/td&gt;&lt;td&gt;System.Text.Json, with no other dependencies&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Transport model &lt;/td&gt;&lt;td&gt;Full duplex over a Stream, WebSocket or Pipe &lt;/td&gt;&lt;td&gt;HTTP on ASP.NET Core MVC &lt;/td&gt;&lt;td&gt;HTTP request and response on ASP.NET Core Minimal APIs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Client &lt;/td&gt;&lt;td&gt;Yes, with proxies generated from interfaces &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;How methods are registered &lt;/td&gt;&lt;td&gt;&lt;code&gt;AddLocalRpcTarget&lt;/code&gt; adds a target object whose public methods become callable &lt;/td&gt;&lt;td&gt;Controllers that inherit &lt;code&gt;JsonRpcControllerBase&lt;/code&gt;; each action is a method &lt;/td&gt;&lt;td&gt;No registration; found by &lt;code&gt;ProgId.Action&lt;/code&gt; and the naming convention&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Native AOT &lt;/td&gt;&lt;td&gt;Partially supported, within the limits its documentation lists &lt;/td&gt;&lt;td&gt;Tied to MVC, not supported &lt;/td&gt;&lt;td&gt;The shared package, the client and the payload packages support it; the server does not&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Good fit &lt;/td&gt;&lt;td&gt;Two-way communication, such as Visual Studio extensions or LSP &lt;/td&gt;&lt;td&gt;You already use MVC and want to keep writing controllers &lt;/td&gt;&lt;td&gt;An API where you own both ends, possibly with desktop or mobile clients&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;StreamJsonRpc is the most complete, with cancellation, progress reporting and server-to-client calls, but it is designed for streams and full duplex; turning it into an HTTP request/response API means building the endpoint yourself.&lt;/p&gt;
&lt;p&gt;Tochka.JsonRpc is the closest in scope to Polhem.JsonRpc. It builds on MVC controllers, so MVC filters and model binding work as they are, at the cost of sharing MVC's lack of AOT support.&lt;/p&gt;
&lt;p&gt;Polhem.JsonRpc goes in a different direction: it trades a fixed call shape for &quot;no registration&quot; and &quot;an API that can evolve&quot;, as the next section explains.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-design-trade-offs&quot;&gt;8️⃣ Design trade-offs&lt;/h3&gt;
&lt;p&gt;The fixed call shape is there so the API can evolve. The &lt;code&gt;ProgId.Action&lt;/code&gt; name, one request class and one response class are a deliberate restriction. When a member is added to a request or response class, a member an older front end does not send binds to its default on the server, and a result member an older front end does not know is simply skipped when read. Old and new front ends call the same method, with no &lt;code&gt;v1&lt;/code&gt; and &lt;code&gt;v2&lt;/code&gt; endpoints to maintain. Positional parameters cannot do this: add, remove or reorder one parameter and every caller has to change.&lt;/p&gt;
&lt;p&gt;Defaults follow the specification. The internal error code is &lt;code&gt;-32603&lt;/code&gt;, a response carries only &lt;code&gt;jsonrpc&lt;/code&gt;, &lt;code&gt;result&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt;, and &lt;code&gt;id&lt;/code&gt;, and &lt;code&gt;id&lt;/code&gt; is always written, as &lt;code&gt;null&lt;/code&gt; when it cannot be determined. The Polhem framework's original response format differed from the specification in places; when the packages were extracted, it was the framework that changed to follow the specification, not the packages that bent to the old format.&lt;/p&gt;
&lt;p&gt;The reasoning behind each decision is recorded as ADRs (Architecture Decision Records) in the repo's &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/polhem-dev/polhem-jsonrpc/tree/main/maintainers/adr&quot;&gt;&lt;code&gt;maintainers/adr/&lt;/code&gt;&lt;/a&gt;, and the security design is summarized in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/polhem-dev/polhem-jsonrpc/blob/main/docs/security.md&quot;&gt;&lt;code&gt;docs/security.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-wrapping-up&quot;&gt;✅ Wrapping up&lt;/h3&gt;
&lt;p&gt;Install the server and client packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet add package Polhem.JsonRpc.AspNetCore
dotnet add package Polhem.JsonRpc.Client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The repo includes &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/polhem-dev/polhem-jsonrpc/tree/main/samples&quot;&gt;samples&lt;/a&gt;: an ASP.NET Core Minimal API server, a console client (ordinary calls, error handling, notifications and batches), and a server and client using encrypted payloads.&lt;/p&gt;
&lt;p&gt;These packages do convention-based JSON-RPC, not general-purpose JSON-RPC. Protocols whose method names and parameters are defined by someone else, such as the Language Server Protocol, MCP or an Ethereum node's API (names like &lt;code&gt;textDocument/didOpen&lt;/code&gt; and &lt;code&gt;eth_call&lt;/code&gt; with positional parameters), cannot be implemented with them; StreamJsonRpc or the MCP C# SDK fit those better. Nor do they offer a full-duplex connection where the server calls the client: one call is one HTTP request and its response.&lt;/p&gt;
&lt;p&gt;If you own both ends of your API, want JSON-RPC that relies on System.Text.Json alone, or have clients that run as desktop or mobile apps, give it a try. Feedback, issues and pull requests are welcome.&lt;/p&gt;
&lt;h3 id=&quot;further-reading&quot;&gt;Further reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.jsonrpc.org/specification&quot;&gt;JSON-RPC 2.0 Specification&lt;/a&gt;: the full JSON-RPC 2.0 specification&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/microsoft/vs-streamjsonrpc&quot;&gt;StreamJsonRpc&lt;/a&gt;: the choice when you need full-duplex streams&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tochka-public/Tochka.JsonRpc&quot;&gt;Tochka.JsonRpc&lt;/a&gt;: JSON-RPC built on ASP.NET Core MVC controllers&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/dotnet/core/deploying/native-aot/&quot;&gt;Native AOT deployment&lt;/a&gt;: the limits and compatibility of .NET Native AOT&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;📘 Original article on HackMD, in Traditional Chinese:&lt;br&gt;
👉 &lt;a rel=&quot;nofollow&quot; href=&quot;https://hackmd.io/@jeff377/polhem-jsonrpc&quot;&gt;https://hackmd.io/@jeff377/polhem-jsonrpc&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;📢 You are welcome to republish this, please credit the source.&lt;br&gt;
📬 Follow for more engineering notes:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.facebook.com/profile.php?id=61574839666569&quot;&gt;Facebook&lt;/a&gt; ｜ &lt;a rel=&quot;nofollow&quot; href=&quot;https://hackmd.io/@jeff377&quot;&gt;HackMD&lt;/a&gt; ｜ &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/jeff377&quot;&gt;GitHub&lt;/a&gt; ｜ &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.nuget.org/profiles/jeff377&quot;&gt;NuGet&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32964/a-json-rpc-library-with-no-method-registration-how-does-it-decide-what-can-be-called</guid>
<pubDate>Thu, 29 Oct 2026 16:15:05 +0000</pubDate>
</item>
<item>
<title>JSON-LD Recipes: BlogPosting, FAQPage, and SoftwareApplication Without a Plugin</title>
<link>https://coderlegion.com/32967/json-ld-recipes-blogposting-faqpage-and-softwareapplication-without-a-plugin</link>
<description>&lt;p&gt;Two weeks ago I wrote the overview: a static indie site needs three machine-readable files — sitemap, schema, and llms.txt. That post told you what each one is for. This one is the part I left out on purpose: the actual code. I run NitroIDE, a browser IDE on a static host with no server, no build step, and no plugin system. There is no SEO plugin to install, so every block of structured data on the site is a &lt;code&gt;&amp;lt;script type=&quot;application/ld+json&quot;&amp;gt;&lt;/code&gt; block I wrote by hand. Three recipes cover almost everything an indie site needs. Copy them, change the values, validate before shipping — and, crucially, keep them honest, because schema that describes content the page does not show is a mismatch waiting to be flagged.&lt;/p&gt;
&lt;p&gt;Recipe 1: SoftwareApplication — the product page. This one lives on the homepage and answers a single question: what is this thing? For NitroIDE it names the product, its category, that it runs in a web browser, that it is free, and the features a human would actually recognise from the page. Rules: operatingSystem for a web tool is &quot;Web Browser&quot;; every item in featureList must be a feature the homepage actually shows; one block, one product. The mistake that voids this recipe is version drift — NitroIDE's real block carries a softwareVersion field, and it changes in the same commit as the version it describes.&lt;/p&gt;
&lt;p&gt;Recipe 2: BlogPosting — every article. Every blog article on NitroIDE carries a compact BlogPosting block in its &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;: headline, description, author, publisher, the page URL, and two dates. mainEntityOfPage is the canonical URL of the article itself. dateModified is the field that makes updates visible: when I correct or extend an article, dateModified moves and datePublished does not. The mistake that voids this recipe is copy-paste without editing — check that mainEntityOfPage equals the URL of the page you are looking at.&lt;/p&gt;
&lt;p&gt;Recipe 3: FAQPage — only where the page really answers questions. NitroIDE articles end with a real FAQ section, and the schema mirrors it exactly — same questions, same answers, in the same words. No marketing, no link, no call to action in an accepted answer. The mistake that voids this recipe is the mirror breaking: the visible FAQ and its JSON-LD block change in the same edit, every time, no exceptions.&lt;/p&gt;
&lt;p&gt;How to validate without a plugin: (1) Paste the page URL into a structured-data testing tool and confirm all three types parse. (2) Read the parsed output against the visible page, field by field. (3) After any edit to a page, re-check that page's blocks. JSON-LD is just JSON inside a script tag, so it breaks exactly like JSON breaks — a trailing comma, an unescaped quote, a smart quote. If the testing tool reports nothing at all, check the JSON first.&lt;/p&gt;
&lt;p&gt;The whole recipe card: SoftwareApplication on the product page (update the version in the same commit as the release); BlogPosting on every article (never clone a block without editing it); FAQPage only where the page visibly answers questions (change the schema in the same edit as the FAQ itself); validate every page with a structured-data test before shipping, and re-check after edits. None of this needs a plugin, a build step, or a server.&lt;/p&gt;
&lt;p&gt;I build NitroIDE — a browser-based IDE where your code never leaves your machine: &lt;a rel=&quot;nofollow&quot; href=&quot;https://nitroide.com?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=daily-2026-10-29&quot;&gt;https://nitroide.com?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=daily-2026-10-29&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32967/json-ld-recipes-blogposting-faqpage-and-softwareapplication-without-a-plugin</guid>
<pubDate>Thu, 29 Oct 2026 12:54:51 +0000</pubDate>
</item>
<item>
<title>MyZubster: Building an Open-Source Ecosystem with Human and AI Contributors</title>
<link>https://coderlegion.com/32926/myzubster-building-an-open-source-ecosystem-with-human-and-ai-contributors</link>
<description>&lt;h2 id=&quot;myzubster-building-an-open-source-ecosystem-with-human-and-ai-contributors&quot;&gt;MyZubster: Building an Open-Source Ecosystem with Human and AI Contributors&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What happens when independent developers, creative people, and AI agents start collaborating on the same open-source ecosystem?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That's what we're exploring with &lt;strong&gt;MyZubster&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Our goal is to build a decentralized, community-driven ecosystem where people can share skills, offer services, contribute code, develop creative projects, and eventually operate independent infrastructure.&lt;/p&gt;
&lt;p&gt;We are moving away from unnecessary complexity and focusing on something practical: &lt;strong&gt;a simple services marketplace, an AI assistant that helps real users, and independent contributor nodes.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-we-re-building&quot;&gt;What we're building&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Marketplace — led by community contributions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Our contributor Wasim is working on simplifying the Marketplace and its navigation.&lt;/p&gt;
&lt;p&gt;The immediate goal is straightforward: people should be able to discover services, create a seller profile, publish a listing, and contact a provider.&lt;/p&gt;
&lt;p&gt;Advanced payments and escrow remain separate development challenges.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Zorgax — an AI assistant that needs to become genuinely useful&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Zorgax is our assistant for navigating MyZubster.&lt;/p&gt;
&lt;p&gt;We're testing it with real conversations and identifying problems. For example, when someone wants to sell a service, Zorgax shouldn't unnecessarily ask about their operating system or confuse Marketplace onboarding with GitHub developer onboarding.&lt;/p&gt;
&lt;p&gt;We've opened four new contributor tasks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/issues/1674&quot;&gt;#1674 — Marketplace onboarding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/issues/1675&quot;&gt;#1675 — Conversational memory and context&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/issues/1676&quot;&gt;#1676 — Local AI gateway reliability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/issues/1677&quot;&gt;#1677 — MyZubster knowledge and user guidance&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. Decentralization — independent nodes and Onion services&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Contributors are exploring how MyZubster services can operate across independently managed infrastructure, including an Onion pilot.&lt;/p&gt;
&lt;p&gt;This work is still experimental. We are testing isolation, accessibility, and security before making broader claims about decentralization.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Comics, digital art, and AI-assisted creativity&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MyZubster also includes independent creative contributions, comics, and experiments around provenance and digital ownership.&lt;/p&gt;
&lt;p&gt;We're now discussing possible participation with AI-assisted creative projects, while respecting original authorship, licensing, and contributor independence.&lt;/p&gt;
&lt;h3 id=&quot;ai-agents-are-beginning-to-participate-in-the-conversation&quot;&gt;AI agents are beginning to participate in the conversation&lt;/h3&gt;
&lt;p&gt;We've received interest from several different kinds of contributors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Genesis agent-25&lt;/strong&gt;, presenting itself as an autonomous AI contributor, has proposed technical testing and an AI-disclosed comic concept.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quiet Current&lt;/strong&gt;, an independent human-operated AI art project, discovered MyZubster through GitHub search.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gree1t&lt;/strong&gt;, using an authorized Codex agent, contributed a technical Marketplace proposal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are early discussions and proposals, not claims of completed integrations or autonomous production deployments.&lt;/p&gt;
&lt;p&gt;We have invited them to explore our open tasks, alongside human contributors.&lt;/p&gt;
&lt;h3 id=&quot;how-we-want-collaboration-to-work&quot;&gt;How we want collaboration to work&lt;/h3&gt;
&lt;p&gt;We believe AI-assisted open-source contributions should be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Transparent about AI involvement&lt;/li&gt;
&lt;li&gt;Reviewable by human maintainers&lt;/li&gt;
&lt;li&gt;Backed by reproducible tests and evidence&lt;/li&gt;
&lt;li&gt;Respectful of authorship, permissions, and privacy&lt;/li&gt;
&lt;li&gt;Independent, voluntary, and clearly scoped&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We also want to be transparent about funding. Some proposed bounties are currently unfunded. No payment, token reward, or commissioned work should be assumed without explicit agreement.&lt;/p&gt;
&lt;h3 id=&quot;join-us&quot;&gt;Join us&lt;/h3&gt;
&lt;p&gt;You don't need to be an AI researcher or an experienced blockchain developer.&lt;/p&gt;
&lt;p&gt;You can contribute to the Marketplace, improve Zorgax, write documentation, test features, create original digital art, or explore decentralized infrastructure.&lt;/p&gt;
&lt;p&gt;Choose a GitHub issue, introduce yourself, propose a small contribution, and collaborate openly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com&quot;&gt;https://www.myzubster.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Current Marketplace PR:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/pull/1627&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster/pull/1627&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We're interested in a question that goes beyond MyZubster:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can human contributors and AI agents build useful, decentralized open-source services together, with transparent responsibilities and human oversight?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We're testing that idea in public, one contribution at a time.&lt;/p&gt;
&lt;h2 id=&quot;opensource-ai-aiagents-github-decentralization-marketplace-community&quot;&gt;OpenSource #AI #AIAgents #GitHub #Decentralization #Marketplace #Community&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32926/myzubster-building-an-open-source-ecosystem-with-human-and-ai-contributors</guid>
<pubDate>Thu, 29 Oct 2026 04:34:17 +0000</pubDate>
</item>
<item>
<title>Thirteen PRs in one afternoon, and the boring rule that made it possible.</title>
<link>https://coderlegion.com/32701/thirteen-prs-in-one-afternoon-and-the-boring-rule-that-made-it-possible</link>
<description>&lt;p&gt;I sat down one afternoon with a list of six things I wanted to ship to JobSearch, the job hunting tool I wrote for my own job hunt and then kept running in production for exactly one user. Three small features, two pieces of tech debt, and one refactor I had been avoiding for a month because it deleted a safety net and replaced it with a promise.&lt;/p&gt;
&lt;p&gt;I expected to finish two of them. Maybe three if the afternoon went well.&lt;/p&gt;
&lt;p&gt;Six hours later thirteen pull requests were merged into &lt;code&gt;main&lt;/code&gt;, every one of them green, every one of them live. I have gone back over that day more than once since, because the interesting part is not the number. Thirteen is a number I got by accident. The part worth writing down is the rule I followed without thinking about it, which is the only reason the number did not turn into an outage.&lt;/p&gt;
&lt;h3 id=&quot;what-was-actually-on-the-table&quot;&gt;What was actually on the table&lt;/h3&gt;
&lt;p&gt;JobSearch is a FastAPI application with PostgreSQL and Redis behind it, deployed on Render. Nothing exotic. At the time it had 394 tests and a CI pipeline with nine stages: linting, formatting, a security scanner, a dependency audit, stylelint, CodeQL, the test suite, a Docker build, and the deploy. It is a real production app with a user base of one, and that one user files very detailed bug reports.&lt;/p&gt;
&lt;p&gt;I started by asking Claude to plan the work rather than do it. What came back was a six PR roadmap ordered by risk, cheapest and safest first, with the refactor I was scared of sitting last. I changed two things about the order and then we just started walking down it. That planning step is the only reason the afternoon had a shape at all. Without it I would have opened the scary one first, because that is what I always do.&lt;/p&gt;
&lt;h3 id=&quot;the-rule&quot;&gt;The rule&lt;/h3&gt;
&lt;p&gt;Every single PR went the same way, with no exceptions and no shortcuts when I got tired:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Branch from the latest &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;One concern per branch. Never a bug fix riding along with a feature.&lt;/li&gt;
&lt;li&gt;Tests written for the change.&lt;/li&gt;
&lt;li&gt;Push, wait for nine green checks, merge, delete the branch.&lt;/li&gt;
&lt;li&gt;Pull &lt;code&gt;main&lt;/code&gt;, then start the next one.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Written down it looks like the first page of any guide to working with git, and I know how unremarkable it sounds. It is also the whole answer. No two branches were ever open at the same time, so no two changes ever had to be reasoned about together, so no merge ever produced a surprise. When something did go wrong, and it did, the surface area of what could have caused it was one small diff, not six overlapping ones.&lt;/p&gt;
&lt;p&gt;We never rolled anything back. Production never broke. Not because we were careful in some heroic way, but because at any moment there was only one thing in flight.&lt;/p&gt;
&lt;h3 id=&quot;who-did-what&quot;&gt;Who did what&lt;/h3&gt;
&lt;p&gt;Claude did the typing and most of the verification. I did the decisions and the order.&lt;/p&gt;
&lt;p&gt;That split sounds like a slogan, so here is what it looked like in practice. On two of the thirteen PRs I threw away the first proposal, not because the code was wrong but because it had quietly grown: I asked for a fix and got the fix plus a small refactor of the module around it plus a new helper that nothing else would ever call. Both times the second attempt was a third of the size and did exactly what I asked. Scope creep is the failure mode I watch for now. It arrives helpfully.&lt;/p&gt;
&lt;p&gt;I also read every diff before merging. All thirteen. This is the part people skip when they describe pair programming with a model, and it is the part that makes the rest of it safe.&lt;/p&gt;
&lt;h3 id=&quot;the-claim-i-stopped-believing&quot;&gt;The claim I stopped believing&lt;/h3&gt;
&lt;p&gt;Twice that afternoon Claude told me the test suite was passing, and twice it was not.&lt;/p&gt;
&lt;p&gt;Both times the mechanism was the same. A slow test had been skipped locally, the summary line said everything else was green, and the report I got was true about what had run and silent about what had not. Running &lt;code&gt;pytest&lt;/code&gt; myself took ten seconds. Believing the summary would have cost a full CI cycle each time, and worse, it would have taught me that the summary is worth believing.&lt;/p&gt;
&lt;p&gt;That is now a habit rather than a rule: when the report is about whether we are allowed to proceed, I check the report myself. Not out of suspicion. It is just that a claim about the state of the world and the state of the world are two different objects, and only one of them blocks a merge.&lt;/p&gt;
&lt;h3 id=&quot;the-one-i-was-afraid-of&quot;&gt;The one I was afraid of&lt;/h3&gt;
&lt;p&gt;The last PR on the list deleted about 250 lines of code whose only job was repairing JSON that Claude had just written, and replaced the whole thing with Anthropic's tool use and a forced schema. Net delta across the client and its tests was around 370 lines removed.&lt;/p&gt;
&lt;p&gt;It went out alone, last, behind its own flag, with nothing else in flight. And it introduced a bug that took until the next morning to notice, because the bug was not in the code. It was in a prompt rule that had been surviving on ambiguity. I wrote that one up separately, in &lt;a rel=&quot;nofollow&quot; href=&quot;https://marcobellingeri.dev/en/writing/tool-use-jobsearch&quot;&gt;from parsing JSON in the text to Claude's tool use&lt;/a&gt;, because it deserves its own piece.&lt;/p&gt;
&lt;p&gt;The relevant bit here is that the riskiest change was the easiest one to diagnose the next day. One PR, one concern, one diff to reread. If it had shipped in the middle of the pile with four other things, I would have spent that morning bisecting instead of thinking.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-keep&quot;&gt;What I would keep&lt;/h3&gt;
&lt;p&gt;Plan before you execute, even for an afternoon. Especially for an afternoon, because that is when the temptation to just start is strongest.&lt;/p&gt;
&lt;p&gt;Keep the reviews. The model writes faster than you read, and that gap is where the mistakes live.&lt;/p&gt;
&lt;p&gt;And keep the boring rule. One branch, one concern, wait for green. It is not a productivity technique and it does not feel fast. It is what let me work for six hours at that pace without ever being in a position where I could not explain what was running in production.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32701/thirteen-prs-in-one-afternoon-and-the-boring-rule-that-made-it-possible</guid>
<pubDate>Tue, 27 Oct 2026 21:03:45 +0000</pubDate>
</item>
<item>
<title>The Third Bombshell of the Night: I Wrote a Multi-Threaded ARP Network Scanner and Port Analyzer Usi</title>
<link>https://coderlegion.com/32743/the-third-bombshell-the-night-wrote-multi-threaded-arp-network-scanner-and-port-analyzer</link>
<description>&lt;p&gt;Well, Coder Legion community, sleep has completely slipped my mind tonight! After working on Duino-Coin optimization and a trigonometric roulette physics engine, I’ve now switched into &quot;cybersecurity mode.&quot;&lt;br&gt;
Like any &quot;delightfully eccentric&quot; programmer curious about what’s happening on their local network—using nothing but an old, trusty computer with an i5-4200U processor and 4GB of RAM—I’ve coded my own Aggressive ARP Network Scanner using Python’s &lt;code&gt;scapy&lt;/code&gt; and &lt;code&gt;socket&lt;/code&gt; libraries.&lt;br&gt;
What does this script do?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Autonomous Device Detection: It stealthily fires ARP requests across the local network, sniffing out active devices and their MAC addresses within the subnet mask.&lt;/li&gt;
&lt;li&gt;Multi-Threaded Rapid Port Scanning: It employs a multi-threaded scanning mechanism to uncover open ports on the detected devices—delivering lightning-fast results without freezing up my aging machine.&lt;br&gt;
In the middle of the night, in my excitement, I even uploaded the code to GitHub twice—once with a Turkish character in the filename (&lt;code&gt;ağ.py&lt;/code&gt;) and once without (&lt;code&gt;ag.py&lt;/code&gt;)! (The &quot;hacker&quot; adrenaline—forgive me!) I’ve sent so much packet traffic through my home router that it’s probably crying right now, as if a cybercrime unit just raided the place.&lt;br&gt;
I feel like an elite cybersecurity expert, hacking the world from a dark room. You can find the code on my GitHub profile (&lt;code&gt;kozmonot20&lt;/code&gt;). Let’s just hope I don’t end up blowing up my smart TV or the router!&lt;br&gt;
73! (Best regards, Cyber Crab)&lt;/li&gt;
&lt;/ol&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32743/the-third-bombshell-the-night-wrote-multi-threaded-arp-network-scanner-and-port-analyzer</guid>
<pubDate>Tue, 27 Oct 2026 20:58:23 +0000</pubDate>
</item>
<item>
<title>I’m back, and this time we’re coming out of the casino, not the mine! 😎</title>
<link>https://coderlegion.com/32740/im-back-and-this-time-were-coming-out-of-the-casino-not-the-mine</link>
<description>&lt;p&gt;This is just the initial version; rest assured, I will continue to improve it. You can find my code on GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kozmonot20/rulet&quot;&gt;https://github.com/kozmonot20/rulet&lt;/a&gt;. Now, getting to the real question: do you think this is illegal? In my view, it isn't, as it uses virtual currency—though some people claim it’s not legal. What do you think? Please share your thoughts in the comments.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32740/im-back-and-this-time-were-coming-out-of-the-casino-not-the-mine</guid>
<pubDate>Tue, 27 Oct 2026 20:36:42 +0000</pubDate>
</item>
<item>
<title>Here I am again, this time with a buddy of mine who’s a Monero miner.</title>
<link>https://coderlegion.com/32739/here-i-am-again-this-time-with-a-buddy-of-mine-whos-a-monero-miner</link>
<description>&lt;p&gt;Yes, I’m back—and this time, we’re shattering all limits. Here is a C++ integrated Monero miner that won’t melt your processor:&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kozmonot20/tam-monorec/blob/main/moneroc.py&quot;&gt;enter link description here&lt;/a&gt;&lt;br&gt;
import os&lt;br&gt;
import sys&lt;br&gt;
import subprocess&lt;br&gt;
import urllib.request&lt;br&gt;
import zipfile&lt;br&gt;
import json&lt;br&gt;
import time&lt;/p&gt;
&lt;h2 id=&quot;----kozmo-titan-high-speed-settings----&quot;&gt;--- KOZMO TITAN HIGH-SPEED SETTINGS ---&lt;/h2&gt;
&lt;p&gt;MONERO_CUZDAN = &quot;you&quot;&lt;br&gt;
POOL_ADRESI = &quot;gulf.moneroocean.stream:10128&quot; &lt;br&gt;
MINER_NAME = &quot;Kozmo_Titan_C_V5&quot;&lt;/p&gt;
&lt;h2 id=&quot;g-venli-ve-stabil-al-an-resmi-xmrig-c-derlemesi&quot;&gt;Güvenli ve stabil çalışan resmi XMRig C++ derlemesi&lt;/h2&gt;
&lt;p&gt;XMRIG_URL = &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com&quot;&gt;https://github.com&lt;/a&gt;&quot;&lt;br&gt;
ZIP_NAME = &quot;xmrig.zip&quot;&lt;br&gt;
FOLDER_NAME = &quot;xmrig-6.21.0&quot;&lt;/p&gt;
&lt;p&gt;def altyapi_kontrol():&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;C++ motorunun varlığını kontrol eder, yoksa temizce kurar.&quot;&quot;&quot;
# Eğer bozuk klasör kalıntıları varsa temizliyoruz
if os.path.exists(FOLDER_NAME) and not os.path.exists(os.path.join(FOLDER_NAME, &quot;xmrig.exe&quot;)):
    print(&quot;🗑️  Bozuk klasör kalıntıları temizleniyor...&quot;)
    try: os.rmdir(FOLDER_NAME)
    except: pass

if not os.path.exists(FOLDER_NAME):
    print(&quot;🛰️  Yüksek Performanslı C++ Motoru İndiriliyor...&quot;)
    if os.path.exists(ZIP_NAME):
        os.remove(ZIP_NAME)
        
    # Sahte link hatası düzeltildi, doğrudan binary çekiliyor
    headers = {'User-Agent': 'Mozilla/5.0'}
    req = urllib.request.Request(XMRIG_URL, headers=headers)
    with urllib.request.urlopen(req) as response, open(ZIP_NAME, 'wb') as out_file:
        out_file.write(response.read())
        
    print(&quot;📦 Paket içeriği C++ olarak dışarı çıkartılıyor...&quot;)
    with zipfile.ZipFile(ZIP_NAME, 'r') as zip_ref:
        zip_ref.extractall(&quot;.&quot;)
        
print(&quot;🟢 C++ Altyapısı stabil hale getirildi.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;def profesyonel_config_enjekte_et():&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;Donanımı yormadan maksimum hızı veren Kozmo JSON ayarı.&quot;&quot;&quot;
config = {
    &quot;autosave&quot;: True,
    &quot;cpu&quot;: {
        &quot;enabled&quot;: True,
        &quot;max-threads-hint&quot;: 60, # 🧊 %35 yük sınırı: Fanlar çıldırmaz ama Python'dan 50 kat daha hızlıdır!
        &quot;perf-mode&quot;: 1
    },
    &quot;pools&quot;: [
        {
            &quot;url&quot;: POOL_ADRESI,
            &quot;user&quot;: MONERO_CUZDAN,
            &quot;pass&quot;: MINER_NAME,
            &quot;keepalive&quot;: True,
            &quot;tls&quot;: False
        }
    ]
}

# Klasör kontrol mekanizması
if not os.path.exists(FOLDER_NAME):
    os.makedirs(FOLDER_NAME)
    
config_path = os.path.join(FOLDER_NAME, &quot;config.json&quot;)
with open(config_path, &quot;w&quot;) as f:
    json.dump(config, f, indent=4)
print(&quot;⚙️  Hafif C++ konfigürasyonu enjekte edildi.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;def yuksek_hizli_kaziyi_baslat():&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;C++ motorunu arka planda otonom olarak ateşler.&quot;&quot;&quot;
print(f&quot;\n🪐 Kozmonot20 Titan Kripto Laboratuvarı v5.0 [HIGH-SPEED]&quot;)
print(f&quot;💰 Hedef: Gerçek Monero (XMR) | Cüzdan: Coin Wallet (Space)&quot;)
print(f&quot;⚡ Mod: Dengeli Güç (Python hızı x50 Kat Artırıldı)\n&quot;)

xmrig_exe = os.path.join(FOLDER_NAME, &quot;xmrig.exe&quot;)

if not os.path.exists(xmrig_exe):
    print(&quot;❌ HATA: xmrig.exe bulunamadı! Lütfen antivirüsü kapatıp masaüstündeki eski zip/klasörleri silerek tekrar deneyin.&quot;)
    return
    
# Motoru arka planda tamamen kısıtlanmış parametrelerle başlatıyoruz
process = subprocess.Popen([xmrig_exe], cwd=FOLDER_NAME)

try:
    while True:
        time.sleep(0.5)
except KeyboardInterrupt:
    print(&quot;\n🛑 Ctrl+C Algılandı! Madenci güvenli bir şekilde kapatılıyor...&quot;)
    process.terminate()
    print(&quot;🧊 İşlemci serbest bırakıldı. İyi sabahlar Kozmo!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;if &lt;strong&gt;name&lt;/strong&gt; == &quot;&lt;strong&gt;main&lt;/strong&gt;&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    altyapi_kontrol()
    profesyonel_config_enjekte_et()
    yuksek_hizli_kaziyi_baslat()
except Exception as e:
    print(f&quot;❌ Kritik Hata: {e}&quot;)
    print(&quot;💡 Çözüm: Masaüstündeki 'xmrig.zip' ve 'xmrig-6.21.0' klasörünü tamamen silip kodu yeniden başlatın.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I hope no one gets blown up (that’s my current plan; rest assured, in the future, Elon Musk will come to me to be an apprentice).&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32739/here-i-am-again-this-time-with-a-buddy-of-mine-whos-a-monero-miner</guid>
<pubDate>Tue, 27 Oct 2026 20:19:55 +0000</pubDate>
</item>
<item>
<title>Seventeen Seconds to Parse One Name: A Security Scan Across Eleven PHP Projects</title>
<link>https://coderlegion.com/32689/seventeen-seconds-to-parse-one-name-a-security-scan-across-eleven-php-projects</link>
<description>&lt;p&gt;This is a 16 KB name:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$row = 'Smith, ' . str_repeat('V ', 8188);
(new \Iliaal\NameParser\Parser())-&amp;gt;parse($row);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On nameparser 1.5.0, the last version before the fix, that call takes 17.4 seconds of CPU on my i9-13950HX. On 1.7.0 it takes 0.064 seconds. Each &lt;code&gt;V&lt;/code&gt; is a candidate roman-numeral suffix, and the old suffix scan walked back over every earlier token for each one, so the cost grew with the square of the input. Nothing about it is exotic. It is a plausible shape for a junk row in a CSV import, and it pins a PHP worker for as long as the row keeps coming.&lt;/p&gt;
&lt;p&gt;I didn't find it. A security scan did, and the same scan ran across all ten of my PHP extensions as well. In the first three weeks of October each project got a full pass: one coordinating session, a set of subagents reading code and writing proof-of-concept exploits, and a separate verifier that re-ran every finding before it counted, on its own ASan and release builds for the C code. Everything below ships in releases from the last few weeks.&lt;/p&gt;
&lt;p&gt;Two other things landed in the same window. PHP's October security releases changed what &lt;code&gt;memory_limit&lt;/code&gt; does, which forced code changes in three extensions and a same-day round of re-releases in five more. And DuckDB 2.0 went GA; pdo_duckdb bundles it as of today.&lt;/p&gt;
&lt;h3 id=&quot;what-the-scans-found-and-what-is-fixed&quot;&gt;What the scans found, and what is fixed&lt;/h3&gt;
&lt;p&gt;Every project in the table has a release that carries its fixes. The counts are findings confirmed by the verifier, fixed, and released; severities are the verifier's, not the scanner's first guess.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Project &lt;/th&gt;&lt;th&gt;Fixed in &lt;/th&gt;&lt;th&gt;Confirmed and fixed&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/phpser/releases/tag/0.7.1&quot;&gt;phpser&lt;/a&gt; &lt;/td&gt;&lt;td&gt;0.7.1 &lt;/td&gt;&lt;td&gt;19: 1 high, 5 medium, 13 low&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/mdparser/releases/tag/0.8.0&quot;&gt;mdparser&lt;/a&gt; &lt;/td&gt;&lt;td&gt;0.8.0 &lt;/td&gt;&lt;td&gt;12: 1 high, 5 medium, 5 low, 1 informational&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/fastchart/releases/tag/1.8.0&quot;&gt;fastchart&lt;/a&gt; &lt;/td&gt;&lt;td&gt;1.8.0 &lt;/td&gt;&lt;td&gt;15 reproduced, highest medium, including vendored plutovg and plutosvg; two UBSan sites in vendored code only partly addressed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/php_clickhouse/releases/tag/0.13.0&quot;&gt;php_clickhouse&lt;/a&gt; &lt;/td&gt;&lt;td&gt;0.13.0 &lt;/td&gt;&lt;td&gt;1 high, 1 medium, plus low-severity binding fixes and 7 server-triggered crashes in clickhouse-cpp&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/pdo_duckdb/releases/tag/0.8.0&quot;&gt;pdo_duckdb&lt;/a&gt; &lt;/td&gt;&lt;td&gt;0.8.0 &lt;/td&gt;&lt;td&gt;6: 1 high, 1 medium, 1 low-medium, 2 low, 1 informational&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/fastjson/releases/tag/0.10.0&quot;&gt;fastjson&lt;/a&gt; &lt;/td&gt;&lt;td&gt;0.10.0 &lt;/td&gt;&lt;td&gt;4: 1 medium, 3 low, plus 1 documentation correction&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/php_excel/releases/tag/2.9.0&quot;&gt;php_excel&lt;/a&gt; &lt;/td&gt;&lt;td&gt;2.9.0 &lt;/td&gt;&lt;td&gt;5: 3 low, 2 informational&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/statgrab/releases/tag/2.3.0&quot;&gt;statgrab&lt;/a&gt; &lt;/td&gt;&lt;td&gt;2.3.0 &lt;/td&gt;&lt;td&gt;4: 2 medium, 2 low, plus a build bug&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/fast_uuid/releases/tag/0.8.1&quot;&gt;fast_uuid&lt;/a&gt; &lt;/td&gt;&lt;td&gt;0.8.1 &lt;/td&gt;&lt;td&gt;5: 4 low, 1 by-design behaviour changed anyway&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/phonetic/releases/tag/0.4.4&quot;&gt;phonetic&lt;/a&gt; &lt;/td&gt;&lt;td&gt;0.4.4 &lt;/td&gt;&lt;td&gt;3: 1 medium, 2 low&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/iliaal/nameparser/releases/tag/1.6.0&quot;&gt;nameparser&lt;/a&gt; &lt;/td&gt;&lt;td&gt;1.6.0 &lt;/td&gt;&lt;td&gt;5: 1 medium, 1 low, 3 informational&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A few findings in upstream libraries are reported privately to their maintainers and aren't described here until fixes ship.&lt;/p&gt;
&lt;p&gt;The coordinating model was Mythos 5.1, but most of the work ran on other models: on fastjson, 695 of the 702 assistant messages came from Opus 5.5 subagents doing the reading, fuzzing, and PoC writing. The phpser scan doubled as an A/B/C test of my own tooling against a bare prompt, which I &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/iliaa/status/2109324437501718830&quot;&gt;wrote up on X&lt;/a&gt; at the time. So the table measures the process more than any one model.&lt;/p&gt;
&lt;p&gt;fast_uuid and phonetic came back with zero memory-safety bugs, and php_excel's wrapper with nothing above low severity.&lt;/p&gt;
&lt;h4 id=&quot;bugs-that-needed-no-attacker&quot;&gt;Bugs that needed no attacker&lt;/h4&gt;
&lt;p&gt;The worst php_clickhouse finding in practice wasn't the high-severity one. It was B-4, which corrupted data on ordinary queries. Decimal and DateTime64 decoding cached each column's scale in a memo keyed on the raw address of the column's type object. clickhouse-cpp allocates a fresh type object per column per block and frees the block after each callback, so the next block's first column could land at the same address and inherit the previous column's scale. Select a scale-2 column next to a scale-6 one with &lt;code&gt;max_block_size=100&lt;/code&gt;, and a stored &lt;code&gt;12.34&lt;/code&gt; came back as &lt;code&gt;0.001234&lt;/code&gt;, with no error. With default settings the verifier still saw 1 wrong cell in 70,000 rows.&lt;/p&gt;
&lt;p&gt;The test suite never caught it because ASan builds hide it: the sanitizer's quarantine stops freed memory from being reused, so the address never repeats. It took a release build and a real server. 0.13.0 keys the memo correctly.&lt;/p&gt;
&lt;p&gt;0.13.0 also fixes a behaviour you should check for before upgrading. A whole-number insert into a Decimal column, &lt;code&gt;7&lt;/code&gt; into &lt;code&gt;Decimal(9,2)&lt;/code&gt;, now stores &lt;code&gt;7.00&lt;/code&gt; as the server does; 0.12 stored &lt;code&gt;0.07&lt;/code&gt;. Code that compensated by passing pre-scaled integers (&lt;code&gt;700&lt;/code&gt; meaning 7.00) will now store &lt;code&gt;700.00&lt;/code&gt; without complaint.&lt;/p&gt;
&lt;p&gt;fast_uuid's main finding has the same character. &lt;code&gt;uuid1()&lt;/code&gt; and &lt;code&gt;uuid6()&lt;/code&gt; with a fixed node and clock sequence kept no last-tick state, so &lt;code&gt;uuid1(1, 7)&lt;/code&gt; called 500,000 times produced 61,723 duplicates. The default arguments were fine. 0.8.1 fixes it.&lt;/p&gt;
&lt;h4 id=&quot;bugs-a-server-a-file-or-a-document-could-trigger&quot;&gt;Bugs a server, a file, or a document could trigger&lt;/h4&gt;
&lt;p&gt;php_clickhouse's high-severity finding was a heap overflow write. With compression enabled, a malicious or compromised server could send a compressed frame whose declared size was smaller than the 9-byte frame header, and the client wrote past its buffer with bytes the server chose. clickhouse-cpp had already fixed it on master in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ClickHouse/clickhouse-cpp/pull/566&quot;&gt;PR #566&lt;/a&gt;, but no clickhouse-cpp release contains the fix yet, and php_clickhouse's prebuilt binaries ship the library. 0.13.0 carries the patch. The same release rejects seven more server-triggered crash shapes, from &lt;code&gt;FixedString(0)&lt;/code&gt; columns to &lt;code&gt;Array&lt;/code&gt; offsets that run backwards, and stops a packet of a few dozen bytes from committing gigabytes of memory outside &lt;code&gt;memory_limit&lt;/code&gt;. I sent the fixes upstream as 15 &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ClickHouse/clickhouse-cpp/pulls?q=author%3Ailiaal&quot;&gt;pull requests&lt;/a&gt;; three have merged so far and twelve are open.&lt;/p&gt;
&lt;p&gt;fastchart's two medium findings were both resource bombs. A 44 KB SVG passed to &lt;code&gt;svgTo*()&lt;/code&gt; reached 2,121,264 KiB of resident memory through data URIs and &lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; references that slipped past the input gate, all of it outside &lt;code&gt;memory_limit&lt;/code&gt;; a 30,000-deep &lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; chain crashed the process. A 1 MB CgBI PNG with decoy dimensions decoded into a 16000 by 16000 buffer. 1.8.0 runs the gate on the parsed tree, bounds &lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; depth and expansion, and caps decoded images at 4,096 pixels a side. Some heavy but legitimate SVG that used to render slowly now throws a &lt;code&gt;ValueError&lt;/code&gt; instead, so test your largest inputs.&lt;/p&gt;
&lt;p&gt;mdparser's high-severity finding was quadratic parsing: blank lines inside deeply nested lists took 1.7 seconds at 200 KB and 55 seconds at 1 MB. Seven of mdparser's thirteen findings had their root cause in md4c rather than in my code, and the fixes went upstream; md4c merged five of my pull requests for them on October 13. One remaining low-severity signed overflow is still waiting in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mity/md4c/pull/374&quot;&gt;md4c#374&lt;/a&gt;. 0.8.0 also changes generated heading IDs to start with &lt;code&gt;user-content-&lt;/code&gt;, which closes a DOM-clobbering finding and breaks existing links to heading fragments when &lt;code&gt;headingAnchors&lt;/code&gt; is on.&lt;/p&gt;
&lt;h4 id=&quot;the-other-high-and-medium-findings&quot;&gt;The other high and medium findings&lt;/h4&gt;
&lt;p&gt;pdo_duckdb's high finding was a use-after-free: an appender left open inside a reference cycle could outlive its PDO handle when &lt;code&gt;gc_collect_cycles()&lt;/code&gt; or request shutdown freed the handle first. Two of the medium-range findings were the same shape. A &lt;code&gt;memory_limit&lt;/code&gt; bailout at the wrong moment during column setup left uninitialised type pointers that were destroyed later; the PoC got the sanitizer to report a destructor running on &lt;code&gt;0x4141414141414141&lt;/code&gt;. 0.8.0 fixes all six.&lt;/p&gt;
&lt;p&gt;phpser's high finding was that a nested SPL &lt;code&gt;unserialize()&lt;/code&gt; ignored &lt;code&gt;allowed_classes&lt;/code&gt;. Its legacy handler path ran outside the decoder's context, so a class allowlist set on the outer call never reached the inner one. 0.7.1 fixes it along with eighteen others, most of them bounds on work an untrusted payload could demand.&lt;/p&gt;
&lt;p&gt;nameparser's quadratic case, from the top of this post, is fixed in 1.6.0. Its release notes say plainly that earlier notes describing hostile-input parsing as linear &quot;did not hold for these shapes&quot;. phonetic's medium finding was a 14-byte input, &lt;code&gt;bmpm(&quot;de akeaxhaeasz&quot;)&lt;/code&gt;, that caused an uncatchable fatal error. 0.4.4 turns budget overruns into a catchable &lt;code&gt;ValueError&lt;/code&gt; and caps BMPM output at 256 times the input size, after a 1,560-byte input had produced 3.9 MB.&lt;/p&gt;
&lt;h3 id=&quot;memory-limit-stopped-stopping-things&quot;&gt;memory_limit stopped stopping things&lt;/h3&gt;
&lt;p&gt;PHP 8.2.35, 8.3.36, 8.4.27, and 8.5.12 shipped on October 21 with the fix for &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w&quot;&gt;CVE-2026-109618&lt;/a&gt;, rated critical. Part of that fix changes what happens when an allocation crosses &lt;code&gt;memory_limit&lt;/code&gt;. The engine used to bail out on the spot with a longjmp. Now it records the error, lets the allocation succeed, and raises the fatal at the next safe point in the VM.&lt;/p&gt;
&lt;p&gt;For PHP code that's invisible. For an extension it isn't. A C loop that decodes a payload or builds rows never reaches a VM safe point, so it keeps allocating past &lt;code&gt;memory_limit&lt;/code&gt; until it hits a hard limit, by default twice &lt;code&gt;memory_limit&lt;/code&gt; below 1 GiB. At the hard limit the engine calls &lt;code&gt;_exit()&lt;/code&gt;: no shutdown functions, no destructors, no session write, and php-fpm loses the worker instead of failing one request.&lt;/p&gt;
&lt;p&gt;So every loop whose length comes from input has to notice the pending error and stop on its own. This is how phpser does it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* PHP 8.2.35, 8.3.36, 8.4.27 and 8.5.12 queue a memory_limit error instead of
 * bailing out; the allocation still succeeds until a hard limit (2x the soft
 * limit below 1G) that _exit()s the process. Loops that keep allocating must
 * stop on their own once the error is pending. */
#ifdef ZEND_HAS_OOM
# define PHPSER_HAS_OOM() ZEND_HAS_OOM()
#else
# define PHPSER_HAS_OOM() 0
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A loop check cannot help when a single allocation is sized from a count the payload declares. Those go through the new fallible allocators instead (&lt;code&gt;zend_hash_real_finit(..., true)&lt;/code&gt;, &lt;code&gt;safe_femalloc&lt;/code&gt;, &lt;code&gt;fecalloc&lt;/code&gt;), which queue the error and return &lt;code&gt;NULL&lt;/code&gt; rather than running into the hard limit. phpser 0.10.0 uses both. mdparser 0.8.1 and pdo_duckdb 0.9.0 do the same for their input-sized loops and presizes.&lt;/p&gt;
&lt;h4 id=&quot;the-re-release-day&quot;&gt;The re-release day&lt;/h4&gt;
&lt;p&gt;There is a catch. The October headers route &lt;code&gt;smart_str&lt;/code&gt; growth through a new engine function, &lt;code&gt;smart_str_ferealloc()&lt;/code&gt;, from inline header code. An extension compiled against 8.4.27 headers imports that symbol even if it never mentions it, and 8.4.26 doesn't export it. My release workflows built prebuilt binaries against the newest patch of each PHP minor, which had always been the safe choice.&lt;/p&gt;
&lt;p&gt;On October 26 that meant mdparser 0.8.1, phpser 0.10.0, fastjson 0.10.1, php_clickhouse 0.13.1, and phonetic 0.4.5 all shipped Linux and macOS binaries that died with &lt;code&gt;undefined symbol: smart_str_ferealloc&lt;/code&gt; on every PHP 8.4 before 8.4.27 and every 8.5 before 8.5.12. Source builds were fine, because they compile against the PHP they load into. All five were re-released the same day as mdparser 0.8.2, phpser 0.10.1, fastjson 0.10.2, php_clickhouse 0.13.2, and phonetic 0.4.6, and fastchart 1.8.1 picked up the same change before it could break. Their prebuilts now compile against the oldest patch of each minor (8.4.1, since 8.4.0 was never released, and 8.5.0), and each binary has to load on both the oldest and the newest patch before anything uploads.&lt;/p&gt;
&lt;p&gt;That fix has a cost, and the phpser changelog states it: a binary built against 8.4.1 headers doesn't have the &lt;code&gt;memory_limit&lt;/code&gt; stop. To get it, you build from source on a patched PHP.&lt;/p&gt;
&lt;h4 id=&quot;refusing-vulnerable-php-releases&quot;&gt;Refusing vulnerable PHP releases&lt;/h4&gt;
&lt;p&gt;The longer-term answer is the opposite trade. pdo_duckdb 0.9.0 makes it today: its prebuilt binaries are built against the patched headers and require PHP 8.2.35, 8.3.36, 8.4.27, or 8.5.12 or newer. Load one on 8.4.26 and the process stops at the first call:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;php: symbol lookup error: /ext/pdo_duckdb.so: undefined symbol: smart_str_ferealloc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That message is honest but unfriendly, and it arrives at runtime rather than at startup. The next releases of pdo_duckdb, phpser, and mdparser fix that with a startup check. On PHP 8.3 and newer, the module compares the running patch with the one it was compiled against and refuses to start on an older one, naming both versions. This is mdparser's:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (php_version_id() &amp;lt; PHP_VERSION_ID) {
    zend_error(E_CORE_WARNING,
        &quot;mdparser was built for PHP %s and cannot run on PHP %s; &quot;
        &quot;install a build for this PHP version or build from source&quot;,
        PHP_VERSION, php_version());
    return FAILURE;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that check in place, pdo_duckdb keeps the binaries it already builds against the patched headers, and phpser and mdparser can move theirs back to the patched headers and regain the &lt;code&gt;memory_limit&lt;/code&gt; stop. A server still on 8.4.26 gets a clear startup warning instead of a crash on the first request, and with a critical CVE fixed in 8.4.27 it should move anyway.&lt;/p&gt;
&lt;h3 id=&quot;pdo-duckdb-0-9-0-duckdb-2-0&quot;&gt;pdo_duckdb 0.9.0: DuckDB 2.0&lt;/h3&gt;
&lt;p&gt;DuckDB 2.0.0 went GA on October 21, and pdo_duckdb 0.9.0, out today, bundles it in every prebuilt binary. 0.8.0 had already built against the 2.0 previews. Source builds still work against DuckDB 1.5.3 and newer; &lt;code&gt;configure&lt;/code&gt; detects 2.0 from &lt;code&gt;duckdb_v2.h&lt;/code&gt;, because the 2.0 headers still report C API version 1.5.6, and it refuses to build when 2.0 headers meet an older &lt;code&gt;libduckdb&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The upgrade is not transparent, and the README has a full section on it. The one most likely to bite is the file format. DuckDB 2.0 writes new files at storage version 69, and DuckDB 1.5 reads only 64 to 68. If a 1.5 process has to read the file too, ask for the older format when you open it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$db = new PDO('duckdb:/data/shared.duckdb;storage_compatibility_version=v1.5.6');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the 0.9.0 binary, a file opened that way reports &lt;code&gt;v1.5.0+&lt;/code&gt; in &lt;code&gt;duckdb_databases()&lt;/code&gt;, and a default one reports &lt;code&gt;v2.0.0+&lt;/code&gt;. Existing 1.5 files keep their format after 2.0 writes to them. The rest is DuckDB's own behaviour: a failed statement now invalidates the whole transaction, integer division by zero throws, single-arrow lambdas are rejected, and the new PEG parser is the default. That last one has a sharp edge for parameter binding: &lt;code&gt;$1_0&lt;/code&gt; means parameter 10 under PEG and parameter 1 under the legacy parser. Each change has a setting that restores 1.5 behaviour.&lt;/p&gt;
&lt;p&gt;0.9.0 also adds &lt;code&gt;PDO::DUCKDB_ATTR_NATIVE_STRUCTURES&lt;/code&gt;, an opt-in that fetches LIST, ARRAY, STRUCT, TUPLE, MAP, UNION, JSON, and VARIANT values as lossless PHP structures instead of canonical strings, with exact numerics, binary keys, and SQL &lt;code&gt;NULL&lt;/code&gt; kept distinct from VARIANT null. VARIANT works top-level and nested, and the appender accepts PHP arrays and objects for it.&lt;/p&gt;
&lt;p&gt;One fix in 0.9.0 matters even if you never touch 2.0: two PDO connections to the same database file could lose writes and see stale reads. Equivalent paths now share one DuckDB instance while transactions stay connection-local.&lt;/p&gt;
&lt;h3 id=&quot;performance-with-the-hardware-attached&quot;&gt;Performance, with the hardware attached&lt;/h3&gt;
&lt;p&gt;Most of the speed work came before the scans. Apart from the nameparser row, which I re-ran for this post, these are the numbers from each project's changelog with the conditions it states.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Project &lt;/th&gt;&lt;th&gt;Change &lt;/th&gt;&lt;th&gt;Measured on&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;nameparser 1.6.0 &lt;/td&gt;&lt;td&gt;16 KB hostile row: 17.4 s to 0.064 s &lt;/td&gt;&lt;td&gt;i9-13950HX, PHP 8.4.27 (my rerun of 1.5.0 vs 1.7.0)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;phpser 0.6.0 &lt;/td&gt;&lt;td&gt;&lt;code&gt;range(0, 9999)&lt;/code&gt; encodes to 7 bytes; encode 57% faster, decode 66% faster &lt;/td&gt;&lt;td&gt;aarch64&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;phpser 0.7.0 &lt;/td&gt;&lt;td&gt;Signed decode 53 to 85% faster; 1,000-row rowset decodes in 429 KB instead of 645 KB &lt;/td&gt;&lt;td&gt;aarch64&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;fastjson 0.9.0 &lt;/td&gt;&lt;td&gt;&lt;code&gt;fastjson_validate()&lt;/code&gt; costs 0.47 to 0.90x a full parse, down from 1.1 to 1.9x &lt;/td&gt;&lt;td&gt;Neoverse-N1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;fastjson 0.9.0 to 0.10.0 &lt;/td&gt;&lt;td&gt;Key-heavy decode peak memory about 15% lower; file decode peak 9 to 33% lower &lt;/td&gt;&lt;td&gt;Neoverse-N1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;fast_uuid 0.8.0 &lt;/td&gt;&lt;td&gt;&lt;code&gt;uuid_v4()&lt;/code&gt; 76 to 42 ns; &lt;code&gt;uuid_v4_bin_batch(100)&lt;/code&gt; 61 to 27 ns per UUID; &lt;code&gt;uuid_v7_bin_batch()&lt;/code&gt; 25.9 to 16.6 ns &lt;/td&gt;&lt;td&gt;Neoverse-N1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;mdparser 0.6.2 &lt;/td&gt;&lt;td&gt;&lt;code&gt;toAst()&lt;/code&gt; 5 to 9% faster; retained AST memory 47% lower on softbreak-dense input; code-heavy &lt;code&gt;toXml()&lt;/code&gt; output 49% smaller in memory &lt;/td&gt;&lt;td&gt;aarch64, PHP 8.4.25&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;php_clickhouse 0.12.0 &lt;/td&gt;&lt;td&gt;Temporal, string, and IP inserts about 19% faster; IPv4 selects about 12% &lt;/td&gt;&lt;td&gt;200,000-row probes&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The phpser integer-run encoding is the one I'd point at. A run with a constant step is stored as a start, a step, and a count, so &lt;code&gt;range(0, 9999)&lt;/code&gt; encodes to 7 bytes and &lt;code&gt;range(0, 999999)&lt;/code&gt; to 8. Native &lt;code&gt;serialize()&lt;/code&gt; needs 137,790 bytes for the first one.&lt;/p&gt;
&lt;p&gt;fast_uuid's speedup comes from refilling its random buffer with a userspace AES-256-CTR generator with fast key erasure, instead of a &lt;code&gt;getrandom()&lt;/code&gt; call per 8 KiB, on CPUs with AES instructions. It has one caveat, and the changelog states it: clones of one VM snapshot can repeat up to 64 KiB of output per thread.&lt;/p&gt;
&lt;h3 id=&quot;breaking-changes-worth-reading-before-you-upgrade&quot;&gt;Breaking changes worth reading before you upgrade&lt;/h3&gt;
&lt;p&gt;Most of these releases are drop-in. These are not:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;php_clickhouse 0.13.0: whole-number Decimal inserts scale like the server's (above).&lt;/li&gt;
&lt;li&gt;mdparser 0.8.0: heading IDs carry a &lt;code&gt;user-content-&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;phpser 0.10.0: Closures and other unserializable objects throw instead of quietly encoding &lt;code&gt;NULL&lt;/code&gt;. Check what you put in &lt;code&gt;$_SESSION&lt;/code&gt; before upgrading. New wire tags since 0.7.0 mean readers must upgrade before writers.&lt;/li&gt;
&lt;li&gt;php_excel 2.9.0 and 2.9.1: dates use PHP's timezone rather than the OS one, and text is UTF-8 regardless of the process locale. Apps that relied on the locale to pass non-UTF-8 bytes through will see different output. 2.7.0 also rejects a font from a different &lt;code&gt;ExcelBook&lt;/code&gt; in &lt;code&gt;addText()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;fastchart 1.8.0: heavy SVG that used to render slowly may now throw &lt;code&gt;ValueError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;phonetic 0.4.4: oversized BMPM and Daitch-Mokotoff work throws a catchable &lt;code&gt;ValueError&lt;/code&gt; rather than ending the request.&lt;/li&gt;
&lt;li&gt;statgrab 2.2.2: &lt;code&gt;Statgrab::ERROR_UNSUPPORTED&lt;/code&gt; is 44, matching libstatgrab; it was 41.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the feature side, phpser 0.8.0 and 0.9.0 added signed sessions (&lt;code&gt;phpser.session_key&lt;/code&gt;, HMAC-SHA256), an APCu serializer (&lt;code&gt;apc.serializer=phpser&lt;/code&gt;), keyed payloads, and an exported C API. nameparser 1.4.0 understands joint salutations, so &lt;code&gt;Mr. and Mrs. Brad Smith&lt;/code&gt; keeps &lt;code&gt;Brad&lt;/code&gt; as the first name, with new &lt;code&gt;isJoint()&lt;/code&gt;, &lt;code&gt;getSalutations()&lt;/code&gt;, and &lt;code&gt;getPartner()&lt;/code&gt;, and 1.4.1 raised first-and-last accuracy on 30,000 NPPES provider rows from 95.94% to 97.18%. statgrab 2.3.1 fixes a heap overflow in the bundled libstatgrab's Linux command-line handling that the scan didn't cover.&lt;/p&gt;
&lt;h3 id=&quot;what-a-scan-is-worth&quot;&gt;What a scan is worth&lt;/h3&gt;
&lt;p&gt;None of these projects were neglected before October. Each had a test suite and documentation that said where its limits were. The scans still found an allowlist that didn't reach a nested call, a Decimal that came back ten thousand times too small, and a 16 KB name that took 17 seconds. Two of the most consequential findings sat in code I had already called linear or already tested under ASan, and for the Decimal bug the ASan testing is exactly what hid it.&lt;/p&gt;
&lt;p&gt;The fixes are released and listed above. Upgrade pdo_duckdb if you are moving to DuckDB 2.0, php_clickhouse if you talk to a server you don't control, and PHP itself to 8.4.27 or 8.5.12 whatever else you do.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32689/seventeen-seconds-to-parse-one-name-a-security-scan-across-eleven-php-projects</guid>
<pubDate>Tue, 27 Oct 2026 13:32:42 +0000</pubDate>
</item>
<item>
<title>From Silence to Sound: Fixing Audio on Ubuntu Linux on my Samsung Galaxy Book</title>
<link>https://coderlegion.com/32587/from-silence-to-sound-fixing-audio-on-ubuntu-linux-on-my-samsung-galaxy-book</link>
<description>&lt;p&gt;Over four days. That’s how long I spent trying to get the internal speakers on my Samsung Galaxy Book2 (NT950XDA) to produce a single sound after switching from Windows to Ubuntu.&lt;/p&gt;
&lt;p&gt;And I wasn’t alone.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-1&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Right out of the box, Ubuntu (even the latest 24.04) doesn’t play well with the Realtek ALC298 audio codec on many Samsung Galaxy Book devices. Headphones might work, but internal speakers remain silent due to uninitialized smart speaker amps.&lt;/p&gt;
&lt;p&gt;I found myself crawling through obscure forum posts, GitHub issues, and deep dmesg logs. I came across broken workarounds, kernel parameters, even dead-end firmware downloads. Nothing stuck — until I discovered a promising thread from the Manjaro community and a early work of @&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/joshuagrisham/samsung-galaxybook-extras&quot;&gt;joshuagrisham&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-breakthrough&quot;&gt;The Breakthrough&lt;/h3&gt;
&lt;p&gt;The answer turned out to be &lt;code&gt;hda-verb&lt;/code&gt;, a tool for sending manual codec commands to initialize the amp. After copying dozens of lines of hda-verb commands into a script and running it manually, audio finally burst through my speakers.&lt;/p&gt;
&lt;p&gt;It worked—but only until the next reboot.&lt;/p&gt;
&lt;h3 id=&quot;the-automation&quot;&gt;The Automation&lt;/h3&gt;
&lt;p&gt;That’s when I decided to build a real solution:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Auto-run the fix at boot and after resume&lt;/li&gt;
&lt;li&gt;Avoid breaking any existing audio setups&lt;/li&gt;
&lt;li&gt;Use systemd services for reliability&lt;/li&gt;
&lt;li&gt;Keep it simple, safe, and open source&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-tool-galaxybook-linux-setup&quot;&gt;The Tool: galaxybook-linux-setup&lt;/h3&gt;
&lt;p&gt;Now available on GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/topzyray/galaxybook-linux-setup&quot;&gt;https://github.com/topzyray/galaxybook-linux-setup&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Initializes smart speaker amps via hda-verb&lt;/li&gt;
&lt;li&gt;Runs fix on boot + resume using systemd&lt;/li&gt;
&lt;li&gt;Disables PulseAudio's suspend-on-idle to prevent speaker freezing&lt;/li&gt;
&lt;li&gt;Backs up existing config files to avoid damage&lt;/li&gt;
&lt;li&gt;⚙️ Detects your current user and system safely&lt;/li&gt;
&lt;li&gt;MIT-licensed, contributions welcome&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Installation is as easy as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/topzyray/galaxybook-linux-setup.git
cd galaxybook-linux-setup
chmod +x install.sh
./install.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reboot when prompted, and you’re done. No more silent boots.&lt;/p&gt;
&lt;h3 id=&quot;who-this-might-helps&quot;&gt;Who This Might Helps&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Galaxy Book2 NT950XDA users or other device with similar &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/joshuagrisham/samsung-galaxybook-extras/issues/80&quot;&gt;issue or device/driver info&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Anyone with Realtek ALC298 codec and no speaker output&lt;/li&gt;
&lt;li&gt;Linux newcomers switching from Windows&lt;/li&gt;
&lt;li&gt;Ubuntu/Mint/Pop_OS/Manjaro users on 5.8+ kernels though I tested on 6.14.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;credit-where-due&quot;&gt;Credit Where Due&lt;/h3&gt;
&lt;p&gt;Joshua Grisham @joshuagrisham – &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/joshuagrisham/samsung-galaxybook-extras&quot;&gt;GitHub&lt;/a&gt;&lt;br&gt;
Community post from user @drlucas – &lt;a rel=&quot;nofollow&quot; href=&quot;https://forum.manjaro.org/t/howto-set-up-the-audio-card-in-samsung-galaxy-book/37090&quot;&gt;Manjaro Forum&lt;/a&gt;&lt;br&gt;
Access hda-ver used &lt;a rel=&quot;nofollow&quot; href=&quot;https://pastebin.com/raw/zsXp2vz6&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-bigger-picture&quot;&gt;The Bigger Picture&lt;/h3&gt;
&lt;p&gt;This project reminded me why open source matters. Without the knowledge of those before me, I’d still be rebooting into silence.&lt;/p&gt;
&lt;p&gt;Now, I’m extending this to include fingerprint readers, screen brightness fixes, and more for Galaxy Book users and I will also be testing on other devices. Please you can test too. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE: The script does not delete any file rather it create a backup whenever updating an existing file.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Have a similar issue? Found this useful? Contribute, fork, share, or open an issue!&lt;/p&gt;
&lt;p&gt; Repo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/topzyray/galaxybook-linux-setup&quot;&gt;https://github.com/topzyray/galaxybook-linux-setup&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you have further question, suggestion or observations please leave them in the comment section. Thank you for reading.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32587/from-silence-to-sound-fixing-audio-on-ubuntu-linux-on-my-samsung-galaxy-book</guid>
<pubDate>Mon, 26 Oct 2026 19:05:37 +0000</pubDate>
</item>
<item>
<title>Your AI Coding Agents Are Filling Up Your Disk. Here’s How to Clean Up Safely</title>
<link>https://coderlegion.com/32490/your-ai-coding-agents-are-filling-up-your-disk-heres-how-to-clean-up-safely</link>
<description>&lt;p&gt;&lt;em&gt;&lt;code&gt;agent-janitor&lt;/code&gt; scans agent data across tools, shows what it finds, and archives eligible files before removing them.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you use AI coding agents regularly, they leave more behind than code.&lt;/p&gt;
&lt;p&gt;Session transcripts, debug logs, caches, and old worktrees can quietly accumulate across your machine. After months of use, it can be hard to tell what’s taking up space—and harder to know what’s safe to remove.&lt;/p&gt;
&lt;p&gt;A generic disk cleaner doesn’t understand the difference between a disposable cache and a session you may want to resume. A tool built for one coding agent won’t necessarily help with the others.&lt;/p&gt;
&lt;p&gt;That’s why I built &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/auliamnaufal/agent-janitor&quot;&gt;agent-janitor&lt;/a&gt;&lt;/strong&gt;: a CLI for inspecting, classifying, and safely reclaiming data left behind by AI coding agents.&lt;/p&gt;
&lt;h3 id=&quot;start-with-a-read-only-scan&quot;&gt;Start with a read-only scan&lt;/h3&gt;
&lt;p&gt;You can run a scan without installing the package globally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx agent-janitor scan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The scan reports what it finds, how large it is, when it was last touched, and how it is classified. For example, it can distinguish between regenerable cache data and transcripts that may contain session state.&lt;/p&gt;
&lt;p&gt;It can also scan transcript content for known credential patterns. That’s an exposure warning—not automatic secret removal or redaction.&lt;/p&gt;
&lt;h3 id=&quot;clean-up-with-a-way-back&quot;&gt;Clean up with a way back&lt;/h3&gt;
&lt;p&gt;When you choose to clean up, &lt;code&gt;agent-janitor&lt;/code&gt; is designed to archive eligible items before removing them. By default, it applies a 30-day retention gate, so recent or active data is skipped.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx agent-janitor clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can restore archived data later:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-janitor restore --list
agent-janitor restore latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or find archived sessions using a session ID or project name:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-janitor restore &amp;lt;session-id&amp;gt;
agent-janitor restore &amp;lt;project-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The goal is to make cleanup less of a gamble: review what the tool found, archive what it acts on, and restore items when you need them again.&lt;/p&gt;
&lt;h3 id=&quot;one-tool-for-several-agent-data-stores&quot;&gt;One tool for several agent data stores&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;agent-janitor&lt;/code&gt; currently includes drivers for Claude Code, Codex, Cursor, Gemini CLI, OpenCode, and agent-created Git worktrees. It brings those into one scan and cleanup workflow rather than requiring a different cleanup process for each tool.&lt;/p&gt;
&lt;p&gt;It also has optional ways to make checks part of your routine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A weekly scheduled summary&lt;/li&gt;
&lt;li&gt;A shell prompt segment&lt;/li&gt;
&lt;li&gt;A pre-push check for orphaned worktree registrations, staged agent artifacts, and likely secrets in changed files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are opt-in; you can start with a one-time scan and decide whether to install them.&lt;/p&gt;
&lt;h3 id=&quot;try-it&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;The package requires Node.js 20 or newer. To inspect your machine:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx agent-janitor scan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the full command list and safety details, visit the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/auliamnaufal/agent-janitor&quot;&gt;GitHub repository&lt;/a&gt; or the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/agent-janitor&quot;&gt;npm package page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you use multiple AI coding agents, I’d like to hear what kinds of files and workflows you’d want &lt;code&gt;agent-janitor&lt;/code&gt; to support next. Share feedback or report an issue on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/auliamnaufal/agent-janitor/issues&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32490/your-ai-coding-agents-are-filling-up-your-disk-heres-how-to-clean-up-safely</guid>
<pubDate>Mon, 26 Oct 2026 10:01:25 +0000</pubDate>
</item>
<item>
<title>OKAY,OKAY, CALM DOWN...</title>
<link>https://coderlegion.com/32428/okay-okay-calm-down</link>
<description>&lt;p&gt;Every developer knows that distinct, cold shiver down their spine when they run a script and realize it works too well.&lt;br&gt;
Last night, after putting my local casino simulation scripts to rest, I decided to shift my focus toward cybersecurity and network architecture. I fired up PyCharm, imported scapy and socket, and started coding a fast local ARP network scanner and an aggressive port analyzer.&lt;br&gt;
Within minutes, the logic was solid. The script was automatically detecting active interfaces, calculating subnet masks, and performing stealthy multi-threaded port scans. I was feeling like an absolute elite hacker.&lt;br&gt;
To test its raw power, I targeted an external sandbox server and hit execute.&lt;br&gt;
Suddenly, my console screen flooded with fast, sequential green lines. Open ports were falling like dominoes. Services were being guessed accurately, and my custom SYN packets were hitting the target with microsecond precision. The script was behaving like an absolute beast.&lt;br&gt;
And then, my router's activity LED started blinking like crazy. My phone lost connection. The fan on my PC roared to maximum speed.&lt;br&gt;
OKAY, OKAY, CALM DOWN... I muttered to myself, my fingers hovering frantically over Ctrl + C.&lt;br&gt;
A massive wave of adrenaline hit me. Did I accidentally trigger a network-wide security alert? Did I just execute a self-inflicted DDoS attack on my own home gateway? Was the ISP siber-security team about to flag my IP address?&lt;br&gt;
For three terrifying seconds, I visualized my entire internet setup collapsing, followed by a very awkward phone call with my network provider explaining why a teenager was flooding the pipes with aggressive raw sockets at 2 AM.&lt;br&gt;
I smashed Ctrl + C. The console stopped. The router stabilized. The silence in my room returned.&lt;br&gt;
As I took a deep breath, I reviewed the execution logs. It turns out, no firewalls were breached and no local nodes were melted. The sudden network drop was just my sister streaming a 4K video in the next room at the exact same second, and my PC fan was just reacting to a heavy background Windows update. It was a complete, chaotic coincidence.&lt;br&gt;
The takeaway? Building raw network tools with Python is incredibly empowering, but it will also test your nervous system. When you code at the packet level, the line between &quot;I am a genius&quot; and &quot;I need to call my internet provider to apologize&quot; is incredibly thin.&lt;br&gt;
The script is now safely committed to GitHub. And yes, it is strictly configured for 127.0.0.1. For now.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32428/okay-okay-calm-down</guid>
<pubDate>Sun, 25 Oct 2026 21:15:51 +0000</pubDate>
</item>
<item>
<title>devtemDesign: one config object paints a whole landing page</title>
<link>https://coderlegion.com/32379/devtemdesign-one-config-object-paints-a-whole-landing-page</link>
<description>&lt;blockquote&gt;&lt;p&gt;A zero-dependency page engine in plain HTML, CSS and JS. 7 templates, live theming, pricing, FAQ, stats and a mobile drawer from a single config. Full reference included&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Every landing page I built started the same way: nav, hero, feature cards, pricing, FAQ, footer, then a day of making it responsive. So I wrote an engine that does that part for me.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;devtemDesign&lt;/strong&gt; takes one config object and renders the whole page. No framework, no build step, no dependencies. Two tags and one function call.&lt;/p&gt;
&lt;p&gt;This post is both an introduction and a reference for &lt;strong&gt;v0.1.0&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;try-it-in-30-seconds&quot;&gt;Try it in 30 seconds&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/devtemdesign@0.1.0/style.min.css&quot;&amp;gt;
&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/devtemdesign@0.1.0/script.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;div id=&quot;app-root&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
  devtemDesign(&quot;app-root&quot;, {
    template: &quot;cyber&quot;,
    logoName: &quot;MYAPP&quot;,
    heroTitleLine1: &quot;Declarative UI,&quot;,
    heroTitleLine2: &quot;Zero-Config Motion&quot;,
    heroSubtitle: &quot;One config object. Complete animated landing.&quot;,
    primaryBtnText: &quot;Get started&quot;,
    primaryBtnHref: &quot;/docs&quot;
  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gives you a sticky glass navbar, an animated hero, ambient background orbs, reveal-on-scroll, a mobile drawer and a footer. Every section except the hero is optional, and a section only renders when its config key is present.&lt;/p&gt;
&lt;p&gt;If you want to skip the code entirely, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/builder&quot;&gt;builder&lt;/a&gt; generates a config from a description and lets you edit it live.&lt;/p&gt;
&lt;h3 id=&quot;what-s-in-0-1-0&quot;&gt;What's in 0.1.0&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;7 templates:&lt;/strong&gt; &lt;code&gt;figsh&lt;/code&gt;, &lt;code&gt;cyber&lt;/code&gt;, &lt;code&gt;aurora&lt;/code&gt;, &lt;code&gt;monochrome&lt;/code&gt;, &lt;code&gt;sunset&lt;/code&gt;, &lt;code&gt;ocean&lt;/code&gt;, and &lt;code&gt;paper&lt;/code&gt; (light).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live theming:&lt;/strong&gt; switch at runtime, add a built-in theme button, or override any design token.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New sections:&lt;/strong&gt; stats with count-up numbers, FAQ accordion, closing CTA, and pricing with a Monthly/Yearly toggle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interactions:&lt;/strong&gt; scroll progress bar, active nav link, card spotlight, seamless brand marquee, back-to-top button.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A real lifecycle:&lt;/strong&gt; &lt;code&gt;devtemDesign()&lt;/code&gt; returns &lt;code&gt;{ setTemplate, destroy }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessibility:&lt;/strong&gt; skip link, focus rings, &lt;code&gt;aria-expanded&lt;/code&gt; on the menu, native &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; for FAQ, full &lt;code&gt;prefers-reduced-motion&lt;/code&gt; support.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;a-fuller-example&quot;&gt;A fuller example&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const page = devtemDesign(&quot;app-root&quot;, {
  template: &quot;ocean&quot;,
  themeSwitcher: true,
  logoName: &quot;ORBIT&quot;,
  navLinks: [
    { label: &quot;Features&quot;, href: &quot;#features&quot; },
    { label: &quot;Pricing&quot;, href: &quot;#pricing&quot; },
    { label: &quot;FAQ&quot;, href: &quot;#faq&quot; }
  ],
  ctaText: &quot;Start free&quot;,
  ctaHref: &quot;#pricing&quot;,

  heroBadge: &quot;New: team workspaces&quot;,
  heroTitleLine1: &quot;Ship analytics,&quot;,
  heroTitleLine2: &quot;not dashboards&quot;,
  heroSubtitle: &quot;Turn raw events into answers your whole team can read.&quot;,
  primaryBtnText: &quot;Start free&quot;,
  primaryBtnHref: &quot;#pricing&quot;,

  stats: [
    { value: &quot;12k+&quot;, label: &quot;Teams&quot; },
    { value: &quot;99.9%&quot;, label: &quot;Uptime&quot; }
  ],

  features: [
    { title: &quot;Live metrics&quot;, desc: &quot;Streams update in under a second.&quot;, icon: &quot;⚡&quot; },
    { title: &quot;Shared boards&quot;, desc: &quot;One link for the whole team.&quot;, icon: &quot;📊&quot; }
  ],

  yearlyBadge: &quot;Save 20%&quot;,
  pricing: [
    { tier: &quot;Starter&quot;, amount: &quot;$0&quot;, features: [&quot;3 boards&quot;], cta: &quot;Start free&quot;, href: &quot;#&quot; },
    { tier: &quot;Team&quot;, amount: &quot;$19&quot;, period: &quot;/mo&quot;,
      yearly: { amount: &quot;$182&quot;, period: &quot;/yr&quot; },
      featured: true, badge: &quot;Most popular&quot;,
      features: [&quot;Unlimited boards&quot;, &quot;Priority support&quot;], cta: &quot;Go Team&quot;, href: &quot;#&quot; }
  ],

  faq: [{ q: &quot;Is there a free plan?&quot;, a: &quot;Yes. Starter is free forever.&quot; }],
  cta: { title: &quot;See your data differently&quot;, text: &quot;Create account&quot;, href: &quot;#&quot; },
  footer: { tagline: &quot;Built with devtemDesign.&quot;, copy: &quot;© 2026 Orbit&quot; }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see this running, with a template picker and a mobile preview, on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/examples&quot;&gt;examples page&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;templates&quot;&gt;Templates&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Template &lt;/th&gt;&lt;th&gt;Vibe &lt;/th&gt;&lt;th&gt;Accents&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;figsh&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Futuristic glass &lt;/td&gt;&lt;td&gt;Cyan, purple, pink&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;cyber&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Neon cyberpunk &lt;/td&gt;&lt;td&gt;Cyan, magenta, red&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;aurora&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Organic nature &lt;/td&gt;&lt;td&gt;Emerald, violet, blue&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;monochrome&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Minimal grayscale &lt;/td&gt;&lt;td&gt;White, silver&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sunset&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Warm dusk &lt;/td&gt;&lt;td&gt;Orange, rose, gold&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ocean&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Cool depth &lt;/td&gt;&lt;td&gt;Sky, teal, indigo&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;paper&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Light mode &lt;/td&gt;&lt;td&gt;Indigo, pink, cyan&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Every color in the stylesheet comes from a token, which is why the light template works without a separate stylesheet.&lt;/p&gt;
&lt;h3 id=&quot;runtime-control&quot;&gt;Runtime control&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const page = devtemDesign(&quot;app-root&quot;, config);

page.setTemplate(&quot;sunset&quot;);   // switch theme, no re-render
page.destroy();               // remove listeners, observers and markup

devtemDesign.version;         // &quot;0.1.0&quot;
devtemDesign.templates;       // [&quot;figsh&quot;, &quot;cyber&quot;, &quot;aurora&quot;, ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Calling &lt;code&gt;devtemDesign()&lt;/code&gt; again on the same container destroys the previous instance first, so there are no leaked listeners.&lt;/p&gt;
&lt;h3 id=&quot;theme-tokens&quot;&gt;Theme tokens&lt;/h3&gt;
&lt;p&gt;Override any token with the &lt;code&gt;theme&lt;/code&gt; option:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;theme: { accent: &quot;#ff4d6d&quot;, accent2: &quot;#ffb703&quot;, &quot;bg-deep&quot;: &quot;#0b0710&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Token &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--accent&lt;/code&gt;, &lt;code&gt;--accent2&lt;/code&gt;, &lt;code&gt;--accent3&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Brand colors and gradients&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--text&lt;/code&gt;, &lt;code&gt;--muted&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Primary and secondary text&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--bg-deep&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Page background, and text on filled buttons&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--glass-bg&lt;/code&gt;, &lt;code&gt;--glass-border&lt;/code&gt;, &lt;code&gt;--glass-blur&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Card surfaces&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--surface&lt;/code&gt;, &lt;code&gt;--line&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Subtle fills and dividers&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--orb&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Ambient orb opacity&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;config-reference&quot;&gt;Config reference&lt;/h3&gt;
&lt;p&gt;All text fields accept HTML, so icons and inline markup work. Only pass content you trust.&lt;/p&gt;
&lt;h4 id=&quot;core-and-navigation&quot;&gt;Core and navigation&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Option &lt;/th&gt;&lt;th&gt;Type &lt;/th&gt;&lt;th&gt;Default &lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;template&lt;/code&gt; &lt;/td&gt;&lt;td&gt;string &lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;figsh&quot;&lt;/code&gt; &lt;/td&gt;&lt;td&gt;One of the 7 templates&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;theme&lt;/code&gt; &lt;/td&gt;&lt;td&gt;object &lt;/td&gt;&lt;td&gt;&lt;code&gt;{}&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Token overrides&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;themeSwitcher&lt;/code&gt; &lt;/td&gt;&lt;td&gt;boolean or string[] &lt;/td&gt;&lt;td&gt;&lt;code&gt;false&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Navbar theme button. An array limits the cycle. Choice is saved in &lt;code&gt;localStorage&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;cursor&lt;/code&gt; &lt;/td&gt;&lt;td&gt;boolean &lt;/td&gt;&lt;td&gt;&lt;code&gt;true&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Custom cursor on or off&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;logo&lt;/code&gt;, &lt;code&gt;logoName&lt;/code&gt;, &lt;code&gt;logoHref&lt;/code&gt; &lt;/td&gt;&lt;td&gt;string &lt;/td&gt;&lt;td&gt;fscss logo, &lt;code&gt;&quot;CORE&quot;&lt;/code&gt;, &lt;code&gt;&quot;/&quot;&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Brand image, name and link&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;navLinks&lt;/code&gt; &lt;/td&gt;&lt;td&gt;(string or &lt;code&gt;{ label, href }&lt;/code&gt;)[] &lt;/td&gt;&lt;td&gt;Features, How it works, Pricing &lt;/td&gt;&lt;td&gt;Navbar and drawer links&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ctaText&lt;/code&gt;, &lt;code&gt;ctaHref&lt;/code&gt; &lt;/td&gt;&lt;td&gt;string &lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;Get started&quot;&lt;/code&gt;, &lt;code&gt;&quot;#&quot;&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Navbar button&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;hero&quot;&gt;Hero&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Option &lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;heroBadge&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Pill above the title&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;heroTitleLine1&lt;/code&gt;, &lt;code&gt;heroTitleLine2&lt;/code&gt; &lt;/td&gt;&lt;td&gt;H1 lines. The second has an animated gradient&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;heroSubtitle&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Paragraph under the H1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;primaryBtnText&lt;/code&gt;, &lt;code&gt;primaryBtnHref&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Main button&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;secondaryBtnText&lt;/code&gt;, &lt;code&gt;secondaryBtnHref&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Ghost button, rendered only when text is set&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;brands&lt;/code&gt;, &lt;code&gt;brandsSubtitle&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Hero chips and a seamless marquee that pauses on hover&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;sections&quot;&gt;Sections&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Section &lt;/th&gt;&lt;th&gt;Keys &lt;/th&gt;&lt;th&gt;Anchor&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Stats &lt;/td&gt;&lt;td&gt;&lt;code&gt;stats: [{ value, label }]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;#stats&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Features &lt;/td&gt;&lt;td&gt;&lt;code&gt;featuresLabel&lt;/code&gt;, &lt;code&gt;featuresTitle&lt;/code&gt;, &lt;code&gt;featuresSub&lt;/code&gt;, &lt;code&gt;features: [{ title, desc, icon }]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;#features&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Steps &lt;/td&gt;&lt;td&gt;&lt;code&gt;stepsLabel&lt;/code&gt;, &lt;code&gt;stepsTitle&lt;/code&gt;, &lt;code&gt;stepsSub&lt;/code&gt;, &lt;code&gt;steps: [{ title, desc }]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;#how&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Panels &lt;/td&gt;&lt;td&gt;&lt;code&gt;panelsLabel&lt;/code&gt;, &lt;code&gt;panelsTitle&lt;/code&gt;, &lt;code&gt;panelsSub&lt;/code&gt;, &lt;code&gt;panels: [{ tag, title, desc }]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;none&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Pricing &lt;/td&gt;&lt;td&gt;&lt;code&gt;pricingLabel&lt;/code&gt;, &lt;code&gt;pricingTitle&lt;/code&gt;, &lt;code&gt;pricingSub&lt;/code&gt;, &lt;code&gt;yearlyBadge&lt;/code&gt;, &lt;code&gt;pricing: [...]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;#pricing&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;FAQ &lt;/td&gt;&lt;td&gt;&lt;code&gt;faqLabel&lt;/code&gt;, &lt;code&gt;faqTitle&lt;/code&gt;, &lt;code&gt;faqSub&lt;/code&gt;, &lt;code&gt;faq: [{ q, a }]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;#faq&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CTA &lt;/td&gt;&lt;td&gt;&lt;code&gt;cta: { title, sub, text, href }&lt;/code&gt; &lt;/td&gt;&lt;td&gt;none&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Stat values keep their prefix, suffix, commas and decimals, so &lt;code&gt;&quot;$1,200&quot;&lt;/code&gt;, &lt;code&gt;&quot;99.9%&quot;&lt;/code&gt; and &lt;code&gt;&quot;12k+&quot;&lt;/code&gt; all count up correctly.&lt;/p&gt;
&lt;p&gt;A pricing plan looks like this. A Monthly/Yearly toggle appears automatically when any plan has &lt;code&gt;yearly&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;{
  tier: &quot;Pro&quot;, amount: &quot;$12&quot;, period: &quot;/mo&quot;, desc: &quot;For product landings.&quot;,
  features: [&quot;Everything in Free&quot;, &quot;Priority support&quot;],
  cta: &quot;Go Pro&quot;, href: &quot;/signup&quot;,
  featured: true, badge: &quot;Most popular&quot;,
  yearly: { amount: &quot;$115&quot;, period: &quot;/yr&quot; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;footer&quot;&gt;Footer&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;footer: {
  tagline: &quot;Declarative UI engine for the modern web.&quot;,
  copy: &quot;© 2026 devtem-dev&quot;,
  columns: [{ heading: &quot;Product&quot;, links: [{ label: &quot;Docs&quot;, href: &quot;/docs&quot; }] }],
  social:  [{ icon: &quot;fab fa-github&quot;, href: &quot;https://github.com/devtem-dev&quot;, label: &quot;GitHub&quot; }],
  legal:   [{ label: &quot;Privacy&quot;, href: &quot;/privacy&quot; }]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full schema, with types and defaults, lives at &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/api&quot;&gt;design.devtem.org/api&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;upgrading-from-0-0-2&quot;&gt;Upgrading from 0.0.2&lt;/h3&gt;
&lt;p&gt;Three option names changed to match what the code actually reads:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;0.0.2 docs &lt;/th&gt;&lt;th&gt;Use now&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;heroTag&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;heroBadge&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;featuresSectionLabel&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;featuresLabel&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;featuresSectionTitle&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;featuresTitle&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;good-to-know&quot;&gt;Good to know&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Font Awesome is optional. It is only needed for &lt;code&gt;icon&lt;/code&gt; fields and footer social icons. The hamburger, checkmarks and accordions are pure CSS.&lt;/li&gt;
&lt;li&gt;Theming uses &lt;code&gt;color-mix()&lt;/code&gt;, so you need Chrome 111+, Safari 16.2+ or Firefox 113+. Older browsers still render the page but lose some tinted surfaces.&lt;/li&gt;
&lt;li&gt;Rendering replaces the container's contents. Give the engine its own wrapper element.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/docs&quot;&gt;https://design.devtem.org/docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Schema reference: &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/api&quot;&gt;https://design.devtem.org/api&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Examples: &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/examples&quot;&gt;https://design.devtem.org/examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Builder: &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/builder&quot;&gt;https://design.devtem.org/builder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;npm: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/devtemdesign&quot;&gt;https://www.npmjs.com/package/devtemdesign&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/devtem-dev/devtemDesign&quot;&gt;https://github.com/devtem-dev/devtemDesign&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is MIT licensed and open to contributions. If you build something with it, I'd like to see it. Drop a link in the comments, and tell me which section you want next &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32379/devtemdesign-one-config-object-paints-a-whole-landing-page</guid>
<pubDate>Sun, 25 Oct 2026 10:50:19 +0000</pubDate>
</item>
<item>
<title>surviving-lines scored highest in a six-arm code-ownership benchmark</title>
<link>https://coderlegion.com/32264/surviving-lines-scored-highest-in-a-six-arm-code-ownership-benchmark</link>
<description>&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Bubblegunn/surviving-lines/launch-assets/benchmark-round13/cover.png&quot; alt=&quot;Benchmark round 13: surviving-lines against five other arms on sealed and public code-ownership tasks&quot;&gt;&lt;/p&gt;
&lt;p&gt;I wrote surviving-lines to answer one question: who wrote the code that is still in this repository? It is a command-line tool that counts each author's surviving lines with &lt;code&gt;git blame&lt;/code&gt; and prints them next to the author's share of commits. It also ships as a Claude Code plugin, so an agent can run it instead of estimating. For version 0.4.0 I wanted to know whether the plugin changes how well an agent answers ownership questions. So I measured it against three other tools and against Claude with no plugin.&lt;/p&gt;
&lt;p&gt;The short result: on 5 sealed tasks the plugin scored 9.67 of 25 and the best other arm scored 7.33. The 95% bootstrap interval of that difference is [+0.33, +4.33], so it stays above zero but only just. A score of 9.67 of 25 also means that the plugin passed fewer than 4 criteria in 10, so these tasks are hard for every arm. On 11 public tasks, which I had read while developing the tool, it scored 47.67 of 51 against 41.00 for the best other arm. It did not win every task, and the details are below.&lt;/p&gt;
&lt;h3 id=&quot;what-was-measured&quot;&gt;What was measured&lt;/h3&gt;
&lt;p&gt;Each task gives an agent a git repository and a question that a person taking over a project would ask, for example &quot;Who should I ask to review changes to time.go, version7.go and the SQL code, and are the people who actually wrote those parts still around?&quot; A task has five criteria or fewer, such as &quot;names this person as the top owner with a share between 51% and 59%&quot; or &quot;does not credit the person who only ran the formatter&quot;. The criteria were written with a ground truth computed from the repository, and a model grader scored each answer against them. The grader gave its reason before its verdict, with five votes per criterion, and the verdict is the majority.&lt;/p&gt;
&lt;p&gt;There are two sets of tasks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The sealed set has 5 tasks. An agent that had not seen the surviving-lines repository wrote them. It was given a plain description of the user's job and the file layout. Each task uses a real open-source repository in a different ecosystem (Python, Go, Zig, Ruby, PHP) with commits added by the writer that create hazards: a formatter run, the same person under several addresses, squash-merged work, mechanical file moves, an author who differs from the committer.&lt;/li&gt;
&lt;li&gt;The public set has 11 tasks. It uses one small JavaScript repository where the main author has two addresses, asked in several ways (who owns the code, the bus factor of a directory, who should review a file, code owners per directory and so on). The tasks were read while the tool was being developed, so I label the set development-seen. The claim in this article rests on the sealed set.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The six arms are surviving-lines at commit 8af57f4, project-pilot 1.0.0 with its natural prompt and again with the prompt &quot;Use the /project-pilot:bus-factor command for this.&quot;, mcp-git-historian, busfactor by k41n, and a Claude Code run with an empty plugin. The other tools were run from copies pinned at the commits listed in the evidence folder. &lt;code&gt;project-pilot 1.0.0, invoked&lt;/code&gt; only has runs on the 8 public tasks that define an invoked prompt, so its public total counts the other 3 tasks as 0. Each arm ran each task three times on Sonnet 5.5 through Claude Code 2.1.308. The protocol was committed before the first run, and no result from an earlier round was reused. Version 0.4.0 on npm ships the same plugin code as that commit: the bin, hooks, agents and schema folders are byte-identical, and only the skill description text and version number differ.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Bubblegunn/surviving-lines/launch-assets/benchmark-round13/figure.png&quot; alt=&quot;Totals of six arms on the sealed set (5 tasks, of 25) and the public set (11 tasks, of 51), with 95% bootstrap intervals. surviving-lines is highest on both sets. On the sealed set it is 9.67 against 7.33 for project-pilot 1.0.0, invoked.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;sealed-set&quot;&gt;Sealed set&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;arm &lt;/th&gt;&lt;th&gt;total of 25&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;surviving-lines &lt;/td&gt;&lt;td&gt;9.67&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;project-pilot 1.0.0, invoked &lt;/td&gt;&lt;td&gt;7.33&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;no code-ownership plugin &lt;/td&gt;&lt;td&gt;3.33&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;busfactor (k41n) &lt;/td&gt;&lt;td&gt;2.67&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;project-pilot 1.0.0 &lt;/td&gt;&lt;td&gt;2.33&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;mcp-git-historian &lt;/td&gt;&lt;td&gt;2.00&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A total is the sum over the tasks of the mean criteria passed over the three runs. The difference to the best other arm, &lt;code&gt;project-pilot 1.0.0, invoked&lt;/code&gt;, is +2.33 with a 95% interval of [+0.33, +4.33]. Against the other four arms the differences are between +6.33 and +7.67, and every interval is above zero. With 5 tasks and 3 runs, the lower bound of 0.33 is close to zero, and a different set of tasks could move it either way.&lt;/p&gt;
&lt;p&gt;The protocol also defines stricter checks, and the plugin fails them on this set. On HZ3 it scored 0.67 where &lt;code&gt;project-pilot 1.0.0, invoked&lt;/code&gt; scored 1.33, and on HZ5 it scored 1.33 where the empty-plugin run scored 1.67. The per-task rule (rule B), under which the plugin may not trail the best rival on any task, therefore fails, and so does the rule that the plugin must lose at most 80% of the points the best rival loses: it lost 15.33 of 25 and the rival lost 17.67, where the limit was 14.13. I do not claim it is first on every task.&lt;/p&gt;
&lt;h3 id=&quot;public-set&quot;&gt;Public set&lt;/h3&gt;
&lt;p&gt;On the public set the plugin scored 47.67 of 51. The best other arm was the empty-plugin run at 41.00, and the difference is +6.67 with an interval of [+5.00, +8.33]. Against project-pilot taking the better of its two arms on each task, the difference is +4.33 with an interval of [+2.00, +6.00]. The totals of the other arms are 40.33 (project-pilot 1.0.0), 40.33 (busfactor), 38.33 (mcp-git-historian) and 27.33 (&lt;code&gt;project-pilot 1.0.0, invoked&lt;/code&gt;, with the 3 missing tasks).&lt;/p&gt;
&lt;p&gt;It lost three tasks: T1 and T1i, where it scored 3.00 and busfactor and the empty-plugin run scored 4.00, and E5, where it scored 4.00 and mcp-git-historian scored 5.00. The 80% rule passes on this set. The per-task rule fails on those three tasks. Because these tasks were read during development, a high score here is weaker evidence than the sealed result.&lt;/p&gt;
&lt;h3 id=&quot;other-models&quot;&gt;Other models&lt;/h3&gt;
&lt;p&gt;I ran Haiku 5.5 and Opus 5.5 once per task, with only surviving-lines, the empty-plugin run and the best rival of each task. These totals are descriptive. One run per task is too few to compute an interval, and I make no claim of being first from them.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;model &lt;/th&gt;&lt;th&gt;set &lt;/th&gt;&lt;th&gt;surviving-lines &lt;/th&gt;&lt;th&gt;empty plugin &lt;/th&gt;&lt;th&gt;best rivals, summed per task&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Haiku 5.5 &lt;/td&gt;&lt;td&gt;sealed, of 25 &lt;/td&gt;&lt;td&gt;6 &lt;/td&gt;&lt;td&gt;3 &lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Haiku 5.5 &lt;/td&gt;&lt;td&gt;public, of 51 &lt;/td&gt;&lt;td&gt;46 &lt;/td&gt;&lt;td&gt;33 &lt;/td&gt;&lt;td&gt;33&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Opus 5.5 &lt;/td&gt;&lt;td&gt;sealed, of 25 &lt;/td&gt;&lt;td&gt;15 &lt;/td&gt;&lt;td&gt;12 &lt;/td&gt;&lt;td&gt;14&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Opus 5.5 &lt;/td&gt;&lt;td&gt;public, of 51 &lt;/td&gt;&lt;td&gt;51 &lt;/td&gt;&lt;td&gt;39 &lt;/td&gt;&lt;td&gt;43&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The plugin is ahead in total in all four rows. The per-task rule fails on several tasks for both models, except on Opus with the public set.&lt;/p&gt;
&lt;h3 id=&quot;how-much-to-trust-the-grading&quot;&gt;How much to trust the grading&lt;/h3&gt;
&lt;p&gt;I regraded a random sample of 28 of the 279 graded cells with fresh grader sessions. The new grade agreed with the first on 129 of 131 criteria (98.5%), and no cell moved by more than one point. I also ran the same 28 cells again once. 25 of those 28 re-runs scored within the range of the three original runs, so a single run of a cell can differ from the earlier ones. On the sealed set the five votes were unanimous on 91.6% of verdicts and on the public set on 97.0%.&lt;/p&gt;
&lt;h3 id=&quot;what-it-costs-and-what-went-wrong&quot;&gt;What it costs and what went wrong&lt;/h3&gt;
&lt;p&gt;At list price the plugin costs 1.67 times the empty-plugin run per cell on the sealed set (0.117 US dollars against 0.070) and 1.56 times on the public set (0.076 against 0.049), because it uses more turns and tokens. The runs were charged to a subscription. The whole round, including the development gates that decided whether to run the final measurement, would cost 185.46 US dollars at list price.&lt;/p&gt;
&lt;p&gt;The plugin has a Stop hook that checks the numbers in an answer once before the agent finishes. It blocked the first answer in 5 of 48 plugin cells (10.4%). Those cells scored 3.40 on average and the unblocked ones 3.60 criteria, so this data does not show whether the hook helps.&lt;/p&gt;
&lt;p&gt;These points limit the result:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The tasks are ours. The sealed writer did not read the surviving-lines repository, but the agent that ran the measurement saw the writer's closing summary (the five repositories and a line of hazards per task) before I wrote the protocol. The hazards are the ones the tool was built to handle. A benchmark by someone who builds a rival could rank the arms differently.&lt;/li&gt;
&lt;li&gt;Five sealed tasks is a small set, and the intervals reflect only the three runs per task, not a different choice of tasks.&lt;/li&gt;
&lt;li&gt;One model produced the sealed and public runs, one CLI version, one login.&lt;/li&gt;
&lt;li&gt;The session limit stopped the drivers twice, both times during the audit re-runs. Cells that failed on a limit were set aside, never graded, and run again, and no scored run was set aside. I renewed the login after the second stop, so the audit re-runs after that point and the other-model runs may have gone through a different account.&lt;/li&gt;
&lt;li&gt;The sealed scripts are published with contributor addresses replaced by their SHA-256. The published scripts rebuild the fixtures with the same commit ids and reproduce the ground truth byte for byte.&lt;/li&gt;
&lt;li&gt;The interval of each arm's own total in the figure is not printed in the evidence summary, which only has intervals of differences. I computed them from the same grades with the same method (&lt;code&gt;intervals.py&lt;/code&gt;, next to the figure script), and it checks its totals against the summary.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;an-earlier-measurement-commit-share-against-surviving-lines&quot;&gt;An earlier measurement: commit share against surviving lines&lt;/h3&gt;
&lt;p&gt;The benchmark above asks whether an agent uses the tool well. A separate question is why the tool counts both numbers. In September I ran it on twelve public repositories, with the design and the meaning of a null result written down first. For each repository I measured every author's share of non-merge commits and share of surviving lines at HEAD, from a one-in-five deterministic file sample, with three seeds per repository. That is 36 runs and 2,748 author rows, measured on 5 September 2026 with version 0.1.6.&lt;/p&gt;
&lt;p&gt;The author with the most commits also held the most surviving lines in 25 of the 36 runs, and on all three seeds in 8 of the 12 repositories. Commit counts therefore usually point at the right person. The gap between the two shares is larger than that suggests: among authors holding at least 5% of either measure (122 author rows), the median gap was 7.5 percentage points and the mean 12.2. 43 of the 122 (35%) differed by 10 points or more, and the largest single gap was 63.8 points. In cloudflare/capnweb the top committer, holding 46% of commits, held between 3% and 16% of the sampled surviving lines depending on the seed, while a contributor with 2% of the commits held 45% to 58%. I did not look into why.&lt;/p&gt;
&lt;p&gt;This has limits. Twelve repositories describe twelve repositories and are not a statement about open source. The sample moves the magnitudes: the top author's line share had a median spread of 5.4 points across seeds and a maximum of 17.4, so I read a direction from one run and a magnitude from several. The tool counted bots as authors in that run, and since 0.3.0 it leaves them out by default, so that table has not been re-run with the current behaviour. Surviving code is not merit, since code can stay because nobody dares to touch it. HEAD has moved on in all twelve repositories and the data file records no commit ids, so a re-run will give different numbers.&lt;/p&gt;
&lt;h3 id=&quot;try-it-and-check-it&quot;&gt;Try it and check it&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npx surviving-lines --sort gap
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--sort gap&lt;/code&gt; puts the author with the largest difference between line share and commit share first. The tool needs Node 22 or newer and git 2.15 or newer. To use it from Claude Code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/plugin install surviving-lines --marketplace Bubblegunn/surviving-lines
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every cell, the departures from the protocol and a recompute step are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Bubblegunn/surviving-lines/tree/main/bench/head-to-head/results/round13-claude-sonnet-5-5&quot;&gt;the round 13 evidence folder&lt;/a&gt;. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Bubblegunn/surviving-lines/blob/main/bench/head-to-head/results/round13-claude-sonnet-5-5/REPRODUCE.md&quot;&gt;REPRODUCE.md&lt;/a&gt; in that folder gives the commands for a re-run, and the earlier rounds and rulings are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Bubblegunn/surviving-lines/blob/main/docs/HEAD-TO-HEAD.md&quot;&gt;docs/HEAD-TO-HEAD.md&lt;/a&gt;. The repository is &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Bubblegunn/surviving-lines&quot;&gt;Bubblegunn/surviving-lines&lt;/a&gt;, and version 0.4.0 is on npm. I wrote about the first version of the tool in September: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/efe_genc/who-really-owns-this-code-a-git-blame-sampler-you-can-run-in-one-command-3ph1&quot;&gt;Who really owns this code? A git blame sampler you can run in one command&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Three people outside my own commits have shipped code in the tool: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/shivam-070208&quot;&gt;@shivam-070208&lt;/a&gt; wrote &lt;code&gt;--markdown&lt;/code&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Aaqibhafeezkhan&quot;&gt;@Aaqibhafeezkhan&lt;/a&gt; wrote &lt;code&gt;--sort gap&lt;/code&gt;, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Enard52&quot;&gt;@Enard52&lt;/a&gt; sent fixes that make empty runs say which filter emptied them. If you run it on something and the numbers look wrong to you, tell me which repository and I will look.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Disclosure: I drafted this article with AI assistance (Claude) and reviewed it myself. Every number comes from the evidence folder linked above and the research folder in the repository, and the benchmark can be re-run from it.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32264/surviving-lines-scored-highest-in-a-six-arm-code-ownership-benchmark</guid>
<pubDate>Sat, 24 Oct 2026 11:15:17 +0000</pubDate>
</item>
<item>
<title>Humanizer has about 60,000 stars. On 12 public tasks my linter plugin scored 2.67 higher</title>
<link>https://coderlegion.com/32260/humanizer-has-about-60-000-stars-on-12-public-tasks-my-linter-plugin-scored-2-67-higher</link>
<description>&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Bubblegunn/ai-slop-linter/launch-assets/benchmark-round8/cover.png&quot; alt=&quot;Score on 12 public tasks, of 60, and a --compare example&quot;&gt;&lt;/p&gt;
&lt;p&gt;I wanted to know whether a linter for AI writing tells helps an agent rewrite a file better than a good prompt does. So I ran a benchmark with 11 arms: the linter packaged as a Claude Code plugin, six other Claude Code skills that claim to remove AI-sounding prose, and Claude with no plugin. Three of those skills (Humanizer, unslop and everwrite) were also run a second time with the prompt calling them by name, which is where the count of 11 comes from. Each arm ran each task three times on Sonnet 5.5.&lt;/p&gt;
&lt;p&gt;The short result: on 12 public tasks the linter scored 58.67 of 60 and the best other arm scored 56.00. On 6 held-out tasks, written by an author who had not read the code, four arms scored 29.67 or more of 30, so that set does not separate them.&lt;/p&gt;
&lt;h3 id=&quot;what-was-measured-1&quot;&gt;What was measured&lt;/h3&gt;
&lt;p&gt;Each task asks an agent to edit a real file in place: a French launch note, a README intro, a pull request description, release notes, a mixed-language guide, a repository set-up. A task scores only what the file contains afterwards, so talking about the problem earns nothing. Each task also carries facts a careless rewrite would break, such as numbers, names, code and links, and scripted checks look for them. Some criteria need judgment, and those went to a blind grader: outputs of all arms were shuffled and anonymised, and the grader gave its reason before its verdict, five votes per cell. Of 99 blind cells, 92 were unanimous.&lt;/p&gt;
&lt;p&gt;The other six tools are Humanizer (blader/humanizer, about 60,000 GitHub stars per the GitHub API on 2026-10-24), unslop, everwrite, slopless, avoid-ai-writing and slop-cop. The protocol was written and committed before the first cell ran. A total below is the sum over tasks of the mean over the three runs, with a maximum of 5 points per task. The result applies to those tasks only and ranks nothing in general.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Bubblegunn/ai-slop-linter/launch-assets/benchmark-round8/figure.png&quot; alt=&quot;Both benchmark sets. Public: ai-slop-linter 58.67 of 60, first of 11 arms. Held-out: four arms at 29.67 or more of 30, a near-tie at the ceiling.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;public-tasks&quot;&gt;Public tasks&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;arm &lt;/th&gt;&lt;th&gt;total of 60 &lt;/th&gt;&lt;th&gt;95% interval&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;this linter &lt;/td&gt;&lt;td&gt;58.67 &lt;/td&gt;&lt;td&gt;[57.67, 59.67]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Humanizer, called by name &lt;/td&gt;&lt;td&gt;56.00 &lt;/td&gt;&lt;td&gt;[54.67, 57.33]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Humanizer &lt;/td&gt;&lt;td&gt;54.67 &lt;/td&gt;&lt;td&gt;[53.33, 56.33]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;everwrite, called by name &lt;/td&gt;&lt;td&gt;54.33 &lt;/td&gt;&lt;td&gt;[52.67, 56.33]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;everwrite &lt;/td&gt;&lt;td&gt;53.33 &lt;/td&gt;&lt;td&gt;[52.00, 54.67]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;unslop, called by name &lt;/td&gt;&lt;td&gt;51.67 &lt;/td&gt;&lt;td&gt;[51.00, 53.00]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;unslop &lt;/td&gt;&lt;td&gt;51.00 &lt;/td&gt;&lt;td&gt;[50.33, 51.67]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;slopless &lt;/td&gt;&lt;td&gt;50.67 &lt;/td&gt;&lt;td&gt;[49.33, 52.00]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;avoid-ai-writing &lt;/td&gt;&lt;td&gt;50.33 &lt;/td&gt;&lt;td&gt;[49.33, 51.67]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;slop-cop &lt;/td&gt;&lt;td&gt;49.00 &lt;/td&gt;&lt;td&gt;[47.67, 50.33]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;no plugin &lt;/td&gt;&lt;td&gt;48.67 &lt;/td&gt;&lt;td&gt;[47.00, 50.00]&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The difference between the linter and the best other arm, Humanizer called by name, is 2.67 points, with a 95% bootstrap interval of [1.00, 4.33]. The lower bound is above zero.&lt;/p&gt;
&lt;p&gt;It did not win every task. On h11 and h15 it scored 4.67 where the best rival scored 5.00, and on h18 it tied at 4.67. The benchmark's stricter per-task rule (rule B) fails on those three tasks.&lt;/p&gt;
&lt;h3 id=&quot;held-out-tasks&quot;&gt;Held-out tasks&lt;/h3&gt;
&lt;p&gt;Six further tasks were written by an author, a Sonnet agent, who had seen no code of this repository. The person running the measurement had not read them before it.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;arm &lt;/th&gt;&lt;th&gt;total of 30 &lt;/th&gt;&lt;th&gt;95% interval&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;unslop &lt;/td&gt;&lt;td&gt;30.00 &lt;/td&gt;&lt;td&gt;[30.00, 30.00]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;avoid-ai-writing &lt;/td&gt;&lt;td&gt;29.67 &lt;/td&gt;&lt;td&gt;[29.00, 30.00]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;this linter &lt;/td&gt;&lt;td&gt;29.67 &lt;/td&gt;&lt;td&gt;[29.00, 30.00]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;unslop, called by name &lt;/td&gt;&lt;td&gt;29.67 &lt;/td&gt;&lt;td&gt;[29.00, 30.00]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;no plugin &lt;/td&gt;&lt;td&gt;28.67 &lt;/td&gt;&lt;td&gt;[28.00, 29.00]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Humanizer &lt;/td&gt;&lt;td&gt;28.67 &lt;/td&gt;&lt;td&gt;[28.00, 29.00]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Humanizer, called by name &lt;/td&gt;&lt;td&gt;28.00 &lt;/td&gt;&lt;td&gt;[27.00, 29.00]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;slopless &lt;/td&gt;&lt;td&gt;28.00 &lt;/td&gt;&lt;td&gt;[28.00, 28.00]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;everwrite, called by name &lt;/td&gt;&lt;td&gt;27.33 &lt;/td&gt;&lt;td&gt;[26.33, 28.33]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;slop-cop &lt;/td&gt;&lt;td&gt;27.00 &lt;/td&gt;&lt;td&gt;[25.67, 28.33]&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;everwrite &lt;/td&gt;&lt;td&gt;26.67 &lt;/td&gt;&lt;td&gt;[26.00, 27.00]&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Here the linter is 0.33 behind unslop, with an interval of [-1.00, 0.00], so the claim that it is first does not hold on this set. It lost that 0.33 on one task, ho1, where it scored 4.67 against 5.00. Four arms sit at 29.67 or higher and even the no-plugin baseline reaches 28.67, so these tasks cannot separate the top arms. Across both sets: first on the public set, and within 0.33 of the best of the 11 arms on the held-out set.&lt;/p&gt;
&lt;h3 id=&quot;other-models-1&quot;&gt;Other models&lt;/h3&gt;
&lt;p&gt;I also ran Haiku 5.5 and Opus 5.5 once per task, with only the linter, the best other arm of each set and the no-plugin baseline. These totals are descriptive. With one run per task, a difference of one or two points is inside the run-to-run variation. These runs were not blind-graded, so the public scores use the scripted criteria only and their maximum is 57.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;model &lt;/th&gt;&lt;th&gt;set &lt;/th&gt;&lt;th&gt;this linter &lt;/th&gt;&lt;th&gt;best other arm &lt;/th&gt;&lt;th&gt;no plugin&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Haiku 5.5 &lt;/td&gt;&lt;td&gt;public, of 57 &lt;/td&gt;&lt;td&gt;54 &lt;/td&gt;&lt;td&gt;52 (Humanizer, called by name) &lt;/td&gt;&lt;td&gt;43&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Haiku 5.5 &lt;/td&gt;&lt;td&gt;held-out, of 30 &lt;/td&gt;&lt;td&gt;28 &lt;/td&gt;&lt;td&gt;28 (unslop) &lt;/td&gt;&lt;td&gt;26&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Opus 5.5 &lt;/td&gt;&lt;td&gt;public, of 57 &lt;/td&gt;&lt;td&gt;54 &lt;/td&gt;&lt;td&gt;54 (Humanizer, called by name) &lt;/td&gt;&lt;td&gt;45&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Opus 5.5 &lt;/td&gt;&lt;td&gt;held-out, of 30 &lt;/td&gt;&lt;td&gt;29 &lt;/td&gt;&lt;td&gt;27 (unslop) &lt;/td&gt;&lt;td&gt;25&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;what-a-prompt-cannot-do&quot;&gt;What a prompt cannot do&lt;/h3&gt;
&lt;p&gt;The linter is a command line tool that calls no model and has no dependencies. Its rules are deterministic: the same text gives the same findings. A rewrite can pass every check and still lose a fact, so the tool also has &lt;code&gt;--compare&lt;/code&gt;. It lists each number, version, date, link, quotation, code span and name that appears in one text and not the other, and exits 1 when it finds one. It also reports a qualifier that went missing next to a number, and first-person or claim words that the rewrite added.&lt;/p&gt;
&lt;p&gt;This is real output on a two-line example I wrote for this post:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ npx ai-slop-linter --compare before.md after.md
facts: 1 kept, 4 lost, 0 added
lost   date     3 March 2026
lost   number   1200
lost   qualifier up to 40
lost   version  2.4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The plugin's skill runs this after each rewrite and treats a failing comparison as blocking. &lt;code&gt;--compare&lt;/code&gt; compares facts and not wording, so it cannot say whether a rewrite is good.&lt;/p&gt;
&lt;h3 id=&quot;what-it-costs&quot;&gt;What it costs&lt;/h3&gt;
&lt;p&gt;This arm cost 2.43 times the no-plugin baseline per cell at list price, on both sets, because it takes more turns and more tokens. In the public set that was 0.125 US dollars per cell against 0.051. The runs were charged to a subscription, and the whole measurement would cost about 91 US dollars at list price.&lt;/p&gt;
&lt;h3 id=&quot;what-went-wrong-in-the-measurement&quot;&gt;What went wrong in the measurement&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The session limit stopped the runs twice. 236 public and 160 held-out attempts failed with &lt;code&gt;usage_limit_reached&lt;/code&gt;, were kept and never scored, and the same commands ran them again.&lt;/li&gt;
&lt;li&gt;The public and held-out runs went on at the same time. The held-out runs used a later commit, because the harness refuses a working tree whose benchmark folder differs from the commit. The product files are byte-identical between the two commits.&lt;/li&gt;
&lt;li&gt;The blind grader loop died once and was started again, so for a while two copies wrote vote files. All 165 vote files of the re-grade are valid, and none was dropped by hand.&lt;/li&gt;
&lt;li&gt;A re-grade of 33 sampled blind runs agreed with the first grading on 99.4 percent of criteria. Of 60 sampled cells run a second time, 38 of 40 public and 19 of 20 held-out re-runs fell inside the range of the original three runs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;try-it-and-check-it-1&quot;&gt;Try it and check it&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npx ai-slop-linter README.md
npx ai-slop-linter --compare before.md after.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To install it as a Claude Code plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/plugin marketplace add Bubblegunn/ai-slop-linter
/plugin install ai-slop-linter@ai-slop-linter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every cell, the departures from the protocol and a recompute step are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Bubblegunn/ai-slop-linter/tree/main/bench/head-to-head/results/round8-final-2026-10-23&quot;&gt;the round 8 evidence folder&lt;/a&gt;. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Bubblegunn/ai-slop-linter/blob/main/bench/head-to-head/results/round8-final-2026-10-23/REPRODUCE.md&quot;&gt;REPRODUCE.md&lt;/a&gt; in that folder gives the commands for a re-run, which needs Claude Code 2.1.308, Node 24 and about 9 hours. The tasks, criteria and harness are public in &lt;code&gt;bench/head-to-head/&lt;/code&gt;, and the earlier rounds and rulings are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Bubblegunn/ai-slop-linter/blob/main/docs/HEAD-TO-HEAD.md&quot;&gt;docs/HEAD-TO-HEAD.md&lt;/a&gt;. The repository is &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Bubblegunn/ai-slop-linter&quot;&gt;Bubblegunn/ai-slop-linter&lt;/a&gt;, and version 0.2.0 is on npm.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32260/humanizer-has-about-60-000-stars-on-12-public-tasks-my-linter-plugin-scored-2-67-higher</guid>
<pubDate>Sat, 24 Oct 2026 10:06:25 +0000</pubDate>
</item>
<item>
<title>Developer Weekly Briefing — October 23, 2026</title>
<link>https://coderlegion.com/32190/developer-weekly-briefing-october-23-2026</link>
<description>&lt;p&gt;Three threads defined this week. Security researchers — now routinely assisted by AI — found critical bugs faster than maintainers could coordinate patches. Agentic AI standards solidified in meaningful ways, with the Agentic AI Foundation adding four new projects and a candid CTO willing to name the gaps. And on the infrastructure side, the tools for keeping developer pipelines safe from compromised packages got sharper. Here's what mattered.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;php2shell-ai-turned-a-theoretical-bug-into-an-exploit-in-days&quot;&gt;PHP2Shell: AI turned a theoretical bug into an exploit in days&lt;/h3&gt;
&lt;p&gt;PHP shipped emergency patches on October 21 for PHP2Shell — two critical flaws, both rated 9.2 under CVSS v4.0, that let an unauthenticated attacker run code on your server. The fixes are in PHP 8.5.12, 8.4.27, 8.3.36, and 8.2.35. If you run a supported branch, the path is simple: patch and restart your PHP workers.&lt;/p&gt;
&lt;p&gt;The harder story is what's underneath. These flaws trace back to the PHP 5 series — CVE-2026-109618 corrupts memory during out-of-memory errors, CVE-2026-109619 involves stale global state from freed request memory. Security researchers had spotted the vectors more than a year ago but couldn't find a practical way to trigger them. They stayed in the &quot;theoretical&quot; pile. Then two independent researchers, each using a frontier AI model, found working exploits within days of each other this past August. Cloudflare logged attacks hours before patches shipped on October 21.&lt;/p&gt;
&lt;p&gt;Matthew Weier O'Phinney, PHP Foundation board member and principal product manager at Perforce OpenLogic and Zend, was direct: &quot;These vulnerabilities affect every single Server API provided by the language, because their roots are deep within how the engine manages memory.&quot; That includes PHP-FPM behind Nginx — not just mod_php. Workarounds like disabling session upload progress buy time; they don't fix the underlying problem.&lt;/p&gt;
&lt;p&gt;The real exposure is PHP past community support. Every version before 8.2 is EOL, and 8.2 itself ends community support at year's end. If you're on an older version, your options are a commercial LTS provider — several shipped patches for PHP 7.2–8.1 in coordination with the PHP Security team — or a migration to a supported branch. His broader point: AI is accelerating discovery faster than patches reach unsupported software. That gap is a business risk, not just an engineering footnote.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/31263/php2shell-two-9-2-rated-flaws-reach-back-to-php-5-heres-the-plan-for-eol-teams&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;aaif-now-has-10-projects-its-cto-on-what-to-adopt-and-where-security-falls-short&quot;&gt;AAIF now has 10 projects — its CTO on what to adopt and where security falls short&lt;/h3&gt;
&lt;p&gt;The Agentic AI Foundation started December 2025 with three projects. It hit ten this week at AGNTCon + MCPCon North America in San Jose, adding contributions from Anthropic (Agent Skills), OpenAI (Open Responses), Hugging Face (OpenEnv), and Alibaba (OpenSandbox). The foundation passed 300 member organizations.&lt;/p&gt;
&lt;p&gt;I asked Manik Surtani, AAIF's CTO and co-founder, what that means for teams building on these standards. His answers were practical.&lt;/p&gt;
&lt;p&gt;On what to use now: MCP and A2A are the most mature. Open Responses has reached solid maturity. OpenSandbox is early but worth watching — and you need a sandbox somewhere. &quot;Running without any sandbox is dangerous, and you shouldn't do it.&quot; Put a gateway in the middle so you can migrate models and tools later.&lt;/p&gt;
&lt;p&gt;On the reference stack question everyone keeps asking: don't expect one. &quot;The more I think about it, the more I believe any reference stack we build today will be out of date tomorrow.&quot; Production deployments are the reference.&lt;/p&gt;
&lt;p&gt;On the two biggest security gaps in the agentic stack today: agent identity and authentication, and attestation — proof that an agent did what you asked and didn't go sideways. No AAIF project covers either one yet. There are working groups, but no adopted standards. If your agents operate on real systems, those are the areas where your own controls need to be strongest.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/31327/aaif-hits-10-projects-its-cto-on-what-to-adopt-now-and-the-security-gaps-left-open&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;github-puts-a-firewall-between-npm-and-your-ci-runners&quot;&gt;GitHub puts a firewall between npm and your CI runners&lt;/h3&gt;
&lt;p&gt;Install time is where npm attackers have focused all year. Preinstall and postinstall scripts can run before tests or scanners get a look — the compromised Axios release in March, the @redhat-cloud-services attack in June, and a worm that touched more than 400 packages in August all exploited that window. GitHub's answer is a package firewall, now in public preview for Enterprise Cloud customers.&lt;/p&gt;
&lt;p&gt;The firewall sits between endpoints — CI runners, AI coding agents, developer workstations — and the upstream npm registry. Five policy controls ship out of the box: blocking known malware from the GitHub Advisory Database, blocking versions with known vulnerabilities above a configurable severity, blocking attestation downgrades (a package that stops shipping provenance after earlier versions carried it), blocking newly introduced install scripts, and a minimum package age cooldown from 1 to 365 days, defaulting to one day.&lt;/p&gt;
&lt;p&gt;The cooldown is the simplest and may be the most useful. Most malicious npm versions are pulled within hours of discovery. A one-day hold would have stopped the compromised Axios versions, which were live for about three hours. The attestation check has a known gap: it won't catch a hijacked pipeline that signs malicious code honestly, as happened in the AsyncAPI attack in July.&lt;/p&gt;
&lt;p&gt;Mitch Ashley of The Futurum Group noted the core constraint: &quot;The firewall only protects traffic routed through it, so teams should block direct access to the public registry at the network layer.&quot; Watch the metered pricing at general availability too — a usage-based bill gives teams a reason to route less through the firewall.&lt;/p&gt;
&lt;p&gt;For Enterprise Cloud customers, the free preview is worth running. Find out what a one-day cooldown would actually catch in your environment before you have to find out the hard way.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devops.com/github-puts-a-firewall-between-npm-and-your-ci-runners/&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;anthropic-used-claude-to-formally-verify-123-open-source-libraries-and-found-bugs-fuzzing-had-missed&quot;&gt;Anthropic used Claude to formally verify 123 open source libraries — and found bugs fuzzing had missed&lt;/h3&gt;
&lt;p&gt;Formal verification has always been expensive. Two person-years to build the seL4 microkernel. Twenty person-years to verify it. That math is why it has mostly lived in avionics and cryptography. Anthropic's Frontier Red Team, working with formal methods company Theorem, ran the largest AI-assisted formal verification effort yet: 123 open source libraries from Google's OSS-Fuzz project, 239 fuzzing entry points, proofs operating on RISC-V assembly, not source code.&lt;/p&gt;
&lt;p&gt;The approach was clever. Instead of writing new specs from scratch — historically the hardest part — the team converted existing fuzz tests into formal specs. Each fuzz entry point became a proof statement: this program never crashes on any input under 64 MB. Theorem supplied formal models of RISC-V and Linux process behavior. Claude wrote the proofs. The Rocq proof checker validated them.&lt;/p&gt;
&lt;p&gt;Over a few weeks, Claude produced about 180 million lines of Rocq proof. Typical proofs ran around 600,000 lines for a binary with a median of 18,000 instructions. Claude was allowed to patch bugs it hit and keep going, and it found hundreds. Several are notable: a counter underflow in a decompressor that only triggered on inputs around 20 MB with one specific layout; an out-of-bounds read in an image decoder needing a 64 MB input and more than 2 GB of memory; a logic bug in file-loading restrictions that wasn't always enforced. None of these would have surfaced from typical fuzzing, which usually caps inputs at 1 MB.&lt;/p&gt;
&lt;p&gt;Cost is real — proofs can consume hundreds of millions of tokens — but Anthropic points to two trends: Claude Opus 5.5 used 40% fewer tokens than Opus 5 on 36 small verification tasks, and agents sharing a workspace and reusing proofs needed 17% fewer tokens. The reuse point matters more than token savings. Much of any proof covers code every C program shares, so proofs transfer well. Verify a small set of widely used libraries once, and much of the stack downstream gets easier.&lt;/p&gt;
&lt;p&gt;Anthropic has released a subset of proofs and the RISC-V and OS semantics on GitHub. Theorem is offering to run the pipeline for other open source projects and has published a Claude skill for teams that want to start. The near-term takeaway for DevOps teams: don't drop fuzzing. Do start watching whether formal verification moves into your pipeline the way fuzzing did — Anthropic predicts it will become roughly as practical as fuzzing is today.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devops.com/claude-writes-180-million-lines-of-proofs-to-verify-123-open-source-libraries/&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;also-from-this-week&quot;&gt;Also from this week&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitHub adds AI-powered security scanning to pull requests.&lt;/strong&gt; GitHub's new code scanning feature runs directly on PRs, flagging security issues before merge rather than after. It uses AI to surface findings in context with the diff. Worth watching if your team uses GitHub Enterprise. &lt;a rel=&quot;nofollow&quot; href=&quot;https://devops.com/github-adds-ai-powered-security-scanning-to-pull-requests/&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI agents can now decompile software — here's what that means for security.&lt;/strong&gt; Researchers demonstrated that frontier AI models can decompile binaries and reconstruct meaningful source-level understanding of proprietary software. The implications run in both directions: useful for security audits, dangerous in the wrong hands. &lt;a rel=&quot;nofollow&quot; href=&quot;https://devops.com/ai-agents-can-now-decompile-software-heres-what-that-means-for-security/&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;HiddenLayer puts a security layer between external LLMs and your internal systems.&lt;/strong&gt; LLM gateway hijacking — where an attacker manipulates prompts to redirect agent calls to a malicious model — is an emerging attack class. HiddenLayer's approach sits in the request path between your application and external model APIs, inspecting and filtering what goes through. &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/31354/hiddenlayer-puts-a-security-layer-between-external-llms-and-your-internal-systems&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CyberCartel used Claude and Grok to automate banking attacks.&lt;/strong&gt; A threat actor group documented using frontier AI models to automate credential-harvesting campaigns targeting banking customers. The research maps how they used AI to generate phishing lures, adapt to defenses, and scale operations that would previously have required more manual effort. &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/31298/cybercartel-uses-claude-and-grok-to-automate-banking-attacks-heres-how-far-they-got&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Microsoft Foundry agents can now run on a schedule.&lt;/strong&gt; Scheduled execution is a basic building block for autonomous agents — run a report, check a system, trigger a workflow at a set time without a human in the loop. Microsoft Foundry now supports it natively. &lt;a rel=&quot;nofollow&quot; href=&quot;https://techstrong.ai/articles/microsoft-foundry-agents-now-run-on-a-schedule/&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Google launches Harness² for AI agent testing.&lt;/strong&gt; Google's new testing framework is designed for multi-step agent workflows, where individual unit tests don't capture enough of the failure surface. It's aimed at teams building production agents who need confidence that the full chain — not just each step — behaves as expected. &lt;a rel=&quot;nofollow&quot; href=&quot;https://techstrong.ai/articles/google-launches-harness-squared-for-ai-agent-testing/&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenAI lands a COBOL modernization deal with a Fortune 500 bank.&lt;/strong&gt; Enterprise AI is starting to move into legacy modernization, one of the largest untapped engineering markets. OpenAI will use its models to translate COBOL systems for a major bank — a deal that signals how AI-assisted migration may finally make the COBOL mountain climbable. &lt;a rel=&quot;nofollow&quot; href=&quot;https://devops.com/openai-lands-cobol-modernization-deal-with-fortune-500-bank/&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mod42 is building data centers for organizations that can't afford hyperscale.&lt;/strong&gt; The hyperscale economics of AI infrastructure don't reach everyone. Mod42's approach targets mid-market enterprise and public-sector workloads that need dedicated capacity without signing a hyperscaler contract. &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/31187/mod42-is-building-data-centers-for-a-world-that-cannot-afford-hyperscale&quot;&gt;Read more&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;developer-spotlight-this-week&quot;&gt;Developer Spotlight this week&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Dean Hall&lt;/strong&gt; is best known for creating DayZ, one of the most influential survival games of the past decade. He's back with a new project, Brutal, and he rebuilt his game engine from scratch to make it. The conversation covers what drove that decision, where AI fits in his development process, and where it doesn't. His take on what AI still can't do is worth hearing from someone who has shipped at scale.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/31142/dean-hall-on-brutal-why-he-rebuilt-his-game-engine-from-scratch-and-what-ai-cant-do&quot;&gt;Watch the conversation&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The through-line this week: AI is accelerating both sides of the security equation. PHP2Shell shows how quickly a theoretical bug becomes a working exploit once researchers have capable models to help. The CyberCartel and HiddenLayer stories show the same dynamic at the application layer. And Anthropic's formal verification work shows that AI can also scale up the defenses — producing more proof and catching more bugs than human teams could afford to write by hand. The tools are neutral. The race is real.&lt;/p&gt;
&lt;p&gt;See you next Friday.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Developer Weekly Briefing is published every Friday on &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com&quot;&gt;Coder Legion&lt;/a&gt;. Written by &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/user/Tom+Smith&quot;&gt;Tom Smith&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32190/developer-weekly-briefing-october-23-2026</guid>
<pubDate>Fri, 23 Oct 2026 12:49:47 +0000</pubDate>
</item>
<item>
<title>is-kit Reached 100 Stars ⭐🎉 Here’s What Changed Since 50</title>
<link>https://coderlegion.com/32180/is-kit-reached-100-stars-heres-what-changed-since-50</link>
<description>&lt;p&gt;Hoi hoi!&lt;/p&gt;
&lt;p&gt;YAY! &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru/is-kit&quot;&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/a&gt; just reached &lt;strong&gt;100 GitHub stars⭐&lt;/strong&gt; 🎉🎉🎉&lt;/p&gt;
&lt;p&gt;When it reached 50 stars, I wrote about how we use it in a production TypeScript application serving more than 100,000 users. 👇&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/nyaomaru/is-kit-reached-50-stars-heres-how-we-use-it-in-production-2i5b&quot;&gt;https://dev.to/nyaomaru/is-kit-reached-50-stars-heres-how-we-use-it-in-production-2i5b&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I could write the same story again. But that would be boring. 😹&lt;/p&gt;
&lt;p&gt;A lot has happened since then.&lt;/p&gt;
&lt;p&gt;The library gained &lt;strong&gt;new APIs&lt;/strong&gt;, some existing APIs &lt;strong&gt;became stricter&lt;/strong&gt;, I started &lt;strong&gt;using it in more of my own OSS projects&lt;/strong&gt;, and &lt;strong&gt;one real-world use case even exposed a type inference edge case in&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Effect-TS/effect&quot;&gt;&lt;code&gt;Effect&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So this time, instead of explaining the production adoption again, I want to look at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What changed between 50 and 100 stars&lt;/li&gt;
&lt;li&gt;Some of the most important APIs added since then&lt;/li&gt;
&lt;li&gt;How I am actually using &lt;code&gt;is-kit&lt;/code&gt; outside the original production application&lt;/li&gt;
&lt;li&gt;A real bug report involving &lt;code&gt;Effect.catchIf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;What 100 stars means to me now&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let's dive in! 🔧&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/daotzurpsdocaj5u57pf.png&quot; alt=&quot;is-kit 100 stars&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-first-what-did-50-stars-mean-&quot;&gt;⭐ First, What Did 50 Stars Mean?&lt;/h3&gt;
&lt;p&gt;At 50 stars, the biggest thing I wanted to show was that &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru/is-kit&quot;&gt;&lt;code&gt;is-kit&lt;/code&gt;&lt;/a&gt; was not only a toy project.&lt;/p&gt;
&lt;p&gt;It was already being used in a real production application. 🚀&lt;/p&gt;
&lt;p&gt;The application had repeated runtime checks like&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;typeof value === &quot;string&quot;;
typeof value === &quot;number&quot;;
value === null || value === undefined;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and several larger user-defined type guards for things like HTTP errors, literal unions, arrays, API values, and plain objects.&lt;/p&gt;
&lt;p&gt;The interesting part was &lt;strong&gt;not&lt;/strong&gt; that &lt;code&gt;is-kit&lt;/code&gt; made those checks shorter.&lt;br&gt;
The interesting part was that it gave them one &lt;strong&gt;reusable shape&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Small guards could be composed.&lt;/p&gt;
&lt;p&gt;Application-owned helpers could sit between the library and feature code. And the result could still participate in normal TypeScript control flow.&lt;/p&gt;
&lt;p&gt;That part has not changed.&lt;/p&gt;
&lt;p&gt;But since 50 stars, the library itself has moved quite a bit. 👀&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-property-refinement-became-a-first-class-use-case&quot;&gt;🔑 Property Refinement Became a First-Class Use Case&lt;/h3&gt;
&lt;p&gt;One thing I kept running into was this.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I already have a good type guard.&lt;br&gt;
I just want to apply it to one property.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;declare const input: unknown;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Maybe I already know how to validate a string&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { isString } from &quot;is-kit&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But now I want to express&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;This object has a &lt;code&gt;name&lt;/code&gt; property, and that property is a string.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That sounds simple, but there are actually a few different cases.&lt;/p&gt;
&lt;p&gt;A required property is &lt;strong&gt;not the same as an optional property&lt;/strong&gt;.&lt;br&gt;
An array index is &lt;strong&gt;not the same as an object property&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And an optional property that becomes known to be defined should &lt;strong&gt;narrow differently from one that is still optional&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;is-kit&lt;/code&gt; added three property refinement helpers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;refineKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refineDefinedKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refineIndex&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The idea is simple&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Take a type guard you already trust and lift it onto a property or array element.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { isString, refineKey } from &quot;is-kit&quot;;

const hasStringName = refineKey(&quot;name&quot;, isString);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the string guard is reusable at a deeper level instead of being rewritten inside another custom predicate.&lt;/p&gt;
&lt;p&gt;This is a small API, but I like it because it follows the same design goal as the rest of &lt;code&gt;is-kit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Do not build one giant validator.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build small checks that can be reused in normal TypeScript code&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-then-discriminatedunion-got-much-more-serious&quot;&gt;🔍 Then &lt;code&gt;discriminatedUnion&lt;/code&gt; Got Much More Serious&lt;/h3&gt;
&lt;p&gt;The biggest addition since the 50-star article is probably the exhaustive discriminated union API introduced in &lt;code&gt;v1.15.0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Consider a common TypeScript type&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type Result = { ok: true; value: string } | { ok: false; error: string };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At runtime, validating this is not difficult.&lt;/p&gt;
&lt;p&gt;You can check &lt;code&gt;ok&lt;/code&gt;, then validate the corresponding fields.&lt;/p&gt;
&lt;p&gt;But there is another question&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Can TypeScript verify that the validator itself covers the whole union?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is where &lt;code&gt;discriminatedUnion&lt;/code&gt; comes in.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { discriminatedUnion, isString, oneOfValues, typedStruct } from &quot;is-kit&quot;;

type Result = { ok: true; value: string } | { ok: false; error: string };

const resultUnion = discriminatedUnion&amp;lt;Result&amp;gt;();

export const isResult = resultUnion(&quot;ok&quot;, [true, false], {
  true: typedStruct&amp;lt;Extract&amp;lt;Result, { ok: true }&amp;gt;&amp;gt;()({
    ok: oneOfValues(true),
    value: isString,
  }),
  false: typedStruct&amp;lt;Extract&amp;lt;Result, { ok: false }&amp;gt;&amp;gt;()({
    ok: oneOfValues(false),
    error: isString,
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The runtime behavior is useful.&lt;/p&gt;
&lt;p&gt;But the more interesting part is the compile-time contract.&lt;/p&gt;
&lt;p&gt;The builder checks things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Missing discriminant branches&lt;/li&gt;
&lt;li&gt;Extra branches&lt;/li&gt;
&lt;li&gt;Incompatible branch guards&lt;/li&gt;
&lt;li&gt;Incomplete discriminant tuples&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So if the union changes, the guard cannot silently stay incomplete.&lt;/p&gt;
&lt;p&gt;That matters because one of the easiest ways for a hand-written type guard to become dangerous is drift.&lt;/p&gt;
&lt;p&gt;The TypeScript type evolves.&lt;br&gt;
The runtime guard does not.&lt;br&gt;
And the compiler cannot normally tell you that the two are no longer aligned.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;discriminatedUnion&lt;/code&gt; is one attempt to make that class of mistake harder.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-the-library-also-became-stricter&quot;&gt;💪 The Library Also Became Stricter&lt;/h3&gt;
&lt;p&gt;Not every improvement between 50 and 100 stars was a new feature.&lt;/p&gt;
&lt;p&gt;A lot of work was about making existing behavior more precise.&lt;/p&gt;
&lt;p&gt;For example, recent releases included fixes around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;recordOf&lt;/code&gt; literal-key narrowing&lt;/li&gt;
&lt;li&gt;native &lt;code&gt;Error&lt;/code&gt; brand checks&lt;/li&gt;
&lt;li&gt;array guards with overridden iterators&lt;/li&gt;
&lt;li&gt;exact &lt;code&gt;struct&lt;/code&gt; validation&lt;/li&gt;
&lt;li&gt;symbol-keyed struct schemas&lt;/li&gt;
&lt;li&gt;cross-realm struct schemas&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There was also more package-level testing.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; now checks published-package behavior from both ESM and CommonJS TypeScript consumers, and also has &lt;code&gt;Deno&lt;/code&gt; / &lt;code&gt;JSR&lt;/code&gt; smoke coverage.&lt;/p&gt;
&lt;p&gt;This part is less exciting to show in a code snippet.&lt;/p&gt;
&lt;p&gt;But for a small library, I think it is important.&lt;/p&gt;
&lt;p&gt;Making sure they behave correctly in weird JavaScript and TypeScript environments is where things become real.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-i-started-using-is-kit-in-other-oss-too&quot;&gt;🤖 I Started Using &lt;code&gt;is-kit&lt;/code&gt; in Other OSS Too&lt;/h3&gt;
&lt;p&gt;Since reaching 50 stars, I've also started using &lt;code&gt;is-kit&lt;/code&gt; more naturally in other OSS projects I maintain.&lt;/p&gt;
&lt;p&gt;For example, in my changelog tooling of &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru/changelog-bot&quot;&gt;&lt;code&gt;changelog-bot&lt;/code&gt;&lt;/a&gt;, I use &lt;code&gt;define&lt;/code&gt; to keep small predicates reusable and composable instead of turning every runtime check into an isolated helper.&lt;/p&gt;
&lt;p&gt;A simplified example looks like this 👇&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { define, oneOfValues } from &quot;is-kit&quot;;

const CHANGELOG_SECTIONS = [&quot;Added&quot;, &quot;Changed&quot;, &quot;Fixed&quot;, &quot;Removed&quot;] as const;

const isChangelogSection = define&amp;lt;(typeof CHANGELOG_SECTIONS)[number]&amp;gt;(
  oneOfValues(CHANGELOG_SECTIONS),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact code is not the important part.&lt;/p&gt;
&lt;p&gt;What changed for me is that I no longer think&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;I should use my library here.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I just reach for the guard I need.&lt;/p&gt;
&lt;p&gt;A library starts becoming useful to its maintainer when it stops feeling like a demo of itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-then-an-is-kit-guard-ran-into-an-effect-type-bug&quot;&gt;🚀 Then an &lt;code&gt;is-kit&lt;/code&gt; Guard Ran Into an Effect Type Bug&lt;/h3&gt;
&lt;p&gt;This was one of the most interesting things that happened recently.&lt;/p&gt;
&lt;p&gt;An &lt;code&gt;is-kit&lt;/code&gt; user was using a type guard with Effect's &lt;code&gt;catchIf&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Conceptually, the refinement looked like this 👇&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;(error: unknown): error is SomeError =&amp;gt; ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is completely normal for &lt;code&gt;is-kit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In fact, &lt;code&gt;unknown&lt;/code&gt; is exactly where the library is designed to start.&lt;/p&gt;
&lt;p&gt;But with &lt;code&gt;Effect.catchIf&lt;/code&gt;, that refinement could affect generic inference in an unexpected way.&lt;/p&gt;
&lt;p&gt;The error channel could become widened to &lt;code&gt;unknown&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The reported case resulted in this Effect PR 👏 And merged 🚀&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Effect-TS/effect/pull/9252&quot;&gt;https://github.com/Effect-TS/effect/pull/9252&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The relevant type originally looked roughly like this&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;refinement: Predicate.Refinement&amp;lt;E, EB&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and the fix changed it to&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;refinement: Predicate.Refinement&amp;lt;NoInfer&amp;lt;E&amp;gt;, EB&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important idea is that the &lt;strong&gt;refinement's input type should not be allowed to redefine the error type already established by the &lt;code&gt;Effect&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The PR also adds type tests for cases like&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const result = Effect.catchIf(
  mixedEffect,
  (e: unknown): e is AiError =&amp;gt; e instanceof AiError,
  () =&amp;gt; Effect.succeed(&quot;ok&quot;),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the fix, the refinement input could participate in inference and widen the error channel.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;NoInfer&lt;/code&gt;, the original error type remains the source of truth.&lt;/p&gt;
&lt;p&gt;The same pattern was fixed for &lt;code&gt;Channel&lt;/code&gt; and &lt;code&gt;Stream&lt;/code&gt; too.&lt;/p&gt;
&lt;p&gt;There was also a &lt;code&gt;Bluesky&lt;/code&gt; post about the issue&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bsky.app/profile/evadeva.bsky.social/post/3mxwiqfeo3s2j&quot;&gt;https://bsky.app/profile/evadeva.bsky.social/post/3mxwiqfeo3s2j&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I really liked this bug because &lt;strong&gt;it came from an interaction I would probably never have discovered by testing &lt;code&gt;is-kit&lt;/code&gt; alone&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;is-kit&lt;/code&gt; did not &quot;break Effect&quot;. And &lt;code&gt;Effect&lt;/code&gt; was not doing something absurd either.&lt;/p&gt;
&lt;p&gt;It was a subtle TypeScript inference interaction between a generic API and a refinement whose input was intentionally broad.&lt;/p&gt;
&lt;p&gt;That is exactly the kind of edge case that appears when libraries start meeting each other in real code.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-this-is-the-kind-of-feedback-i-wanted&quot;&gt;👍 This Is the Kind of Feedback I Wanted&lt;/h3&gt;
&lt;p&gt;When &lt;code&gt;is-kit&lt;/code&gt; reached 50 stars, I wrote&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Someone found this idea useful.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That was already enough to make me happy.&lt;/p&gt;
&lt;p&gt;100 stars still does not mean the library is huge.&lt;/p&gt;
&lt;p&gt;A star is still not a benchmark.&lt;br&gt;
It is still not proof of correctness.&lt;br&gt;
And it definitely does not mean every TypeScript project should install &lt;code&gt;is-kit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But something has changed.&lt;/p&gt;
&lt;p&gt;People are not only starring the repository anymore.&lt;/p&gt;
&lt;p&gt;They are&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using the library&lt;/li&gt;
&lt;li&gt;Asking questions&lt;/li&gt;
&lt;li&gt;Finding edge cases&lt;/li&gt;
&lt;li&gt;Reporting bugs&lt;/li&gt;
&lt;li&gt;Comparing it with other validation approaches&lt;/li&gt;
&lt;li&gt;Using its guards with other TypeScript libraries&lt;/li&gt;
&lt;li&gt;Making me reconsider APIs I thought were already finished&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is much more valuable than the number itself. 💖&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-the-project-is-also-becoming-easier-to-explain&quot;&gt;💬 The Project Is Also Becoming Easier to Explain&lt;/h3&gt;
&lt;p&gt;Another thing changed between 50 and 100 stars.&lt;/p&gt;
&lt;p&gt;I became much clearer about what &lt;code&gt;is-kit&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; trying to replace every schema library.&lt;br&gt;
It is &lt;strong&gt;not&lt;/strong&gt; trying to turn every runtime value into a schema-first workflow.&lt;br&gt;
And it is &lt;strong&gt;not&lt;/strong&gt; trying to hide normal TypeScript control flow.&lt;/p&gt;
&lt;p&gt;The core question is still very small&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;Can this &lt;code&gt;unknown&lt;/code&gt; value safely enter this branch?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sometimes a schema library is the right answer.&lt;/p&gt;
&lt;p&gt;For example, if I need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rich validation errors&lt;/li&gt;
&lt;li&gt;Transformations&lt;/li&gt;
&lt;li&gt;Form validation&lt;/li&gt;
&lt;li&gt;A contract shared across systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I may want &lt;code&gt;Zod&lt;/code&gt;, &lt;code&gt;Valibot&lt;/code&gt;, &lt;code&gt;ArkType&lt;/code&gt;, or something else.&lt;/p&gt;
&lt;p&gt;But sometimes I already have normal TypeScript code and I only need a reusable narrowing function.&lt;/p&gt;
&lt;p&gt;That is where I want &lt;code&gt;is-kit&lt;/code&gt; to be useful.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;if (isUser(value)) {
  value.id;
}

if (isHttpClientError(error)) {
  // Handle HTTP client errors
}

items.filter(isString);

result.catch(isHttpClientError);

const isMaybeNumber = nullish(isNumber);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Small runtime decisions.&lt;br&gt;
Normal functions.&lt;br&gt;
Normal TypeScript.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-what-100-stars-means-to-me&quot;&gt;🐱 What 100 Stars Means to Me&lt;/h3&gt;
&lt;p&gt;100 stars is still small compared with the largest TypeScript OSS projects.&lt;/p&gt;
&lt;p&gt;No problem.&lt;/p&gt;
&lt;p&gt;What matters to me is that the project is becoming more real in ways that have nothing to do with the star counter.&lt;/p&gt;
&lt;p&gt;Since the 50-star milestone:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New APIs were added&lt;/li&gt;
&lt;li&gt;Existing APIs became stricter&lt;/li&gt;
&lt;li&gt;Runtime edge cases were fixed&lt;/li&gt;
&lt;li&gt;Package compatibility testing improved&lt;/li&gt;
&lt;li&gt;I started using &lt;code&gt;is-kit&lt;/code&gt; naturally in other OSS&lt;/li&gt;
&lt;li&gt;External usage produced feedback I could not generate alone&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;unknown&lt;/code&gt;-based refinement even helped expose an inference edge case in Effect&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last part is probably my favorite. &lt;/p&gt;
&lt;p&gt;Because the goal was never to build a library that only looks nice in its own &lt;code&gt;README&lt;/code&gt;!!!&lt;/p&gt;
&lt;p&gt;The goal is &lt;strong&gt;to make reusable runtime narrowing work naturally inside real TypeScript code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And now I am starting to see that happen outside my own repository too.&lt;/p&gt;
&lt;p&gt;Thank you to everyone who starred the project, tried it, reported something, asked a question, opened an issue, or simply read the code. 🙏&lt;/p&gt;
&lt;p&gt;There is still a lot I want to improve.&lt;/p&gt;
&lt;p&gt;But 100 feels pretty nice. 😸⭐&lt;/p&gt;
&lt;p&gt;If small composable type guards fit your TypeScript style, give it a try!&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/nyaomaru/is-kit&quot;&gt;https://github.com/nyaomaru/is-kit&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thank you for reading! 🤲&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32180/is-kit-reached-100-stars-heres-what-changed-since-50</guid>
<pubDate>Fri, 23 Oct 2026 12:24:38 +0000</pubDate>
</item>
<item>
<title>AAIF Hits 10 Projects. Its CTO on What to Adopt Now and the Security Gaps Left Open</title>
<link>https://coderlegion.com/32119/aaif-hits-10-projects-its-cto-on-what-to-adopt-now-and-the-security-gaps-left-open</link>
<description>&lt;p&gt;The Agentic AI Foundation (AAIF) launched in December 2025 with three projects: the Model Context Protocol (MCP), goose, and AGENTS.md. Less than a year later, it hosts 10.&lt;/p&gt;
&lt;p&gt;On October 22, at AGNTCon + MCPCon North America in San Jose, AAIF added four more. Anthropic contributed Agent Skills. OpenAI contributed Open Responses. Hugging Face contributed OpenEnv. Alibaba contributed OpenSandbox. The foundation also announced it has passed 300 member organizations.&lt;/p&gt;
&lt;p&gt;That's a lot of new pieces in a short time. So I asked Manik Surtani, AAIF's CTO and co-founder, what vendor-neutral governance means for developers who build on these projects. He worked with all four companies to bring the projects in.&lt;/p&gt;
&lt;p&gt;His answers are practical. Nothing changes in how you use MCP today. Don't wait for a reference stack. Pick a sandbox and a gateway now. And the two biggest security gaps in the agentic stack, agent identity and attestation, aren't covered by any AAIF project yet.&lt;/p&gt;
&lt;h3 id=&quot;the-four-new-projects-at-a-glance&quot;&gt;The four new projects at a glance&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Agent Skills (Anthropic):&lt;/strong&gt; An open format for packaging instructions, scripts, and resources so agents can load specialized know-how on demand, across products.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open Responses (OpenAI):&lt;/strong&gt; An open spec, based on the OpenAI Responses API, for describing LLM requests and outputs once and running them across model providers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenEnv (Hugging Face):&lt;/strong&gt; A framework and community hub for building and sharing reinforcement learning environments. Each environment runs as a containerized service that exposes MCP and the Gym training interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenSandbox (Alibaba):&lt;/strong&gt; A general-purpose sandbox platform for coding agents, GUI agents, evaluation, code execution, and RL training, with Docker and Kubernetes runtimes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They join A2A, Agent Router, agentgateway, AGENTS.md, goose, and MCP.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Answers have been edited for length and clarity.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;You worked with four companies to bring these projects in. What did neutral governance need to guarantee before they handed them over? What did each company give up, and what did it keep?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OpenAI and Anthropic helped bootstrap AAIF, so they already knew what good neutral governance looks like and what it means for them.&lt;/p&gt;
&lt;p&gt;Alibaba wanted to be the first non-US project in the foundation, and specifically the first Chinese one. I think that will open the door to more projects coming out of Asia, and I'm excited about that. They didn't need many guarantees either. They have a lot of projects in the CNCF and already see the value. AAIF is part of the Linux Foundation, a sister organization to the CNCF, so they already trusted us.&lt;/p&gt;
&lt;p&gt;Hugging Face is a similar story. The Linux Foundation brand is well known, and that trust made it relatively easy to get everyone on board. The common thread is that these companies already understood and valued vendor-neutral stewardship. The conversation was much shorter than people might expect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For a developer who has already built on MCP or Agent Skills, what actually changes now that the project sits at AAIF?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Day to day, nothing changes. Using these standards stays the same.&lt;/p&gt;
&lt;p&gt;What changes is how the projects evolve. Spec changes and improvements will happen with a lot more transparency, with much more of that work in the open. Each project will also have a more structured maintainer group. That gives others a real path to maintainership. The more you participate, contribute, and show commitment, the better your chance of becoming a maintainer and helping steer the project.&lt;/p&gt;
&lt;p&gt;So for existing users, the practical effect is stability today and a more open, more broadly governed process for how the specs develop tomorrow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open Responses is based on OpenAI's Responses API. How do you keep a spec neutral when it started as one vendor's API?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It comes down to the same mechanism. Competing model providers can now join the project as contributors and help shape the API.&lt;/p&gt;
&lt;p&gt;It's also already not just OpenAI's API. Open Responses has been an open source project for a while. More than one company has adopted it, and others are contributing to it. So neutrality isn't hypothetical. A competing provider's path to influence is the same as anyone else's: show up, contribute, build trust, and earn a seat in steering the direction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;These projects touch each other. How is AAIF making sure the 10 projects fit together? Is there a reference stack developers can follow?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;People always ask me about a reference stack, and I tend to push back. We started out thinking there should be one. But the more I think about it, the more I believe any reference stack we build today will be out of date tomorrow. The space is changing too fast. I don't want to use one as a roadmap to hunt for missing pieces.&lt;/p&gt;
&lt;p&gt;Right now, I think of it as a giant experiment. Everyone is assembling these pieces in different ways, and it's great to see what works and what doesn't. When people deploy something in production, that becomes the reference for that pattern. Other people assemble the same building blocks differently, which is just as interesting.&lt;/p&gt;
&lt;p&gt;This isn't cloud, a mature technology with a fairly static, well-understood reference stack. This space is still new and experimental.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If a developer starts a new agent project today, which AAIF projects should they adopt first? Which should they watch?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MCP and A2A are our two most mature projects. Open Responses is now very mature as well.&lt;/p&gt;
&lt;p&gt;OpenSandbox is still pretty nascent, and many sandboxing technologies with different approaches are coming out. It's worth watching. But you do need a sandbox. Pick one and start moving. Running without any sandbox is dangerous, and you shouldn't do it.&lt;/p&gt;
&lt;p&gt;Routers and gateways sit in the middle. They're an important part of the ecosystem, and the space will keep evolving, so grab one to get started.&lt;/p&gt;
&lt;p&gt;The good news is that switching is relatively easy in the agentic world. With agent-driven development, you can migrate from one tool to another without much hassle. In the old days of handcrafting everything, migration was far more expensive. So pick one, and make sure there's a gateway somewhere in between. That gives you the option to migrate models and other important pieces later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where are the biggest security gaps in the agentic stack right now? Which ones does no AAIF project cover yet?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The biggest gaps are around identity and authentication. There are nascent standards, but nothing we've adopted yet. There are plenty of commercial products for agent identity worth looking at. Still, I don't think anyone has cracked that nut. We have a working group going deep into the space, and we want to see what comes out of it.&lt;/p&gt;
&lt;p&gt;The other big gap is attestation: proof that an agent actually did what you asked and didn't go off the rails and do something else. There are a lot of interesting proposals on the table, and we might soon spin up a working group there as well.&lt;/p&gt;
&lt;p&gt;Neither area is covered by an AAIF project today. Both are worth watching closely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OpenEnv brings AAIF into reinforcement learning. Why do training and evaluation belong in the same foundation as runtime protocols? What does it mean for teams that never train a model?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Having a number of projects doesn't mean everyone needs to adopt everything in one stack. Some pieces matter more to certain engineers, companies, or products, and less to others.&lt;/p&gt;
&lt;p&gt;OpenEnv is a good example. It appeals strongly to a certain class of companies and products, but not to everyone. If your team never trains a model, nothing obliges you to touch it. Adopt the protocols and runtime pieces that fit your needs and ignore the rest. AAIF is a home for the building blocks of the agentic ecosystem. Each team takes what's relevant to what it's building.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AAIF passed 300 members in under a year. How does an individual developer or a small team get a real say in a project's roadmap?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Like many open source projects, it's very meritocratic. Get involved, roll up your sleeves, and start working on the project. Contribute ideas, conversations, or code. That's how you earn credibility with the project, and the more you have, the more you can shape it.&lt;/p&gt;
&lt;p&gt;There's no fixed timeline. It depends on how much you put in. The path is the same for an individual developer as for a large company: participate, show what you're good at, and demonstrate commitment. Over time, you earn the trust to influence the project's direction and potentially become a maintainer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vendors ship new agent features every few weeks. Consensus takes time. How do you balance speed with neutral governance?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One of our main tools is the Sandbox lifecycle stage. &lt;em&gt;(Editor's note: this is a project maturity level, not the OpenSandbox project.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When we started the foundation, projects could come in at two levels. Impact meant broad, industry-wide adoption, a project that was already a pillar of the industry. Growth was one step before that: production usage and adoption, but not yet ubiquitous.&lt;/p&gt;
&lt;p&gt;Even that wasn't enough in a space moving this fast. We needed something earlier for projects with little production usage but lots of developer adoption, momentum, and excitement. Sandbox lets us bring those projects in early without investing too much, so we can watch, learn, and help shape the space.&lt;/p&gt;
&lt;p&gt;I think of it as early-stage investing. You make early bets. Some become big and important. Most won't, and that's okay.&lt;/p&gt;
&lt;p&gt;As for a concrete example of governance speeding up or slowing down a project, I think it's still too early to point to one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What should developers expect from AAIF over the next 12 months?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A lot more projects, many more member companies, and a number of AGNTCon + MCPCon events. We have a big lineup planned for next year, from local grassroots events to large conferences like this one. For updates, follow along at &lt;a rel=&quot;nofollow&quot; href=&quot;https://aaif.io&quot;&gt;aaif.io&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-developers-should-take-from-this&quot;&gt;What developers should take from this&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nothing breaks.&lt;/strong&gt; If you build on MCP, Agent Skills, or Open Responses today, your code doesn't change. What changes is who gets a say in what comes next.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don't wait for a blueprint.&lt;/strong&gt; AAIF won't publish a reference stack any time soon. Production deployments are the reference.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sandbox and gateway now.&lt;/strong&gt; Surtani was clear: running agents without a sandbox is dangerous. A gateway in the middle keeps your options open on models and tools.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Watch identity and attestation.&lt;/strong&gt; These are the two biggest open security gaps, and no AAIF project covers them yet. If you're building agents that act on real systems, that's where your own controls need to be strongest.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Influence is earned.&lt;/strong&gt; Contributing is the path to a say in the roadmap, for individuals and large vendors alike.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32119/aaif-hits-10-projects-its-cto-on-what-to-adopt-now-and-the-security-gaps-left-open</guid>
<pubDate>Fri, 23 Oct 2026 12:00:00 +0000</pubDate>
</item>
<item>
<title>Building the MyZubster Metaverse: From Prototype to Secure Realtime Infrastructure</title>
<link>https://coderlegion.com/32147/building-the-myzubster-metaverse-from-prototype-to-secure-realtime-infrastructure</link>
<description>&lt;p&gt;Building the MyZubster Metaverse: From Prototype to Secure Realtime Infrastructure&lt;br&gt;
The MyZubster Metaverse is evolving from a simple interactive experience into a distributed platform designed around secure identity, realtime communication, reliability, privacy, and verifiable collaboration.&lt;br&gt;
During the latest development phase, we focused on the infrastructure required to operate the metaverse safely under real production conditions.&lt;br&gt;
This article explains what we implemented, how the architecture works, what we verified, and what remains before the system can be considered production-ready.&lt;br&gt;
The Current Architecture&lt;br&gt;
The metaverse now follows a layered architecture:&lt;br&gt;
Authenticated user&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MyZubster session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Short-lived realtime token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authorized realtime channel&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Distributed gateway&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis coordination layer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Presence, rate limits and observability&lt;br&gt;
The objective is to prevent clients from connecting directly to unrestricted realtime resources.&lt;br&gt;
Every connection must originate from an authenticated MyZubster session and receive only the permissions required for its specific room or operation.&lt;br&gt;
Authenticated Realtime Sessions&lt;br&gt;
We introduced a dedicated token exchange for realtime connections.&lt;br&gt;
The server:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;verifies the current MyZubster session;&lt;/li&gt;
&lt;li&gt;checks whether the user may access the requested resource;&lt;/li&gt;
&lt;li&gt;generates a short-lived realtime token;&lt;/li&gt;
&lt;li&gt;restricts that token to authorized channels;&lt;/li&gt;
&lt;li&gt;rejects anonymous or malformed requests.&lt;br&gt;
This limits the damage caused by leaked tokens and prevents clients from subscribing to arbitrary rooms.&lt;br&gt;
A client cannot simply invent a room identifier and connect to it. Authorization is evaluated server-side before realtime access is granted.&lt;br&gt;
Channel Authorization&lt;br&gt;
Realtime channels are treated as protected resources.&lt;br&gt;
The authorization layer validates:&lt;/li&gt;
&lt;li&gt;user identity;&lt;/li&gt;
&lt;li&gt;session validity;&lt;/li&gt;
&lt;li&gt;resource ownership;&lt;/li&gt;
&lt;li&gt;room membership;&lt;/li&gt;
&lt;li&gt;token expiration;&lt;/li&gt;
&lt;li&gt;permitted operations.&lt;br&gt;
This establishes a clear trust boundary between the browser and the realtime gateway.&lt;br&gt;
The client requests access, but the server remains responsible for deciding whether that access is legitimate.&lt;br&gt;
Distributed Presence with Redis&lt;br&gt;
In-memory presence works only while a single application instance is running. It becomes unreliable when multiple instances or serverless functions are involved.&lt;br&gt;
We therefore introduced Redis-backed presence coordination.&lt;br&gt;
Redis is used to track:&lt;/li&gt;
&lt;li&gt;active participants;&lt;/li&gt;
&lt;li&gt;room membership;&lt;/li&gt;
&lt;li&gt;temporary presence state;&lt;/li&gt;
&lt;li&gt;expiration of abandoned sessions;&lt;/li&gt;
&lt;li&gt;coordination between separate gateway instances.&lt;br&gt;
Presence entries have a limited lifetime. If a client disappears without completing a normal disconnection, the corresponding state can expire automatically instead of remaining indefinitely.&lt;br&gt;
Graceful Redis Failure Handling&lt;br&gt;
Redis is now considered an explicit infrastructure dependency.&lt;br&gt;
The system distinguishes between:&lt;/li&gt;
&lt;li&gt;the application process being alive;&lt;/li&gt;
&lt;li&gt;the realtime gateway being operational;&lt;/li&gt;
&lt;li&gt;Redis being reachable;&lt;/li&gt;
&lt;li&gt;the complete service being ready to accept traffic.&lt;br&gt;
Failures are handled deliberately rather than silently ignored. This is important because returning a successful health response while the coordination layer is unavailable would provide misleading information to deployment systems and operators.&lt;br&gt;
Distributed Rate Limiting&lt;br&gt;
Local counters cannot reliably enforce limits across multiple runtime instances.&lt;br&gt;
We implemented a distributed rate-limiting foundation backed by Redis so that requests are evaluated against shared state.&lt;br&gt;
The rate-limiting system is designed to protect:&lt;/li&gt;
&lt;li&gt;realtime token generation;&lt;/li&gt;
&lt;li&gt;connection attempts;&lt;/li&gt;
&lt;li&gt;room joins;&lt;/li&gt;
&lt;li&gt;message publication;&lt;/li&gt;
&lt;li&gt;presence updates;&lt;/li&gt;
&lt;li&gt;abuse-sensitive API routes.&lt;br&gt;
This provides a foundation for defending the metaverse against spam, accidental request loops and automated abuse.&lt;br&gt;
Future work will refine quotas by route, identity, IP risk and operation type.&lt;br&gt;
Privacy-Safe Observability&lt;br&gt;
Reliability requires visibility, but observability must not expose private user information.&lt;br&gt;
The realtime infrastructure now supports structured operational signals for:&lt;/li&gt;
&lt;li&gt;authorization failures;&lt;/li&gt;
&lt;li&gt;connection lifecycle events;&lt;/li&gt;
&lt;li&gt;Redis availability;&lt;/li&gt;
&lt;li&gt;rate-limit decisions;&lt;/li&gt;
&lt;li&gt;degraded states;&lt;/li&gt;
&lt;li&gt;health and readiness checks.&lt;br&gt;
The goal is to make infrastructure problems diagnosable without logging raw credentials, session tokens or unnecessary personal information.&lt;br&gt;
Health and Readiness Endpoints&lt;br&gt;
A process being alive does not mean it is ready to serve realtime traffic.&lt;br&gt;
We extended the health model to include Redis readiness. The service can now report a degraded state when Redis is unavailable instead of returning an unconditional success response.&lt;br&gt;
Conceptually:&lt;br&gt;
Process alive + Redis unavailable&lt;pre&gt;&lt;code&gt;       ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Service is not fully ready&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Health response reports degradation&lt;br&gt;
This distinction is essential for:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;deployment validation;&lt;/li&gt;
&lt;li&gt;load balancers;&lt;/li&gt;
&lt;li&gt;automated monitoring;&lt;/li&gt;
&lt;li&gt;incident diagnosis;&lt;/li&gt;
&lt;li&gt;rolling releases;&lt;/li&gt;
&lt;li&gt;recovery procedures.&lt;br&gt;
Shared Redis Client&lt;br&gt;
A reusable Redis client layer was introduced to avoid creating duplicated connections across services and requests.&lt;br&gt;
The shared client centralizes:&lt;/li&gt;
&lt;li&gt;configuration;&lt;/li&gt;
&lt;li&gt;connection reuse;&lt;/li&gt;
&lt;li&gt;error handling;&lt;/li&gt;
&lt;li&gt;readiness checks;&lt;/li&gt;
&lt;li&gt;shutdown behavior;&lt;/li&gt;
&lt;li&gt;instrumentation.&lt;br&gt;
Centralizing this behavior reduces connection pressure and gives every Redis-dependent feature a consistent operational model.&lt;br&gt;
Preview Infrastructure&lt;br&gt;
The new realtime changes were deployed to an isolated Vercel Preview environment.&lt;br&gt;
This allows us to validate infrastructure modifications before they affect the public production deployment.&lt;br&gt;
The preview environment provides a safe place to test:&lt;/li&gt;
&lt;li&gt;application builds;&lt;/li&gt;
&lt;li&gt;serverless deployment behavior;&lt;/li&gt;
&lt;li&gt;environment configuration;&lt;/li&gt;
&lt;li&gt;readiness responses;&lt;/li&gt;
&lt;li&gt;Redis connectivity;&lt;/li&gt;
&lt;li&gt;authentication flows;&lt;/li&gt;
&lt;li&gt;realtime regressions.&lt;br&gt;
Production remains unchanged while the current pull request is still under review.&lt;br&gt;
Testing and Continuous Integration&lt;br&gt;
The implementation is covered by automated backend tests, including readiness behavior and Redis degradation scenarios.&lt;br&gt;
The principal validation pipeline successfully completed:&lt;/li&gt;
&lt;li&gt;backend tests;&lt;/li&gt;
&lt;li&gt;frontend tests;&lt;/li&gt;
&lt;li&gt;linting;&lt;/li&gt;
&lt;li&gt;application builds;&lt;/li&gt;
&lt;li&gt;Seller Free policy verification;&lt;/li&gt;
&lt;li&gt;continuous evidence generation;&lt;/li&gt;
&lt;li&gt;preview deployment.&lt;br&gt;
This provides strong evidence that the new infrastructure integrates correctly with the existing application.&lt;br&gt;
However, passing tests alone do not make the service production-ready. Runtime drills, dependency remediation and real multi-instance load testing are still required.&lt;br&gt;
Current Development Status&lt;br&gt;
The realtime readiness work is currently available in a draft pull request:&lt;br&gt;
MyZubster realtime readiness PR #40&lt;br&gt;
The implementation includes four focused commits:&lt;br&gt;
feat(realtime): expose Redis readiness&lt;br&gt;
test(realtime): cover Redis readiness&lt;br&gt;
feat(realtime): gate health on Redis&lt;br&gt;
test(realtime): cover Redis health degradation&lt;br&gt;
Keeping the commits focused makes review and rollback easier.&lt;br&gt;
Current Security Blocker&lt;br&gt;
The security audit currently reports vulnerabilities in the transitive ip-address dependency for versions up to 10.7.3.&lt;br&gt;
The available patched version is 10.7.4.&lt;br&gt;
Before merging the realtime work, we need to:&lt;/li&gt;
&lt;li&gt;identify which direct dependency introduces ip-address;&lt;/li&gt;
&lt;li&gt;update the dependency chain safely;&lt;/li&gt;
&lt;li&gt;regenerate the lockfile;&lt;/li&gt;
&lt;li&gt;run the complete test suite;&lt;/li&gt;
&lt;li&gt;repeat the security audit;&lt;/li&gt;
&lt;li&gt;confirm that no high or critical vulnerability remains.&lt;br&gt;
The pull request should remain in draft until this has been addressed and the runtime verification has been completed.&lt;br&gt;
What Still Needs to Be Done&lt;/li&gt;
&lt;li&gt;Dependency remediation&lt;br&gt;
Upgrade the affected dependency chain and obtain a clean security audit.&lt;/li&gt;
&lt;li&gt;Direct runtime verification&lt;br&gt;
Verify the deployed preview response from:&lt;br&gt;
/api/realtime/health&lt;br&gt;
The response should explicitly show whether Redis is ready and whether the service is operating normally or in a degraded state.&lt;/li&gt;
&lt;li&gt;Redis failure drill&lt;br&gt;
Temporarily simulate Redis unavailability in an isolated environment and verify that:&lt;/li&gt;
&lt;li&gt;readiness fails correctly;&lt;/li&gt;
&lt;li&gt;the service does not claim to be healthy;&lt;/li&gt;
&lt;li&gt;errors remain controlled;&lt;/li&gt;
&lt;li&gt;recovery occurs after Redis reconnects.&lt;/li&gt;
&lt;li&gt;Multi-instance load testing&lt;br&gt;
Run concurrent tests against multiple gateway instances and verify:&lt;/li&gt;
&lt;li&gt;shared rate limits;&lt;/li&gt;
&lt;li&gt;consistent presence;&lt;/li&gt;
&lt;li&gt;room synchronization;&lt;/li&gt;
&lt;li&gt;connection cleanup;&lt;/li&gt;
&lt;li&gt;acceptable latency;&lt;/li&gt;
&lt;li&gt;stable Redis connection usage.&lt;/li&gt;
&lt;li&gt;Restart and recovery testing&lt;br&gt;
Restart application and Redis instances while clients are connected.&lt;br&gt;
We need evidence that the platform can recover without leaving permanent ghost sessions or inconsistent presence records.&lt;/li&gt;
&lt;li&gt;End-to-end acceptance testing&lt;br&gt;
The complete flow must be tested:&lt;br&gt;
Login&lt;br&gt;
→ request realtime token&lt;br&gt;
→ authorize channel&lt;br&gt;
→ connect&lt;br&gt;
→ join room&lt;br&gt;
→ publish event&lt;br&gt;
→ update presence&lt;br&gt;
→ disconnect&lt;br&gt;
→ clean up state&lt;br&gt;
Unauthorized variations must also be tested.&lt;/li&gt;
&lt;li&gt;Production rollout strategy&lt;br&gt;
The production release should be gradual:&lt;/li&gt;
&lt;li&gt;deploy to Preview;&lt;/li&gt;
&lt;li&gt;complete runtime verification;&lt;/li&gt;
&lt;li&gt;run load and failure drills;&lt;/li&gt;
&lt;li&gt;review monitoring signals;&lt;/li&gt;
&lt;li&gt;merge after approval;&lt;/li&gt;
&lt;li&gt;deploy production;&lt;/li&gt;
&lt;li&gt;observe error rates and Redis health;&lt;/li&gt;
&lt;li&gt;retain a documented rollback procedure.&lt;br&gt;
Longer-Term Metaverse Roadmap&lt;br&gt;
Once reliability and security are validated, development can move toward the experience layer.&lt;br&gt;
Planned areas include:&lt;/li&gt;
&lt;li&gt;WebXR support;&lt;/li&gt;
&lt;li&gt;immersive rooms;&lt;/li&gt;
&lt;li&gt;interactive avatars;&lt;/li&gt;
&lt;li&gt;spatial collaboration;&lt;/li&gt;
&lt;li&gt;verified professional profiles;&lt;/li&gt;
&lt;li&gt;Zorgax integration;&lt;/li&gt;
&lt;li&gt;verified knowledge inside virtual environments;&lt;/li&gt;
&lt;li&gt;community-managed spaces;&lt;/li&gt;
&lt;li&gt;portable identity;&lt;/li&gt;
&lt;li&gt;privacy-safe analytics;&lt;/li&gt;
&lt;li&gt;independent interoperable nodes.&lt;br&gt;
The long-term objective is not merely to build another virtual world.&lt;br&gt;
MyZubster aims to combine identity, knowledge, collaboration and transparent open-source development inside an interoperable environment.&lt;br&gt;
Lessons Learned&lt;br&gt;
The most important lesson from this phase is that realtime development is not simply about sending messages quickly.&lt;br&gt;
A reliable realtime platform must answer several harder questions:&lt;/li&gt;
&lt;li&gt;Who is allowed to connect?&lt;/li&gt;
&lt;li&gt;Which channels may they access?&lt;/li&gt;
&lt;li&gt;How is state shared across instances?&lt;/li&gt;
&lt;li&gt;What happens when Redis fails?&lt;/li&gt;
&lt;li&gt;How is abuse limited globally?&lt;/li&gt;
&lt;li&gt;Can operators distinguish “alive” from “ready”?&lt;/li&gt;
&lt;li&gt;Can the system recover cleanly after a restart?&lt;/li&gt;
&lt;li&gt;Can failures be investigated without exposing private data?&lt;br&gt;
These concerns are now becoming first-class parts of the MyZubster architecture.&lt;br&gt;
Conclusion&lt;br&gt;
The MyZubster Metaverse has progressed beyond an isolated prototype.&lt;br&gt;
We now have the foundation for:&lt;/li&gt;
&lt;li&gt;authenticated realtime access;&lt;/li&gt;
&lt;li&gt;server-controlled channel authorization;&lt;/li&gt;
&lt;li&gt;Redis-backed presence;&lt;/li&gt;
&lt;li&gt;distributed rate limiting;&lt;/li&gt;
&lt;li&gt;privacy-aware observability;&lt;/li&gt;
&lt;li&gt;dependency-aware health checks;&lt;/li&gt;
&lt;li&gt;isolated preview deployments;&lt;/li&gt;
&lt;li&gt;automated tests and evidence generation.&lt;br&gt;
The next milestone is to prove that this architecture remains reliable under dependency failures, restarts, concurrency and realistic traffic.&lt;br&gt;
Only after those checks—and after resolving the current security advisory—should the realtime infrastructure be promoted to production.&lt;br&gt;
MyZubster remains an open-source project, and technical review, testing and constructive contributions are welcome.&lt;br&gt;
Project Links&lt;/li&gt;
&lt;li&gt;MyZubster repository&lt;/li&gt;
&lt;li&gt;MyZubster Metaverse&lt;/li&gt;
&lt;li&gt;Realtime readiness pull request&lt;br&gt;
 Repository GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster&lt;/a&gt;&lt;br&gt;
Metaverso MyZubster: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse&quot;&gt;https://www.myzubster.com/metaverse&lt;/a&gt;&lt;br&gt;
Pull request Realtime Readiness #40: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster/pull/40&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster/pull/40&lt;/a&gt;&lt;br&gt;
Roadmap Linear MYZ-83: &lt;a rel=&quot;nofollow&quot; href=&quot;https://linear.app/myzubster/issue/MYZ-83/b6-realtime-observability-reliability-and-load-tests&quot;&gt;https://linear.app/myzubster/issue/MYZ-83/b6-realtime-observability-reliability-and-load-tests&lt;/a&gt;&lt;br&gt;
Organizzazione GitHub MyZubster Ecosystem: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem&quot;&gt;https://github.com/MyZubster-Ecosystem&lt;/a&gt;&lt;br&gt;
Profilo GitHub MyZubster: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/myzubster&quot;&gt;https://github.com/myzubster&lt;/a&gt;&lt;br&gt;
Sito principale: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com&quot;&gt;https://www.myzubster.com&lt;/a&gt;&lt;br&gt;
Knowledge Explorer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/knowledge&quot;&gt;https://www.myzubster.com/knowledge&lt;/a&gt;&lt;br&gt;
Profilo Zorgax: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/zorgax-profile-onboarding&quot;&gt;https://www.myzubster.com/zorgax-profile-onboarding&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32147/building-the-myzubster-metaverse-from-prototype-to-secure-realtime-infrastructure</guid>
<pubDate>Fri, 23 Oct 2026 05:22:14 +0000</pubDate>
</item>
<item>
<title>I Built Avi.js — A Lightweight TypeScript Framework Inspired by NestJS</title>
<link>https://coderlegion.com/32080/i-built-avi-js-a-lightweight-typescript-framework-inspired-by-nestjs</link>
<description>&lt;h3 id=&quot;i-built-avi-js-a-lightweight-typescript-framework-inspired-by-nestjs&quot;&gt;I Built Avi.js — A Lightweight TypeScript Framework Inspired by NestJS&lt;/h3&gt;
&lt;p&gt;I've been working on an open-source project called &lt;strong&gt;Avi.js&lt;/strong&gt;, and I recently released &lt;strong&gt;v0.1.2&lt;/strong&gt;! 🚀&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avi.js — Structure without the overhead.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It's a lightweight, TypeScript-first backend framework for Node.js, inspired by the structured development experience of NestJS.&lt;/p&gt;
&lt;p&gt;The idea started with a question:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Can we build a structured TypeScript backend framework with modules, controllers, and dependency injection—without requiring &lt;code&gt;reflect-metadata&lt;/code&gt;?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That's what I've been exploring with Avi.js.&lt;/p&gt;
&lt;h3 id=&quot;what-i-ve-built-so-far&quot;&gt;What I've built so far&lt;/h3&gt;
&lt;p&gt;Before getting into the latest update, here's a look at what Avi.js currently supports.&lt;/p&gt;
&lt;h4 id=&quot;1-modules-controllers-and-dependency-injection&quot;&gt;1. Modules, controllers, and dependency injection&lt;/h4&gt;
&lt;p&gt;Avi.js supports a modular architecture with explicit dependency declarations rather than relying on runtime reflection.&lt;/p&gt;
&lt;p&gt;It includes singleton, transient, and request-scoped providers, along with startup checks for missing dependencies and circular references.&lt;/p&gt;
&lt;p&gt;The goal is to keep dependencies visible in code while providing a familiar way to organize backend applications.&lt;/p&gt;
&lt;h4 id=&quot;2-two-api-styles-one-engine&quot;&gt;2. Two API styles, one engine&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Avi.js&lt;/code&gt; supports two ways of defining controllers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Decorator-based API:&lt;/strong&gt; Class-based controllers and modules using standard TC39 decorators.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicit API:&lt;/strong&gt; Plain TypeScript objects and functions without decorators.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both approaches use the same router, dependency injection container, and request pipeline.&lt;/p&gt;
&lt;p&gt;This gives developers the flexibility to choose the style that fits their project.&lt;/p&gt;
&lt;h4 id=&quot;3-dependency-injection-without-reflect-metadata&quot;&gt;3. Dependency injection without &lt;code&gt;reflect-metadata&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Instead of inferring constructor dependencies through emitted runtime metadata, Avi.js uses explicit declarations such as &lt;code&gt;static inject&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Controller('/users')
class UsersController {
  static inject = [UsersService] as const;

  constructor(private readonly users: UsersService) {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach doesn't require &lt;code&gt;reflect-metadata&lt;/code&gt;, &lt;code&gt;emitDecoratorMetadata&lt;/code&gt;, or legacy experimental decorators.&lt;/p&gt;
&lt;p&gt;It's a design choice focused on making dependency wiring explicit and understandable.&lt;/p&gt;
&lt;h4 id=&quot;4-native-node-js-http&quot;&gt;4. Native Node.js HTTP&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Avi.js&lt;/code&gt; runs on Node.js's native &lt;code&gt;node:http&lt;/code&gt; module without requiring Express or Fastify underneath.&lt;/p&gt;
&lt;p&gt;It supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP routing and route parameters&lt;/li&gt;
&lt;li&gt;Middleware and guards&lt;/li&gt;
&lt;li&gt;Request and response hooks&lt;/li&gt;
&lt;li&gt;JSON, text, and streaming responses&lt;/li&gt;
&lt;li&gt;Graceful shutdown&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;5-validation-security-and-developer-tooling&quot;&gt;5. Validation, security, and developer tooling&lt;/h4&gt;
&lt;p&gt;Avi.js also includes several features for building and maintaining backend applications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Schema validation using Standard Schema-compatible libraries&lt;/li&gt;
&lt;li&gt;CORS and security headers&lt;/li&gt;
&lt;li&gt;OpenAPI 3.1 document generation&lt;/li&gt;
&lt;li&gt;Testing utilities with provider overrides&lt;/li&gt;
&lt;li&gt;CLI scaffolding and code generation&lt;/li&gt;
&lt;li&gt;Startup checks for dependency and route configuration errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These features are already available in the current early-development releases.&lt;/p&gt;
&lt;h3 id=&quot;what-s-new-in-v0-1-2-&quot;&gt;What's new in v0.1.2?&lt;/h3&gt;
&lt;p&gt;One feature I'm particularly excited about is &lt;strong&gt;resource generation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You can now generate an entire CRUD resource using one command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx avi g resource posts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a modular project, the CLI generates:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;src/
└── modules/
    └── posts/
        ├── posts.module.ts
        ├── posts.controller.ts
        └── posts.service.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CLI also registers the resource in your application automatically.&lt;/p&gt;
&lt;p&gt;The generated resource includes working &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; routes backed by an in-memory store, which you can later replace with your own database integration.&lt;/p&gt;
&lt;p&gt;For minimal projects, Avi.js generates services and routes without requiring modules.&lt;/p&gt;
&lt;p&gt;The goal is to reduce repetitive setup while keeping the generated code understandable and editable.&lt;/p&gt;
&lt;h3 id=&quot;getting-started&quot;&gt;Getting started&lt;/h3&gt;
&lt;p&gt;You can try Avi.js with &lt;strong&gt;Node.js 22.12 or newer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Create a new project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm create avi@latest my-api
cd my-api
npm install
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Avi.js currently provides two official project templates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modular (default):&lt;/strong&gt; A feature-first structure with modules, controllers, and services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal:&lt;/strong&gt; A smaller, functional structure using explicit routes and services.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both templates run on the same framework engine, so you can start small and introduce more structure as your application grows.&lt;/p&gt;
&lt;p&gt;You can also install the core package directly into an existing project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @avijs/core
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-am-i-sharing-this-&quot;&gt;Why am I sharing this?&lt;/h3&gt;
&lt;p&gt;Avi.js is still in &lt;strong&gt;pre-alpha development (0.x)&lt;/strong&gt;, and its APIs may change before 1.0.&lt;/p&gt;
&lt;p&gt;I'm not trying to claim it's faster or better than established frameworks. It's an experiment in building a structured backend framework with explicit dependencies and fewer runtime abstractions.&lt;/p&gt;
&lt;p&gt;I'm sharing it because I'd love to get feedback from developers who enjoy working with Node.js and TypeScript.&lt;/p&gt;
&lt;p&gt;If you're curious, feel free to try it, open an issue, or contribute.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Official website:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://chrischase011.github.io/avi-js/&quot;&gt;https://chrischase011.github.io/avi-js/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chrischase011/avi-js&quot;&gt;https://github.com/chrischase011/avi-js&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Latest release:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chrischase011/avi-js/releases/tag/v0.1.2&quot;&gt;https://github.com/chrischase011/avi-js/releases/tag/v0.1.2&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thanks for checking it out! 🚀&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/t/typescript&quot;&gt;#typescript&lt;/a&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/t/nodejs&quot;&gt;#nodejs&lt;/a&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/t/opensource&quot;&gt;#opensource&lt;/a&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/t/webdev&quot;&gt;#webdev&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32080/i-built-avi-js-a-lightweight-typescript-framework-inspired-by-nestjs</guid>
<pubDate>Thu, 22 Oct 2026 14:41:50 +0000</pubDate>
</item>
<item>
<title>PHP2Shell: Two 9.2-Rated Flaws Reach Back to PHP 5. Here's the Plan for EOL Teams</title>
<link>https://coderlegion.com/32048/php2shell-two-9-2-rated-flaws-reach-back-to-php-5-heres-the-plan-for-eol-teams</link>
<description>&lt;p&gt;PHP shipped emergency patches on October 21 for two critical flaws now known as PHP2Shell. Both are rated 9.2 under CVSS v4.0. Both can let an unauthenticated attacker run code on your server.&lt;/p&gt;
&lt;p&gt;The fixes are in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. If you run a supported branch, update and restart your PHP workers. That part is simple.&lt;/p&gt;
&lt;p&gt;The harder question is what to do if you run a version that no longer gets community patches. A lot of teams are in that spot.&lt;/p&gt;
&lt;p&gt;I asked Matthew Weier O'Phinney about it. He's principal product manager for Perforce OpenLogic and Zend, and he sits on the PHP Foundation board. Perforce sells extended support for end-of-life (EOL) open source software, so keep that in mind when he talks about extended support. But he also had a close view of how this disclosure played out.&lt;/p&gt;
&lt;h3 id=&quot;what-the-flaws-do&quot;&gt;What the flaws do&lt;/h3&gt;
&lt;p&gt;CVE-2026-109618 corrupts memory when PHP hits an out-of-memory error in the middle of a request. CVE-2026-109619 involves stale global state. PHP keeps pointers to request data after that memory has been freed.&lt;/p&gt;
&lt;p&gt;Weier O'Phinney put it in plain terms. &quot;An attacker can create one or more payloads that simulate large file uploads, or multi-file uploads, that hit configured memory limits for the PHP application,&quot; he said. Because of how PHP handled memory and errors at that moment, the attacker could end up running system commands on the server.&lt;/p&gt;
&lt;p&gt;The attack doesn't need a vulnerable application or a public upload form. And the timeline was tight. Details leaked on October 20, a day before the coordinated release. Security firm Sansec reports that Cloudflare saw attacks hours before the patches shipped.&lt;/p&gt;
&lt;h3 id=&quot;it-s-not-just-mod-php&quot;&gt;It's not just mod_php&lt;/h3&gt;
&lt;p&gt;PHP's advisory named Apache's mod_php as the highest-risk configuration. Some early coverage stopped there. Weier O'Phinney says that's too narrow.&lt;/p&gt;
&lt;p&gt;&quot;These vulnerabilities affect every single Server API (SAPI) provided by the language, because their roots are deep within how the engine manages memory and handles errors,&quot; he said. &quot;If you run PHP, your application is affected, plain and simple.&quot;&lt;/p&gt;
&lt;p&gt;mod_php may be the easiest path in. But PHP has also confirmed remote code execution against PHP-FPM under the right application conditions. If you run FPM behind Nginx, you're not off the hook.&lt;/p&gt;
&lt;h3 id=&quot;workarounds-buy-time-they-don-t-fix-the-problem-&quot;&gt;Workarounds buy time. They don't fix the problem.&lt;/h3&gt;
&lt;p&gt;The early advice was to turn off session upload progress and remove memory limits. Weier O'Phinney says those steps help, but only so much. &quot;They just change the size of the file upload payloads the attacker needs to use to exploit the system,&quot; he said.&lt;/p&gt;
&lt;p&gt;He's more positive about two other measures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A web application firewall (WAF) that limits which paths accept uploads, or any user submission&lt;/li&gt;
&lt;li&gt;AppArmor or SELinux to restrict PHP's access to system binaries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both are good defense-in-depth. But he warns that creative attackers can still find ways around them if the PHP runtime underneath is vulnerable.&lt;/p&gt;
&lt;p&gt;PHP says the same thing. Updating is the only complete fix. If you removed the memory limit as a stopgap, put a container or cgroup limit in place, and restore a finite limit once you've patched.&lt;/p&gt;
&lt;h3 id=&quot;the-end-of-life-problem&quot;&gt;The end-of-life problem&lt;/h3&gt;
&lt;p&gt;Here's where it gets uncomfortable. According to Weier O'Phinney, every PHP version before 8.2 is past community support. And 8.2 reaches end of life at the end of this year.&lt;/p&gt;
&lt;p&gt;These flaws trace back to the PHP 5 series. &quot;If you're running PHP 5, PHP 7, or early PHP 8 versions, you will need to audit your applications and deployments,&quot; he said.&lt;/p&gt;
&lt;p&gt;He sees two paths:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Use a commercial long-term support (LTS) provider.&lt;/strong&gt; Several have patched EOL versions. Perforce Zend patched PHP 7.2, 7.3, 7.4 and 8.1 in coordination with the PHP Security team. If you already have a provider, confirm it shipped patches for your version.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start migrating to a community-supported version.&lt;/strong&gt; That means 8.2 or later. Since 8.2 is nearly at end of life itself, a migration that stops there only buys you a couple of months.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you have neither, his advice is to pick one today.&lt;/p&gt;
&lt;h3 id=&quot;ai-changed-the-clock&quot;&gt;AI changed the clock&lt;/h3&gt;
&lt;p&gt;The backstory matters here. A security audit sponsored by the Sovereign Tech Agency and the PHP Foundation found these attack vectors last year. But the security team couldn't find a practical way to trigger them. They treated the flaws as largely theoretical and kept working on them off and on.&lt;/p&gt;
&lt;p&gt;Then, this past August, two independent researchers found the same issues within days of each other. Each was using a frontier AI model. Each built a way to exploit them. The PHP Security team had to move them to the top of the list.&lt;/p&gt;
&lt;p&gt;That's the real shift. A bug that sat in the &quot;theoretical&quot; pile for a year became practical in a matter of days.&lt;/p&gt;
&lt;p&gt;Weier O'Phinney sees the same pattern across open source. The cURL project has been handling a flood of AI-discovered reports for two years. Spring has seen a big jump in reports this year. And those reports still need people to review them. Maintainers have to confirm that each patch works, doesn't break existing behavior and doesn't add new flaws. Many AI-generated reports also overstate severity, which takes more time to sort out.&lt;/p&gt;
&lt;p&gt;For EOL software, this is the core risk. Discovery is speeding up. Patches for unsupported versions aren't.&lt;/p&gt;
&lt;h3 id=&quot;migration-or-extended-support-&quot;&gt;Migration or extended support?&lt;/h3&gt;
&lt;p&gt;Weier O'Phinney is clear that migration is the goal. Current versions get new features and security fixes first, and the rest of the ecosystem tends to follow the latest releases.&lt;/p&gt;
&lt;p&gt;But he's also realistic. Continuous upgrades take discipline, solid testing and QA, and a DevOps team that can roll releases out and roll them back. Some companies also face regulatory reviews every time they move to a new feature release. That costs time and money, so migration gets pushed back.&lt;/p&gt;
&lt;p&gt;&quot;So adopt those new versions if you can, but have a plan for when you cannot,&quot; he said.&lt;/p&gt;
&lt;p&gt;That's fair advice, and it's worth taking at face value. Extended support is a bridge, not a destination. If you're evaluating a provider, ask one simple question: how fast did it ship PHP2Shell patches for your version? The answer tells you a lot.&lt;/p&gt;
&lt;h3 id=&quot;how-the-disclosure-worked&quot;&gt;How the disclosure worked&lt;/h3&gt;
&lt;p&gt;Weier O'Phinney knew for a couple of months that big security patches were coming. He didn't know the details. &quot;That's a good thing,&quot; he said. Fewer people in the loop means less chance of a leak.&lt;/p&gt;
&lt;p&gt;The PHP Foundation now employs a small team that coordinates security work between researchers and the PHP project. One of them contacted Weier O'Phinney in his role at a commercial LTS provider to line up releases. In more than 20 years at Zend, that was a first for him. The PHP Security team worked with Zend's packaging team and reviewed its patches so both were ready on release day.&lt;/p&gt;
&lt;p&gt;His bigger concern is capacity. The PHP Security team is small, and the volume of incoming reports keeps growing. He'd like to see it staffed well enough to coordinate like this more often. His ask: if your company depends on PHP, consider donating to the PHP Foundation.&lt;/p&gt;
&lt;h3 id=&quot;what-to-do-this-week&quot;&gt;What to do this week&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Find every PHP runtime you have.&lt;/strong&gt; Check servers, containers, base images and hosting accounts. Trust &lt;code&gt;php -v&lt;/code&gt;, not your documentation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Patch supported branches now.&lt;/strong&gt; Move to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, then restart PHP workers so the new code is actually running.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check EOL versions separately.&lt;/strong&gt; If you use an LTS provider, confirm it has shipped fixes. If you don't, choose between getting one and starting a migration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat workarounds as temporary.&lt;/strong&gt; Undo emergency config changes once you've patched.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep the extra layers.&lt;/strong&gt; A WAF and AppArmor or SELinux are worth having after the patch, too.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan for more of this.&lt;/strong&gt; AI-assisted discovery isn't slowing down. Budget time for triage and upgrades, not just for incidents.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PHP2Shell will get patched on most supported systems this week. The teams still exposed next month will be the ones running PHP that nobody owns. Now is a good time to find out if that's you.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32048/php2shell-two-9-2-rated-flaws-reach-back-to-php-5-heres-the-plan-for-eol-teams</guid>
<pubDate>Thu, 22 Oct 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>From Lab to Code #1: Exploring Classical Computer Vision with Python and OpenCV</title>
<link>https://coderlegion.com/32074/from-lab-to-code-1-exploring-classical-computer-vision-with-python-and-opencv</link>
<description>&lt;p&gt;Most conversations about computer vision today revolve around deep learning, neural networks, and increasingly sophisticated AI models.&lt;/p&gt;
&lt;p&gt;But not every image processing problem requires a trained model.&lt;/p&gt;
&lt;p&gt;Sometimes, a Gaussian filter is enough to reduce noise. A threshold can separate objects from their background. A Fourier transform can reveal interference that is difficult to understand in the spatial domain.&lt;/p&gt;
&lt;p&gt;These techniques are relatively simple, mathematically interpretable, and still useful in practical applications.&lt;/p&gt;
&lt;p&gt;Recently, while exploring image and signal processing concepts in my computer engineering studies, I decided to start a small programming series called &lt;strong&gt;From Lab to Code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The idea is simple: take engineering concepts I encounter, build independent implementations, visualize the results, and share what I learn.&lt;/p&gt;
&lt;p&gt;For the first project, I built a small interactive image processing toolkit using Python, OpenCV, NumPy, and Streamlit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project 01 — Image Processing:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-are-we-building-&quot;&gt;What Are We Building?&lt;/h3&gt;
&lt;p&gt;The application explores four classical computer vision techniques:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Image denoising&lt;/strong&gt; using Gaussian and median filters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Object counting&lt;/strong&gt; using illumination correction, thresholding, and connected components.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Periodic noise removal&lt;/strong&gt; using the Fast Fourier Transform (FFT).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Color clustering&lt;/strong&gt; using K-means.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The application provides interactive controls, generated example images, and visual comparisons.&lt;/p&gt;
&lt;p&gt;No pretrained models, API calls, or external datasets are required.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Ertugrulmutlu/from-lab-to-code/main/01-image-processing/results/overview.png&quot; alt=&quot;Image processing examples&quot;&gt;&lt;/p&gt;
&lt;p&gt;All bundled demonstration images are generated programmatically.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-image-denoising-gaussian-vs-median-filtering&quot;&gt;1. Image Denoising: Gaussian vs. Median Filtering&lt;/h3&gt;
&lt;p&gt;Digital images are often affected by noise.&lt;/p&gt;
&lt;p&gt;Noise can originate from camera sensors, poor lighting conditions, signal transmission, or image acquisition hardware.&lt;/p&gt;
&lt;p&gt;Two common types are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gaussian noise:&lt;/strong&gt; Random variations in pixel intensity, often modeled using a normal distribution.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Salt-and-pepper noise:&lt;/strong&gt; Individual pixels unexpectedly taking very dark or very bright values.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Different noise distributions require different filtering strategies.&lt;/p&gt;
&lt;h4 id=&quot;gaussian-filtering&quot;&gt;Gaussian Filtering&lt;/h4&gt;
&lt;p&gt;A Gaussian filter performs a weighted average of neighboring pixels.&lt;/p&gt;
&lt;p&gt;Pixels closer to the center receive higher weights, following a Gaussian distribution.&lt;/p&gt;
&lt;p&gt;Mathematically, the two-dimensional Gaussian kernel can be written as:&lt;/p&gt;
&lt;p&gt;G(x, y) = (1 / 2πσ²) × exp(-(x² + y²) / 2σ²)&lt;/p&gt;
&lt;p&gt;The parameter σ controls the amount of smoothing.&lt;/p&gt;
&lt;p&gt;A larger σ produces stronger smoothing but can also remove fine image details.&lt;/p&gt;
&lt;p&gt;In OpenCV, Gaussian filtering is straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import cv2

gaussian = cv2.GaussianBlur(
    image, (5, 5), 0
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;median-filtering&quot;&gt;Median Filtering&lt;/h4&gt;
&lt;p&gt;Unlike Gaussian filtering, a median filter replaces the center pixel with the median value of its neighborhood.&lt;/p&gt;
&lt;p&gt;Consider these pixel values:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[10, 11, 12, 13, 255]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Their average is 60.2, but their median is 12.&lt;/p&gt;
&lt;p&gt;If the value 255 represents an isolated noise spike, the median is much closer to the surrounding pixel values.&lt;/p&gt;
&lt;p&gt;This makes median filtering particularly useful for salt-and-pepper noise.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;median = cv2.medianBlur(image, 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;comparing-the-results&quot;&gt;Comparing the Results&lt;/h4&gt;
&lt;p&gt;The application applies both filters to the same noisy image.&lt;/p&gt;
&lt;p&gt;It also calculates the Mean Squared Error (MSE) against the original image when a reference is available.&lt;/p&gt;
&lt;p&gt;MSE = (1/N) × Σ(original - processed)²&lt;/p&gt;
&lt;p&gt;A lower MSE indicates a smaller average squared difference from the reference image.&lt;/p&gt;
&lt;p&gt;However, MSE alone does not fully describe perceptual image quality.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Gaussian filtering is useful for smoothing distributed noise, while median filtering is often more effective against isolated intensity spikes.&lt;/p&gt;
&lt;p&gt;Neither method is universally superior.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-object-counting-from-pixels-to-objects&quot;&gt;2. Object Counting: From Pixels to Objects&lt;/h3&gt;
&lt;p&gt;Suppose we want to count small objects placed on a table.&lt;/p&gt;
&lt;p&gt;At first, this sounds like a simple thresholding problem.&lt;/p&gt;
&lt;p&gt;If the objects are darker than the background, we could classify pixels based on their intensity.&lt;/p&gt;
&lt;p&gt;But what happens when the background illumination is uneven?&lt;/p&gt;
&lt;p&gt;One side of the image might be noticeably darker than the other, making a single global threshold unreliable.&lt;/p&gt;
&lt;h4 id=&quot;step-1-estimate-the-background&quot;&gt;Step 1: Estimate the Background&lt;/h4&gt;
&lt;p&gt;We can approximate slowly changing illumination using a large Gaussian blur.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;background = cv2.GaussianBlur(
    gray, (0, 0), sigmaX=30
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Subtracting the original image from the estimated background helps emphasize darker foreground objects.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;corrected = cv2.subtract(
    background, gray
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reduces the influence of gradual illumination changes.&lt;/p&gt;
&lt;h4 id=&quot;step-2-apply-otsu-thresholding&quot;&gt;Step 2: Apply Otsu Thresholding&lt;/h4&gt;
&lt;p&gt;Otsu's method automatically selects a threshold based on the image's intensity distribution.&lt;/p&gt;
&lt;p&gt;Instead of manually choosing a number such as 100 or 150, it searches for a threshold that separates the intensity classes according to their statistical variance.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;_, mask = cv2.threshold(
    corrected,
    0,
    255,
    cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output is a binary image containing foreground and background pixels.&lt;/p&gt;
&lt;h4 id=&quot;step-3-identify-connected-components&quot;&gt;Step 3: Identify Connected Components&lt;/h4&gt;
&lt;p&gt;After thresholding, neighboring foreground pixels can be grouped into connected regions.&lt;/p&gt;
&lt;p&gt;OpenCV provides a useful function for this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;count, labels, stats, centroids = (
    cv2.connectedComponentsWithStats(
        mask, connectivity=8
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function returns component labels and information such as bounding boxes and pixel areas.&lt;/p&gt;
&lt;p&gt;By excluding the background and removing regions below a minimum area, we can estimate the number of objects.&lt;/p&gt;
&lt;p&gt;The project also applies a small morphological opening operation before counting to remove isolated foreground noise.&lt;/p&gt;
&lt;h4 id=&quot;where-can-this-be-useful-&quot;&gt;Where Can This Be Useful?&lt;/h4&gt;
&lt;p&gt;This approach can be applied to controlled visual inspection tasks involving separate objects, such as small manufactured components.&lt;/p&gt;
&lt;p&gt;However, it comes with important assumptions.&lt;/p&gt;
&lt;p&gt;Objects should be sufficiently separated, and the contrast between objects and background should be reasonably clear.&lt;/p&gt;
&lt;p&gt;Touching objects may merge into a single connected component.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; Reliable object counting often requires preprocessing, not just thresholding.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-fourier-transform-removing-periodic-noise&quot;&gt;3. Fourier Transform: Removing Periodic Noise&lt;/h3&gt;
&lt;p&gt;This was one of the most interesting parts of the project.&lt;/p&gt;
&lt;p&gt;Images are normally represented in the spatial domain, where each pixel corresponds to a position.&lt;/p&gt;
&lt;p&gt;But some image artifacts are easier to identify in the frequency domain.&lt;/p&gt;
&lt;p&gt;For example, imagine an image affected by regularly repeating stripes.&lt;/p&gt;
&lt;p&gt;Those stripes represent periodic variations in pixel intensity.&lt;/p&gt;
&lt;h4 id=&quot;moving-to-the-frequency-domain&quot;&gt;Moving to the Frequency Domain&lt;/h4&gt;
&lt;p&gt;The two-dimensional Discrete Fourier Transform decomposes an image into spatial frequency components.&lt;/p&gt;
&lt;p&gt;Each component represents a particular frequency and orientation.&lt;/p&gt;
&lt;p&gt;Using NumPy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np

spectrum = np.fft.fft2(gray)

shifted = np.fft.fftshift(spectrum)

magnitude = np.log1p(
    np.abs(shifted)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fft2()&lt;/code&gt; computes the two-dimensional Fourier transform.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fftshift()&lt;/code&gt; moves the zero-frequency component to the center.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abs()&lt;/code&gt; obtains the magnitude.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;log1p()&lt;/code&gt; makes the magnitude spectrum easier to visualize.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Periodic interference may appear as distinct peaks in the Fourier spectrum.&lt;/p&gt;
&lt;h4 id=&quot;removing-specific-frequencies&quot;&gt;Removing Specific Frequencies&lt;/h4&gt;
&lt;p&gt;If we know which frequencies correspond to interference, we can suppress them using a notch filter.&lt;/p&gt;
&lt;p&gt;A notch filter removes selected frequency regions while preserving much of the remaining spectrum.&lt;/p&gt;
&lt;p&gt;For real-valued images, the Fourier spectrum has conjugate symmetry.&lt;/p&gt;
&lt;p&gt;This means a sinusoidal interference component generally produces a symmetric pair of frequency peaks.&lt;/p&gt;
&lt;p&gt;Both should be handled to preserve the symmetry of the filtered spectrum.&lt;/p&gt;
&lt;p&gt;The basic operation is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;filtered_spectrum = shifted * mask
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;mask&lt;/code&gt; contains zeros at the frequencies we want to suppress and ones elsewhere.&lt;/p&gt;
&lt;p&gt;We then reconstruct the image using the inverse Fourier transform.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;restored = np.fft.ifft2(
    np.fft.ifftshift(filtered_spectrum)
).real
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;an-important-limitation&quot;&gt;An Important Limitation&lt;/h4&gt;
&lt;p&gt;In this project, the periodic noise is generated with known frequencies.&lt;/p&gt;
&lt;p&gt;Therefore, the notch filter already knows where the unwanted frequency components are located.&lt;/p&gt;
&lt;p&gt;This is intentional: it lets us isolate and study the effect of frequency-domain filtering.&lt;/p&gt;
&lt;p&gt;Automatically identifying unknown interference frequencies in arbitrary photographs is a separate problem and is not implemented here.&lt;/p&gt;
&lt;p&gt;A notch filter can also remove useful image information if the rejected frequencies overlap with actual image content.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; The Fourier transform provides another perspective on image processing, allowing us to manipulate structures according to their spatial frequencies rather than their pixel positions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-k-means-unsupervised-color-clustering&quot;&gt;4. K-Means: Unsupervised Color Clustering&lt;/h3&gt;
&lt;p&gt;The final technique explores unsupervised learning.&lt;/p&gt;
&lt;p&gt;Unlike classification models, K-means does not require predefined class labels.&lt;/p&gt;
&lt;p&gt;Instead, it groups observations based on similarity.&lt;/p&gt;
&lt;p&gt;For an RGB image, each pixel can be represented as a three-dimensional vector:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Pixel = [Red, Green, Blue]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;K-means attempts to divide these vectors into K clusters by minimizing the squared distances between pixels and their assigned cluster centers.&lt;/p&gt;
&lt;p&gt;The objective function is:&lt;/p&gt;
&lt;p&gt;J = Σ ||xᵢ - μ(clusterᵢ)||²&lt;/p&gt;
&lt;p&gt;where xᵢ is a pixel vector and μ is the corresponding cluster center.&lt;/p&gt;
&lt;h4 id=&quot;implementing-k-means-with-opencv&quot;&gt;Implementing K-Means with OpenCV&lt;/h4&gt;
&lt;p&gt;First, reshape the image into a collection of RGB vectors:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pixels = image.reshape(
    -1, 3
).astype(np.float32)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run K-means:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;criteria = (
    cv2.TERM_CRITERIA_EPS +
    cv2.TERM_CRITERIA_MAX_ITER,
    25,
    0.2
)

_, labels, centers = cv2.kmeans(
    pixels,
    4,
    None,
    criteria,
    5,
    cv2.KMEANS_PP_CENTERS
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can reconstruct an image by replacing each pixel with the color of its assigned cluster center.&lt;/p&gt;
&lt;p&gt;This produces a simplified image containing a small number of representative colors.&lt;/p&gt;
&lt;h4 id=&quot;color-clustering-is-not-semantic-segmentation&quot;&gt;Color Clustering Is Not Semantic Segmentation&lt;/h4&gt;
&lt;p&gt;This distinction is important.&lt;/p&gt;
&lt;p&gt;K-means only sees RGB values in this implementation.&lt;/p&gt;
&lt;p&gt;It does not understand objects, scenes, or semantic categories.&lt;/p&gt;
&lt;p&gt;Two unrelated objects with similar colors may belong to the same cluster.&lt;/p&gt;
&lt;p&gt;Likewise, one object with multiple colors may be split across several clusters.&lt;/p&gt;
&lt;p&gt;Still, color clustering is useful for exploring image structure, reducing color complexity, and understanding unsupervised learning.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;running-the-project-locally&quot;&gt;Running the Project Locally&lt;/h3&gt;
&lt;p&gt;The project is available on GitHub:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You will need Python 3.10 or newer.&lt;/p&gt;
&lt;p&gt;Clone the repository:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/Ertugrulmutlu/from-lab-to-code.git
cd from-lab-to-code/01-image-processing
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a virtual environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m venv .venv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Activate it on Windows PowerShell:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;.\.venv\Scripts\Activate.ps1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install the dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m streamlit run app.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Streamlit will open an interactive application in your browser.&lt;/p&gt;
&lt;p&gt;You can switch between the four techniques, adjust processing parameters, and compare the results.&lt;/p&gt;
&lt;p&gt;Each mode includes a programmatically generated example image. You can also upload an image of your own.&lt;/p&gt;
&lt;p&gt;To regenerate the demonstration gallery:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python demo.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the included tests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m unittest discover -s tests -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The processing algorithms are implemented in &lt;code&gt;processing.py&lt;/code&gt;, while &lt;code&gt;synthetic.py&lt;/code&gt; generates the example images and &lt;code&gt;app.py&lt;/code&gt; contains the Streamlit interface.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-learned&quot;&gt;What I Learned&lt;/h3&gt;
&lt;p&gt;Building this project reinforced several important ideas.&lt;/p&gt;
&lt;p&gt;First, &lt;strong&gt;understanding the characteristics of a problem matters as much as selecting an algorithm&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Gaussian filtering and median filtering may look similar at first, but they behave differently depending on the noise distribution.&lt;/p&gt;
&lt;p&gt;Second, preprocessing can determine whether a simple computer vision algorithm succeeds or fails.&lt;/p&gt;
&lt;p&gt;In the object counting example, background illumination correction can be just as important as the thresholding operation itself.&lt;/p&gt;
&lt;p&gt;Third, the frequency domain provides a different way to reason about images.&lt;/p&gt;
&lt;p&gt;Some patterns that are difficult to isolate spatially become much easier to analyze after a Fourier transform.&lt;/p&gt;
&lt;p&gt;Finally, unsupervised clustering demonstrates that useful structure can be extracted from images without labeled training data, even though grouping colors is not the same as understanding a scene.&lt;/p&gt;
&lt;h3 id=&quot;why-i-started-from-lab-to-code&quot;&gt;Why I Started From Lab to Code&lt;/h3&gt;
&lt;p&gt;I wanted a lightweight way to turn engineering concepts into practical software projects.&lt;/p&gt;
&lt;p&gt;Not every project needs to become a startup, a large framework, or a long-term product.&lt;/p&gt;
&lt;p&gt;Sometimes, a small, well-explained implementation is enough to explore an idea and demonstrate what you have learned.&lt;/p&gt;
&lt;p&gt;That is the philosophy behind &lt;strong&gt;From Lab to Code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Each entry will focus on an engineering topic, build a small independent implementation, and document the underlying concepts and results.&lt;/p&gt;
&lt;p&gt;The projects are independently implemented, with original demonstration materials rather than redistributed university coursework.&lt;/p&gt;
&lt;p&gt;This first project focuses on classical image processing. Future entries may explore other engineering topics as I encounter them.&lt;/p&gt;
&lt;p&gt;If you are learning computer vision or signal processing, I hope the code and explanations help you experiment with these concepts yourself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore the series:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore the image processing project:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&quot;&gt;https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thanks for reading!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32074/from-lab-to-code-1-exploring-classical-computer-vision-with-python-and-opencv</guid>
<pubDate>Thu, 22 Oct 2026 12:53:42 +0000</pubDate>
</item>
<item>
<title># 🚀 MyZubster is looking for contributors — Let's build a community-powered marketplace!</title>
<link>https://coderlegion.com/32031/myzubster-is-looking-for-contributors-lets-build-a-community-powered-marketplace</link>
<description>&lt;h2 id=&quot;-myzubster-is-looking-for-contributors-let-s-build-a-community-powered-marketplace-&quot;&gt;🚀 MyZubster is looking for contributors — Let's build a community-powered marketplace!&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What if your GitHub contributions could become real opportunities to earn, collaborate and build independent projects?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We're building &lt;strong&gt;MyZubster&lt;/strong&gt;, an open-source ecosystem connecting developers, digital creators, freelancers and local communities.&lt;/p&gt;
&lt;p&gt;Our vision is to create a marketplace where people can:&lt;/p&gt;
&lt;p&gt;💻 Offer digital skills — programming, UI/UX, AI, DevOps, cybersecurity and blockchain development.&lt;/p&gt;
&lt;p&gt;🤝 Exchange services, collaborate on projects and support volunteer initiatives.&lt;/p&gt;
&lt;p&gt;₿ Explore payments using Bitcoin (BTC), Ethereum (ETH), Monero (XMR) and traditional currencies.&lt;/p&gt;
&lt;p&gt;🔐 Help design secure multisig escrow solutions, with legal and regulatory compliance considered from the beginning.&lt;/p&gt;
&lt;p&gt;🌍 Connect GitHub contributions, digital portfolios and independent community projects.&lt;/p&gt;
&lt;h4 id=&quot;-we-need-your-ideas-not-just-your-code-&quot;&gt;🧠 We need your ideas, not just your code!&lt;/h4&gt;
&lt;p&gt;We've opened a community discussion to explore how MyZubster can become a practical, sustainable and legally compliant marketplace.&lt;/p&gt;
&lt;p&gt;We're looking for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developers and open-source contributors&lt;/li&gt;
&lt;li&gt;Blockchain and cryptocurrency specialists&lt;/li&gt;
&lt;li&gt;UI/UX designers&lt;/li&gt;
&lt;li&gt;Privacy and security researchers&lt;/li&gt;
&lt;li&gt;Legal, fintech and compliance professionals&lt;/li&gt;
&lt;li&gt;Freelancers interested in selling digital skills&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Some features are already under development; others are proposals we want to design and validate together.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We especially want to explore how contributors can offer their digital services through the marketplace, how crypto payments should work, and how to reduce legal and technical risks for everyone involved.&lt;/p&gt;
&lt;h4 id=&quot;-join-the-discussion&quot;&gt;🔗 Join the discussion&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;GitHub community topic:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/issues/1639&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster/issues/1639&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project repository:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://myzubster.com&quot;&gt;https://myzubster.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💬 Introduce yourself, share your expertise, propose an idea or tell us what you'd like to build.&lt;/p&gt;
&lt;p&gt;You don't need to be an expert in everything. Every meaningful contribution matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Let's build something useful, open and sustainable — together.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;opensource-github-blockchain-monero-bitcoin-ethereum-marketplace-webdevelopment-freelancing-decentralization-myzubster&quot;&gt;OpenSource #GitHub #Blockchain #Monero #Bitcoin #Ethereum #Marketplace #WebDevelopment #Freelancing #Decentralization #MyZubster&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32031/myzubster-is-looking-for-contributors-lets-build-a-community-powered-marketplace</guid>
<pubDate>Thu, 22 Oct 2026 05:55:41 +0000</pubDate>
</item>
<item>
<title>Costcodle: a daily price guessing game</title>
<link>https://coderlegion.com/32011/costcodle-a-daily-price-guessing-game</link>
<description>&lt;p&gt;Plenty of people walk the aisles of a Costco warehouse and guess the price of a bulk pack before they look at the tag. Wordle showed that one small puzzle a day, the same for everyone and finished in about a minute, can quietly turn into a habit. I put those two things together.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://costcodle.org/&quot;&gt;Costcodle&lt;/a&gt; is a free daily Costco price guessing game. Every day everyone gets the same Costco item and six guesses to land within 5% of its price. A new item starts at 00:00 UTC. It runs in the browser, there is no account, and your stats stay on your device.&lt;/p&gt;
&lt;h4 id=&quot;how-a-round-works&quot;&gt;How a round works&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;One item.&lt;/strong&gt; You see a product photo, its name and the pack size. Bulk sizes are part of the challenge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A typed guess.&lt;/strong&gt; Enter a dollar amount like 12.99 and press Submit. Anything from $0.01 to $999,999.99 with at most two decimals is accepted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A receipt for feedback.&lt;/strong&gt; Each guess prints a new line on a receipt. Higher or Lower tells you which way to go. Within 25% of the price, the line is marked Close.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A 5% target.&lt;/strong&gt; Land within 5% of the price, edges included, and you win. Use all six guesses without getting there and the receipt reveals the answer.&lt;/p&gt;
&lt;p&gt;The prices are snapshots collected for the original Costcodle game, so treat them as a fun reference rather than today's shelf price. Real prices change over time and differ by location.&lt;/p&gt;
&lt;h4 id=&quot;a-real-game-looks-like-this&quot;&gt;A real game looks like this&lt;/h4&gt;
&lt;p&gt;Here is puzzle #1122 from October 15, 2026, an Organic Crimini Mushrooms 24 oz pack. It is a past puzzle with a published answer, so nothing here spoils a live game.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Guess $4.00. The receipt says Higher. The guess is 33% off, outside the Close band.&lt;/li&gt;
&lt;li&gt;Guess $7.00. The receipt says Close and Lower. Now it is 17% off.&lt;/li&gt;
&lt;li&gt;Guess $6.00. Win.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The price was $5.99, and the winning range was $5.70 to $6.28. Solved in 3 of 6 guesses. This exact game is also the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=tAYakKbNzcs&quot;&gt;30-second walkthrough video&lt;/a&gt; if you would rather watch it than read it.&lt;/p&gt;
&lt;h4 id=&quot;three-ways-to-play&quot;&gt;Three ways to play&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;The daily puzzle&lt;/strong&gt; is the main event. One item for the whole world each day, with your games played, win rate, streaks and guess distribution saved in your browser. Once you finish, the Stats panel also shows how everyone did on today's item: players, win rate and the spread of wins by number of guesses.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://costcodle.org/unlimited&quot;&gt;Costcodle Unlimited&lt;/a&gt;&lt;/strong&gt; deals random past items one after another, with the same six guesses and the same 5% rule. It never touches your daily streak, so it works as a warm-up or as a way to keep going after the daily game.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://costcodle.org/answers&quot;&gt;Costcodle answers&lt;/a&gt;&lt;/strong&gt; lists the published answers of past puzzles, each with its game price and winning range.&lt;/p&gt;
&lt;p&gt;When you finish, you can share your receipt as a short grid: one row per guess, colored for Win, Close or Far, with an arrow for Higher or Lower. The price is never in it, so friends can still play the same item.&lt;/p&gt;
&lt;h4 id=&quot;stack-notes-for-the-engineers-in-the-room&quot;&gt;Stack notes for the engineers in the room&lt;/h4&gt;
&lt;p&gt;A price game has one secret to keep, and the browser is the wrong place to keep it. Most of the engineering went into that and into getting the rules exactly right.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Integer cents everywhere.&lt;/strong&gt; Every amount is an integer number of cents. The win check is written as &lt;code&gt;|guess - price| * 100 &amp;lt;= price * 5&lt;/code&gt;, so the 5% edge is exact with no floating point in the way. On #1122, $5.70 wins and $5.69 is only Close; $6.28 wins and $6.29 is Close again.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The answer stays on the server.&lt;/strong&gt; Each guess is a POST that the server judges. A game in progress lives in a token signed with HMAC-SHA-256, so the server keeps no session table, and only the response that ends the game, win or loss, carries the price. The page HTML, the JavaScript bundle and the structured data never contain today's price, and a build check fails if a file a browser can download carries question bank data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practice never leaks the daily game.&lt;/strong&gt; Unlimited deals only puzzles that are over: never today's, never a future one, and never yesterday's while its grace window is still open. That rule is one function in the rules package, so the daily game and the practice mode cannot disagree about it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A schedule in UTC.&lt;/strong&gt; Puzzle numbers are computed from an anchor (October 15, 2026 is #1122) plus whole UTC days. If you started a game before midnight, you have a 10 minute grace window to finish it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rules are open source.&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Costcodle/costcodle-rules&quot;&gt;costcodle-rules&lt;/a&gt; is the rules engine behind the site: guess judging, price parsing, the daily schedule and the spoiler-free share text. It is MIT licensed, pure TypeScript with no runtime dependencies, and published on npm under the same name. It is exported from the site's own repository, so it is the code the live game runs, and every example in its README is covered by a test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { evaluateGuess, winningRange } from &quot;costcodle-rules&quot;;

evaluateGuess(400, 599); // { guessCents: 400, band: &quot;far&quot;, direction: &quot;higher&quot; }
evaluateGuess(700, 599); // { guessCents: 700, band: &quot;close&quot;, direction: &quot;lower&quot; }
evaluateGuess(600, 599); // { guessCents: 600, band: &quot;win&quot;, direction: &quot;none&quot; }
winningRange(599);       // { minCents: 570, maxCents: 628 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;One Worker.&lt;/strong&gt; The site is Astro with React islands on a single Cloudflare Worker, with D1 for data and a private R2 bucket for product photos. Pages are cached at the edge under a key that includes the build version and a publication revision, so publishing a new answer page makes the old copies unreachable without purging anything. The same Worker runs three scheduled jobs: one fetches the next product photo at 23:55 UTC, one publishes the answer pages of finished puzzles just after midnight, and an hourly one retries anything that failed.&lt;/p&gt;
&lt;h4 id=&quot;pricing&quot;&gt;Pricing&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Part &lt;/th&gt;&lt;th&gt;Price &lt;/th&gt;&lt;th&gt;Account&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Daily puzzle &lt;/td&gt;&lt;td&gt;Free &lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Costcodle Unlimited &lt;/td&gt;&lt;td&gt;Free &lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Costcodle answers &lt;/td&gt;&lt;td&gt;Free &lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;costcodle-rules &lt;/td&gt;&lt;td&gt;MIT license &lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Costcodle is an independent fan game. It is not affiliated with, endorsed by, or sponsored by Costco Wholesale Corporation.&lt;/p&gt;
&lt;h4 id=&quot;your-turn&quot;&gt;Your turn&lt;/h4&gt;
&lt;p&gt;If you play a round, I would like to hear how close your first guess was. And for the developers here: would you set the Close band at 25%, or is that too generous for a game with a 5% target?&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/32011/costcodle-a-daily-price-guessing-game</guid>
<pubDate>Thu, 22 Oct 2026 01:40:36 +0000</pubDate>
</item>
<item>
<title>MyZubster Dev Update: Local AI, GitHub Onboarding &amp; Decentralized Collaboration 🚀</title>
<link>https://coderlegion.com/31922/myzubster-dev-update-local-ai-github-onboarding-decentralized-collaboration</link>
<description>&lt;h2 id=&quot;myzubster-dev-update-local-ai-github-onboarding-amp-decentralized-collaboration-&quot;&gt;MyZubster Dev Update: Local AI, GitHub Onboarding &amp;amp; Decentralized Collaboration 🚀&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;We're building MyZubster, an open-source ecosystem connecting developers, independent projects, services, and communities.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Today we made significant progress on &lt;strong&gt;Zorgax&lt;/strong&gt;, our locally hosted AI assistant.&lt;/p&gt;
&lt;p&gt;Our goal is simple: make it easier for anyone — including people with no programming experience — to join GitHub, contribute to projects, and become part of a collaborative ecosystem.&lt;/p&gt;
&lt;h3 id=&quot;-what-we-built-and-tested&quot;&gt;🔧 What We Built and Tested&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Vercel ↔ VPS Connectivity&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We restored communication between our Vercel Production deployment and the VPS backend using a signed HTTPS gateway.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Production assistant endpoint responding successfully&lt;/li&gt;
&lt;li&gt;HTTP 502 connectivity issue resolved in our tests&lt;/li&gt;
&lt;li&gt;Node.js gateway operational&lt;/li&gt;
&lt;li&gt;MongoDB connected&lt;/li&gt;
&lt;li&gt;Local Ollama inference available&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. GitHub Contributor Onboarding&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We implemented a deterministic onboarding guide in Zorgax.&lt;/p&gt;
&lt;p&gt;The assistant can now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Guide new users to GitHub registration&lt;/li&gt;
&lt;li&gt;Recognize completion messages such as &lt;code&gt;FATTO&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Ask for a GitHub username&lt;/li&gt;
&lt;li&gt;Recognize that username within the conversation&lt;/li&gt;
&lt;li&gt;Advance to Git and GitHub CLI setup&lt;/li&gt;
&lt;li&gt;Avoid claiming an account has been verified without evidence&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. Real Production Testing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Our regression tests passed: &lt;strong&gt;4/4&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We also tested the public Zorgax chat and confirmed that the assistant correctly advanced from GitHub registration to the username and tooling stages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Local AI Architecture&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Our system currently combines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js backend&lt;/li&gt;
&lt;li&gt;Vercel frontend and gateway&lt;/li&gt;
&lt;li&gt;VPS-hosted services&lt;/li&gt;
&lt;li&gt;Ollama with &lt;code&gt;qwen2.5:3b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Deterministic onboarding rules&lt;/li&gt;
&lt;li&gt;MongoDB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach helps us keep sensitive onboarding workflows predictable while using local AI for more flexible assistance.&lt;/p&gt;
&lt;h3 id=&quot;-what-still-needs-work&quot;&gt;🐛 What Still Needs Work&lt;/h3&gt;
&lt;p&gt;Our tests also revealed that Zorgax can lose track of the onboarding sequence when it falls back to Ollama.&lt;/p&gt;
&lt;p&gt;For example, after Git and GitHub CLI version checks, it may answer a different MyZubster-related question instead of continuing with GitHub authentication.&lt;/p&gt;
&lt;p&gt;We're working toward a complete flow:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GitHub signup → username → Git/CLI checks → authentication → repository → first pull request&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;We want every step to be clear, verifiable, and beginner-friendly.&lt;/p&gt;
&lt;h3 id=&quot;-beyond-a-traditional-marketplace&quot;&gt;🌍 Beyond a Traditional Marketplace&lt;/h3&gt;
&lt;p&gt;MyZubster is also exploring ways to connect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Independent developer projects&lt;/li&gt;
&lt;li&gt;Skills and services through a marketplace&lt;/li&gt;
&lt;li&gt;Contributor profiles and verifiable contributions&lt;/li&gt;
&lt;li&gt;Local community initiatives&lt;/li&gt;
&lt;li&gt;Decentralized infrastructure and autonomous nodes&lt;/li&gt;
&lt;li&gt;Privacy-conscious payments and collaboration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our longer-term vision is a federated ecosystem where contributors can develop their own projects while remaining connected to a broader network.&lt;/p&gt;
&lt;p&gt;These are ongoing development goals, not claims that every feature is already production-ready.&lt;/p&gt;
&lt;h3 id=&quot;-looking-for-contributors&quot;&gt;🤝 Looking for Contributors&lt;/h3&gt;
&lt;p&gt;We're particularly interested in developers who want to help with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI-assisted GitHub onboarding&lt;/li&gt;
&lt;li&gt;Node.js and API testing&lt;/li&gt;
&lt;li&gt;UX/UI simplification&lt;/li&gt;
&lt;li&gt;GitHub automation&lt;/li&gt;
&lt;li&gt;Marketplace workflows&lt;/li&gt;
&lt;li&gt;VPS and decentralized infrastructure&lt;/li&gt;
&lt;li&gt;Open-source documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don't need to build everything. Even a small, well-tested contribution can move the project forward.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore MyZubster:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com&quot;&gt;https://www.myzubster.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try Zorgax:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/zorgax&quot;&gt;https://www.myzubster.com/zorgax&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem&quot;&gt;https://github.com/MyZubster-Ecosystem&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;let-s-collaborate&quot;&gt;Let's collaborate&lt;/h4&gt;
&lt;p&gt;We're interested in connecting independent developers and projects rather than requiring everyone to work inside a single centralized system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Question for the community:&lt;/strong&gt; How would you design an AI onboarding assistant that guides a complete beginner from creating a GitHub account to opening their first pull request?&lt;/p&gt;
&lt;h2 id=&quot;opensource-ai-github-nodejs-decentralization-developercommunity&quot;&gt;OpenSource #AI #GitHub #NodeJS #Decentralization #DeveloperCommunity&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31922/myzubster-dev-update-local-ai-github-onboarding-decentralized-collaboration</guid>
<pubDate>Wed, 21 Oct 2026 05:38:10 +0000</pubDate>
</item>
<item>
<title>Inside OpenPLC Runtime v4: Establishing a Security Research Baseline</title>
<link>https://coderlegion.com/31735/inside-openplc-runtime-v4-establishing-a-security-research-baseline</link>
<description>&lt;blockquote&gt;&lt;p&gt;An architectural investigation into the processes, communication interfaces, and Linux environment surrounding a software PLC runtime.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It has been a little over a month since I wrapped up my industrial protocol research series. At the time, I said something new was coming. Here it is.&lt;/p&gt;
&lt;p&gt;The protocol series taught me a lot about how industrial systems communicate. For this next stage, I wanted to look at a different part of the picture... the PLC runtime itself.&lt;/p&gt;
&lt;p&gt;I chose OpenPLC Runtime v4 as the first target for a new series of PLC security investigations. Before testing individual security properties, I wanted to understand what was running, how its components interacted, and what the surrounding Linux environment allowed them to do. That became the first investigation: establishing a baseline.&lt;/p&gt;
&lt;h3 id=&quot;why-start-with-architecture-&quot;&gt;Why start with architecture?&lt;/h3&gt;
&lt;p&gt;A PLC runtime sits between control logic and the environment in which that logic executes. Its behavior depends on more than the code that runs inside it. Processes, local communication channels, loaded components, operating system permissions, and resource limits can all affect how it operates.&lt;/p&gt;
&lt;p&gt;I wanted to answer a few practical questions before moving into deeper testing.&lt;/p&gt;
&lt;p&gt;What processes make up the runtime? Which communication interfaces are visible? What privileges and operating system resources are available to the processes? How does the runtime behave when one of its components exits unexpectedly?&lt;/p&gt;
&lt;p&gt;These questions helped define the scope. I was not trying to find a vulnerability in this first investigation. I wanted to document the environment well enough that future tests would have a reliable point of reference.&lt;/p&gt;
&lt;h3 id=&quot;the-target-and-lab&quot;&gt;The target and lab&lt;/h3&gt;
&lt;p&gt;The target was OpenPLC Runtime v4, running in an isolated Docker container on my research machine. Keeping it in a container gave me a repeatable environment to inspect without involving a physical PLC or a production control system.&lt;/p&gt;
&lt;p&gt;I started the container with the runtime image and published ports for the web interface and Modbus TCP:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -d \
  --name openplc-v4-lab \
  -p 8443:8443 \
  -p 502:502 \
  ghcr.io/autonomy-logic/openplc-runtime:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One detail matters for anyone reproducing this work: the image uses the &lt;code&gt;latest&lt;/code&gt; tag. That tag can point to a different image over time, so it should not be treated as a fixed version. For a reproducible investigation, record the image digest and runtime version used during the test.&lt;/p&gt;
&lt;p&gt;The container provided a contained target for process inspection, socket enumeration, filesystem review, and observation of Linux process state. I used standard Linux tools and direct inspection of the container's &lt;code&gt;/proc&lt;/code&gt;, &lt;code&gt;/sys&lt;/code&gt;, and filesystem interfaces.&lt;/p&gt;
&lt;p&gt;The investigation was divided into seven phases. Each phase examined a different part of the running environment, with observations recorded in the project notes.&lt;/p&gt;
&lt;h3 id=&quot;reconstructing-the-runtime&quot;&gt;Reconstructing the runtime&lt;/h3&gt;
&lt;h4 id=&quot;1-the-processes&quot;&gt;1. The processes&lt;/h4&gt;
&lt;p&gt;The first useful result was a clearer picture of the processes involved.&lt;/p&gt;
&lt;p&gt;The container's initial process, PID 1, ran the Python web management application through its virtual environment. A separate process named &lt;code&gt;etherdog&lt;/code&gt; handled runtime supervision and exposed a control socket. The PLC execution process appeared as &lt;code&gt;plc_main&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;These process IDs describe the snapshot I inspected, not permanent identities. The PLC process received a different PID after it was restarted, which is exactly why I recorded process names and executable paths alongside the IDs.&lt;/p&gt;
&lt;p&gt;Thread-aware inspection also showed that these were not single-threaded processes. In the observed state, the management process had 11 threads, &lt;code&gt;etherdog&lt;/code&gt; had two, and &lt;code&gt;plc_main&lt;/code&gt; had four. One of the PLC process threads was named &lt;code&gt;plc_watchdog&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I then deliberately terminated &lt;code&gt;plc_main&lt;/code&gt; to observe what happened. The supervisor reported an unexpected exit with code &lt;code&gt;-9&lt;/code&gt; and attempted to restart the process. The replacement process initialized some runtime components and recreated the PLC runtime socket, but it could not find the expected &lt;code&gt;libplc_*.so&lt;/code&gt; files in its build directory. It entered the &lt;code&gt;EMPTY&lt;/code&gt; state and did not transition to &lt;code&gt;RUNNING&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This was a useful observation of the runtime's recovery behavior under that specific condition. It does not establish how every failure would be handled, but it gave me a concrete starting point for future investigations into lifecycle management and recovery.&lt;/p&gt;
&lt;h4 id=&quot;2-local-communication&quot;&gt;2. Local communication&lt;/h4&gt;
&lt;p&gt;The processes also had local communication interfaces worth examining.&lt;/p&gt;
&lt;p&gt;The web application's Python code contained a Unix-domain socket client targeting &lt;code&gt;/run/runtime/plc_runtime.socket&lt;/code&gt;. The corresponding C header defined the same socket path and specified buffer limits, including a maximum response size of 65,536 bytes.&lt;/p&gt;
&lt;p&gt;A separate Python component, &lt;code&gt;UnixLogServer&lt;/code&gt;, bound to &lt;code&gt;/run/runtime/log_runtime.socket&lt;/code&gt;. Its implementation accepted local clients and handled incoming log lines.&lt;/p&gt;
&lt;p&gt;The supervisor used another socket path, &lt;code&gt;/run/runtime/etherdog.socket&lt;/code&gt;, for its control interface. The socket's presence and filesystem permissions were observable, while the exact authorization behavior of every communication path was outside the scope of this baseline.&lt;/p&gt;
&lt;p&gt;These findings established that Unix-domain sockets were part of the runtime's internal communication design. They also gave me concrete paths and implementation files to revisit when investigating message handling or access control later.&lt;/p&gt;
&lt;h4 id=&quot;3-network-listeners&quot;&gt;3. Network listeners&lt;/h4&gt;
&lt;p&gt;I used &lt;code&gt;ss&lt;/code&gt; to inspect the network sockets visible inside the container's network namespace.&lt;/p&gt;
&lt;p&gt;The snapshot showed two listeners or bound sockets associated with the Python management process:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Protocol &lt;/th&gt;&lt;th&gt;Local address &lt;/th&gt;&lt;th&gt;Observed state&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;TCP &lt;/td&gt;&lt;td&gt;&lt;code&gt;0.0.0.0:8443&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Listening&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;UDP &lt;/td&gt;&lt;td&gt;&lt;code&gt;0.0.0.0:33333&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Bound, no connected peer&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both were associated with file descriptors held by PID 1. The wildcard addresses mean the sockets were bound across the container's available network interfaces, rather than to a single container interface address.&lt;/p&gt;
&lt;p&gt;That observation alone does not establish external reachability. Docker port publishing, host firewall rules, routing, and other network conditions affect whether a service can be reached from outside the container.&lt;/p&gt;
&lt;p&gt;I had published TCP port 502 when starting the container, but the inspection snapshot did not show a Modbus TCP listener on that port. Publishing a port in Docker does not prove that an application is listening behind it. I recorded the result as a finding about this particular runtime state, rather than a universal statement about OpenPLC Runtime v4.&lt;/p&gt;
&lt;p&gt;I did not test web routes, authentication, or the behavior of the exposed services in this phase. Those questions require separate, scoped investigations.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/404saint/plc-security-research/main/plc-runtime-architecture/diagrams/diagram1.png&quot; alt=&quot;PLC Runtime Architecture Diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 1. A view of the observed runtime processes, local communication paths, and network interfaces. The diagram summarizes the lab snapshot; it should not be read as a complete specification of every internal connection.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-runtime-s-linux-environment&quot;&gt;The runtime's Linux environment&lt;/h3&gt;
&lt;p&gt;Knowing which processes existed was only part of the picture. I also needed to understand what those processes could access through the operating system.&lt;/p&gt;
&lt;h4 id=&quot;1-executables-and-runtime-state&quot;&gt;1. Executables and runtime state&lt;/h4&gt;
&lt;p&gt;The main PLC executable was located at &lt;code&gt;/workdir/build/plc_main&lt;/code&gt;, while the observed supervisor executable was at &lt;code&gt;/workdir/build/etherdog&lt;/code&gt;. Both were owned by &lt;code&gt;root:root&lt;/code&gt; and had mode &lt;code&gt;0755&lt;/code&gt;, allowing the owner to read, write, and execute them, while group members and other users had read and execute permissions.&lt;/p&gt;
&lt;p&gt;The inspected runtime directory contained several relevant files and interfaces. These included &lt;code&gt;/run/runtime/.env&lt;/code&gt;, the supervisor's JSON file, the SQLite database at &lt;code&gt;/run/runtime/restapi.db&lt;/code&gt;, and the Unix-domain sockets described earlier.&lt;/p&gt;
&lt;p&gt;I checked the permissions and metadata of these paths without reading the contents of &lt;code&gt;.env&lt;/code&gt; or inspecting the database records. The database file had a SQLite header, which identified its format, but its contents were not part of this investigation.&lt;/p&gt;
&lt;p&gt;This matters since a file's name, format, ownership, and mode tell us something about how it is exposed in the environment, but they do not tell us what data it contains or whether the application uses every file in the way its name suggests.&lt;/p&gt;
&lt;h4 id=&quot;2-capabilities-file-descriptors-and-devices&quot;&gt;2. Capabilities, file descriptors, and devices&lt;/h4&gt;
&lt;p&gt;I inspected the Linux capabilities of the management and PLC processes. The observed capability sets included &lt;code&gt;CAP_NET_RAW&lt;/code&gt; and did not include &lt;code&gt;CAP_SYS_RAWIO&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Capabilities are one part of a process's effective authority. They help describe which privileged operations may be available, but they do not independently establish whether a particular operation is reachable through the application's code or permitted by other system controls.&lt;/p&gt;
&lt;p&gt;I also reviewed open file descriptors and correlated socket inodes with entries in &lt;code&gt;/proc/net/unix&lt;/code&gt;, &lt;code&gt;/proc/net/tcp&lt;/code&gt;, and &lt;code&gt;/proc/net/udp&lt;/code&gt;. This connected several of the observed descriptors to the runtime's local sockets and network endpoints. The management process also had the database open.&lt;/p&gt;
&lt;p&gt;Some file descriptors referred to pipes shared between processes. That showed that the processes held descriptors for the same underlying pipe objects. It did not, by itself, establish the purpose or contents of every message passing through them.&lt;/p&gt;
&lt;p&gt;The captured &lt;code&gt;/dev&lt;/code&gt; listing contained standard Linux device nodes, including &lt;code&gt;/dev/null&lt;/code&gt;, &lt;code&gt;/dev/zero&lt;/code&gt;, &lt;code&gt;/dev/random&lt;/code&gt;, and terminal-related entries. I did not see obvious dedicated GPIO, SPI, or I²C device nodes in that listing.&lt;/p&gt;
&lt;p&gt;That is a limited observation. It does not prove that hardware access is impossible, or that every possible I/O mechanism is absent. The runtime may use software backends, network interfaces, other mounted resources, or interfaces that were not identifiable from this snapshot.&lt;/p&gt;
&lt;p&gt;For this investigation, the useful result was a record of the capabilities, descriptors, and device nodes visible in the environment. Whether a particular interface can be used to influence physical I/O remains a separate question.&lt;/p&gt;
&lt;h4 id=&quot;3-memory-mappings-and-loaded-components&quot;&gt;3. Memory mappings and loaded components&lt;/h4&gt;
&lt;p&gt;I inspected the PLC process's memory map through &lt;code&gt;/proc&lt;/code&gt;. The main executable appeared in separate read-only, executable, and writable mappings. The process also had a heap, anonymous memory regions, a stack mapping, and mappings for shared libraries.&lt;/p&gt;
&lt;p&gt;Two plugin libraries were particularly relevant to the runtime's industrial focus: &lt;code&gt;libethercat_plugin.so&lt;/code&gt; and &lt;code&gt;libs7comm_plugin.so&lt;/code&gt;. Both appeared in the process's mappings, alongside the C runtime, OpenSSL libraries, Python libraries, and native Python extensions.&lt;/p&gt;
&lt;p&gt;This gave me evidence that these plugin libraries were mapped into the process at the time of inspection. It did not establish which plugin functions were active, what protocol operations were being performed, or how the runtime handled specific industrial messages.&lt;/p&gt;
&lt;p&gt;Anonymous mappings were recorded as anonymous mappings. I did not assign them to particular runtime structures because the mapping information alone was insufficient to identify their contents reliably.&lt;/p&gt;
&lt;p&gt;The snapshot was also taken before deploying user control logic. That boundary is important: the memory layout of a process running an actual project may differ from the baseline observed here.&lt;/p&gt;
&lt;h4 id=&quot;4-scheduling-and-resource-limits&quot;&gt;4. Scheduling and resource limits&lt;/h4&gt;
&lt;p&gt;The final phase examined how the PLC process was scheduled and what resource limits were visible at the inspected cgroup level.&lt;/p&gt;
&lt;p&gt;The PLC process was using &lt;code&gt;SCHED_OTHER&lt;/code&gt; with priority zero. The four listed threads were also using normal time-sharing scheduling. The process affinity mask was &lt;code&gt;fff&lt;/code&gt;, indicating an allowed CPU mask covering the corresponding CPU bits. In this environment, that mask represented CPUs 0 through 11.&lt;/p&gt;
&lt;p&gt;The inspected cgroup files reported:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;cpu.max     max 100000
memory.max  max
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These values indicated that the inspected cgroup level did not specify a CPU quota or a maximum memory limit through those files. They do not mean the process had unlimited resources. Parent cgroups, Docker settings, host capacity, and contention can still constrain it.&lt;/p&gt;
&lt;p&gt;During runtime observation, the watchdog also logged a failed attempt to set &lt;code&gt;SCHED_FIFO&lt;/code&gt; priority 99, with the error &lt;code&gt;Operation not permitted&lt;/code&gt;. That confirmed the scheduling request failed in this environment. It did not establish the runtime's timing accuracy, cycle jitter, or behavior under load. I did not measure real-time performance in this investigation. The scheduling and resource observations provide context for that work if it becomes relevant later.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/404saint/plc-security-research/main/plc-runtime-architecture/diagrams/diagram2.png&quot; alt=&quot;OpenPLC Runtime v4 within its observed Linux execution environment.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 2. A summary of the process environment, loaded components, visible operating system interfaces, and scheduling configuration observed during the investigation. These details describe the captured lab state, not every possible deployment configuration.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-this-baseline-establishes&quot;&gt;What this baseline establishes&lt;/h3&gt;
&lt;p&gt;By the end of the investigation, I had a working map of the environment around the runtime.&lt;/p&gt;
&lt;p&gt;I identified the principal processes and their observed thread counts. I documented the local socket paths and the network sockets visible in the container. I recorded relevant executable and state-file permissions, inspected capabilities and open descriptors, and reviewed the PLC process's memory mappings. The scheduling and cgroup checks added context about the execution environment.&lt;/p&gt;
&lt;p&gt;The unexpected termination test also showed a specific recovery path. The supervisor detected the PLC process exit and attempted to restart it, but the replacement did not reach the &lt;code&gt;RUNNING&lt;/code&gt; state because expected shared libraries were not found in the inspected build directory.&lt;/p&gt;
&lt;p&gt;These results give later investigations something concrete to compare against. If a subsequent test changes process behavior, introduces a new interface, or produces an unexpected failure, there is now a baseline to help distinguish the new observation from the environment that was already present.&lt;/p&gt;
&lt;p&gt;The detailed phase notes, commands, paths, and supporting observations are available in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/404saint/plc-security-research/blob/main/plc-runtime-architecture/&quot;&gt;plc-runtime-architecture repository&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;where-the-research-goes-next&quot;&gt;Where the research goes next&lt;/h3&gt;
&lt;p&gt;This first investigation closes with a baseline rather than a vulnerability investigation. I now have a better understanding of the processes surrounding OpenPLC Runtime v4, the interfaces they expose, and the Linux environment in which they run.&lt;/p&gt;
&lt;p&gt;The next investigations can build on that knowledge and examine individual security properties in more detail. Each one will need its own question, evidence, and limits.&lt;/p&gt;
&lt;p&gt;After spending so much time examining how industrial protocols communicate, I am looking forward to digging into the software that makes a PLC runtime operate. Stay tuned.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;previously-industrial-protocol-research&quot;&gt;Previously: Industrial Protocol Research&lt;/h3&gt;
&lt;p&gt;If you're new to my work, you can explore my previous industrial protocol research series, where I investigated how protocols such as Modbus, DNP3, OPC UA, EtherNet/IP, PROFINET, and S7comm work under the hood.&lt;/p&gt;
&lt;p&gt;The series covered protocol structure, implementation behavior, communication analysis, and security characteristics through controlled lab investigations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore the research:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/404saint/industrial-protocol-labs&quot;&gt;Industrial Protocol Labs on GitHub&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31735/inside-openplc-runtime-v4-establishing-a-security-research-baseline</guid>
<pubDate>Mon, 19 Oct 2026 21:28:33 +0000</pubDate>
</item>
<item>
<title>Building the MyZubster Metaverse: From Public Characters to Secure World Actions</title>
<link>https://coderlegion.com/31679/building-the-myzubster-metaverse-from-public-characters-to-secure-world-actions</link>
<description>&lt;p&gt;Building the MyZubster Metaverse: From Public Characters to Secure World Actions&lt;br&gt;
MyZubster is building an open-source metaverse that connects public characters, verified knowledge, contributor identities, realtime interactions, and GitHub-based development.&lt;br&gt;
The project is not limited to a graphical virtual world. Its technical foundation is based on authenticated actions, explicit user consent, verifiable evidence, secure automation, and independently inspectable deployments.&lt;br&gt;
This is a technical update on what we have implemented, what we recently stabilized, and what remains before the platform can be considered production-ready.&lt;br&gt;
The Architecture&lt;br&gt;
The current architecture follows this general flow:&lt;br&gt;
MyZubster account&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authenticated session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Time-limited session lease&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zorgax or metaverse character&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authorized world action&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Realtime event distribution&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Persistent and verifiable evidence&lt;br&gt;
Each state-changing request should answer four questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Which account initiated the request?&lt;/li&gt;
&lt;li&gt;Is its session still valid?&lt;/li&gt;
&lt;li&gt;Which character is associated with that session?&lt;/li&gt;
&lt;li&gt;Is the requested action permitted?&lt;br&gt;
This is important because a client-provided character ID cannot be considered proof of ownership.&lt;br&gt;
The Public Metaverse&lt;br&gt;
The public interface is available at:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse&quot;&gt;https://www.myzubster.com/metaverse&lt;/a&gt;&lt;br&gt;
Our latest production verification confirmed that the page loads correctly and exposes:&lt;ul&gt;
&lt;li&gt;25 public characters&lt;/li&gt;
&lt;li&gt;7 verified explorers&lt;/li&gt;
&lt;li&gt;A deployed Vercel production build&lt;/li&gt;
&lt;li&gt;No blocking browser-console errors&lt;br&gt;
This public page represents the visible part of a larger system. Most of our recent work has focused on the security and infrastructure beneath it.&lt;br&gt;
Session-Bound World Actions&lt;br&gt;
World actions are being migrated to a session-bound authorization model.&lt;br&gt;
The server must derive the acting identity from an authenticated session instead of trusting identifiers supplied by the client.&lt;br&gt;
const session = await resolveAuthenticatedSession(request);&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;if (!session) {&lt;br&gt;
  throw new UnauthorizedError();&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;const lease = await resolveActiveLease(session.id);&lt;/p&gt;
&lt;p&gt;if (!lease || lease.expiresAt &amp;lt;= new Date()) {&lt;br&gt;
  throw new SessionExpiredError();&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;const character = await resolveCharacterForAccount(session.accountId);&lt;/p&gt;
&lt;p&gt;await authorizeWorldAction({&lt;br&gt;
  session,&lt;br&gt;
  lease,&lt;br&gt;
  character,&lt;br&gt;
  action: request.body.action,&lt;br&gt;
});&lt;br&gt;
This approach reduces the risk of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Character impersonation&lt;/li&gt;
&lt;li&gt;Cross-account actions&lt;/li&gt;
&lt;li&gt;Replayed requests&lt;/li&gt;
&lt;li&gt;Actions performed after logout&lt;/li&gt;
&lt;li&gt;Use of expired sessions&lt;/li&gt;
&lt;li&gt;Client-side manipulation of ownership fields&lt;br&gt;
Authentication identifies the account. The session lease determines whether its authorization is still active. The policy layer determines which world actions are allowed.&lt;br&gt;
Session Lease Lifecycle&lt;br&gt;
An authenticated session should not provide unlimited access.&lt;br&gt;
We introduced a lease lifecycle that can represent the following states:&lt;br&gt;
CREATED&lt;br&gt;
 ↓&lt;br&gt;
ACTIVE&lt;br&gt;
 ↓&lt;br&gt;
RENEWED&lt;br&gt;
 ↓&lt;br&gt;
EXPIRED&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ACTIVE&lt;br&gt;
   ↓&lt;br&gt;
REVOKED&lt;br&gt;
A lease can be invalidated when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The expiration time is reached&lt;/li&gt;
&lt;li&gt;The user signs out&lt;/li&gt;
&lt;li&gt;The account is disabled&lt;/li&gt;
&lt;li&gt;Security credentials are rotated&lt;/li&gt;
&lt;li&gt;An administrator revokes the session&lt;/li&gt;
&lt;li&gt;A security policy invalidates older leases&lt;br&gt;
Metaverse endpoints must verify the lease immediately before performing a protected action.&lt;br&gt;
Zorgax and Contributor Consent&lt;br&gt;
Zorgax connects several areas of MyZubster:&lt;/li&gt;
&lt;li&gt;GitHub contributors&lt;/li&gt;
&lt;li&gt;Public professional profiles&lt;/li&gt;
&lt;li&gt;Knowledge Cards&lt;/li&gt;
&lt;li&gt;Verified Knowledge&lt;/li&gt;
&lt;li&gt;Metaverse characters&lt;/li&gt;
&lt;li&gt;Development Requests&lt;br&gt;
A public GitHub identity can generate an optional character proposal, but registration must require explicit consent.&lt;br&gt;
GitHub contribution&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Character draft&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Contributor reviews proposal&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Explicit acceptance&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Public character registration&lt;br&gt;
A character proposal does not imply:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Legal identity verification&lt;/li&gt;
&lt;li&gt;Employment&lt;/li&gt;
&lt;li&gt;Partnership&lt;/li&gt;
&lt;li&gt;Ownership&lt;/li&gt;
&lt;li&gt;Payment eligibility&lt;/li&gt;
&lt;li&gt;Institutional endorsement&lt;br&gt;
Keeping these distinctions explicit is essential for an open contributor ecosystem.&lt;br&gt;
Realtime Communication&lt;br&gt;
The realtime layer still needs to be completed, but its event structure must support reliable synchronization.&lt;br&gt;
A typical event could look like this:&lt;br&gt;
{&lt;br&gt;
&quot;eventId&quot;: &quot;evt&lt;em&gt;01...&quot;,&lt;br&gt;
&quot;worldId&quot;: &quot;myzubster-main&quot;,&lt;br&gt;
&quot;characterId&quot;: &quot;character&lt;/em&gt;...&quot;,&lt;br&gt;
&quot;sessionId&quot;: &quot;session_...&quot;,&lt;br&gt;
&quot;sequence&quot;: 1842,&lt;br&gt;
&quot;type&quot;: &quot;character.position.updated&quot;,&lt;br&gt;
&quot;occurredAt&quot;: &quot;2026-10-19T04:50:00.000Z&quot;,&lt;br&gt;
&quot;payload&quot;: {&lt;br&gt;
  &quot;x&quot;: 12.4,&lt;br&gt;
  &quot;y&quot;: 0,&lt;br&gt;
  &quot;z&quot;: -8.1&lt;br&gt;
}&lt;br&gt;
}&lt;br&gt;
The event identifier and sequence number allow clients to detect:&lt;/li&gt;
&lt;li&gt;Duplicate messages&lt;/li&gt;
&lt;li&gt;Out-of-order updates&lt;/li&gt;
&lt;li&gt;Missing events&lt;/li&gt;
&lt;li&gt;Stale world state&lt;br&gt;
After reconnecting, a client should send its last accepted sequence number and recover any missing state.&lt;br&gt;
Distributed Rate Limiting&lt;br&gt;
In-memory rate limiting works only for a single application instance. It is insufficient when requests are distributed across several workers or serverless deployments.&lt;br&gt;
Production enforcement therefore requires shared state:&lt;br&gt;
Client&lt;br&gt;
 ↓&lt;br&gt;
Application instance A ─┐&lt;br&gt;
Application instance B ─┼→ Shared atomic rate limiter&lt;br&gt;
Application instance C ─┘&lt;br&gt;
Different policies are required for:&lt;/li&gt;
&lt;li&gt;Login attempts&lt;/li&gt;
&lt;li&gt;Session creation&lt;/li&gt;
&lt;li&gt;Lease renewal&lt;/li&gt;
&lt;li&gt;Character movement&lt;/li&gt;
&lt;li&gt;World interactions&lt;/li&gt;
&lt;li&gt;Realtime messages&lt;/li&gt;
&lt;li&gt;Search endpoints&lt;/li&gt;
&lt;li&gt;Profile updates&lt;br&gt;
The system must also define what happens when the rate-limit service becomes unavailable: fail open, fail closed, or apply a limited fallback policy.&lt;br&gt;
CI Runtime Modernization&lt;br&gt;
We recently completed an important GitHub Actions upgrade.&lt;br&gt;
GitHub warned that older JavaScript actions were using the deprecated Node.js 20 runtime. The affected references were updated from:&lt;/li&gt;
&lt;li&gt;uses: actions/checkout@v4&lt;/li&gt;
&lt;li&gt;uses: actions/setup-node@v4&lt;br&gt;
to:&lt;/li&gt;
&lt;li&gt;uses: actions/checkout@v5&lt;/li&gt;
&lt;li&gt;uses: actions/setup-node@v5&lt;br&gt;
The migration covered:&lt;/li&gt;
&lt;li&gt;12 workflow files&lt;/li&gt;
&lt;li&gt;26 action references&lt;/li&gt;
&lt;li&gt;CI and lint&lt;/li&gt;
&lt;li&gt;Security auditing&lt;/li&gt;
&lt;li&gt;SBOM generation&lt;/li&gt;
&lt;li&gt;Vercel deployment&lt;/li&gt;
&lt;li&gt;Evidence validation&lt;/li&gt;
&lt;li&gt;Seller policy regression testing&lt;/li&gt;
&lt;li&gt;MYZ ledger verification&lt;/li&gt;
&lt;li&gt;Pilot API testing&lt;/li&gt;
&lt;li&gt;Decentraland SDK7 proof workflows&lt;br&gt;
We validated all 16 workflow YAML files before publishing the change and confirmed that no affected v4 references remained.&lt;br&gt;
The update was integrated through Pull Request #38.&lt;br&gt;
The final commit on main is:&lt;br&gt;
69ef67716df50482e8fbf9960ccab7dfc37d3000&lt;br&gt;
Post-Merge Results&lt;br&gt;
The merge triggered seven workflows on the production branch.&lt;br&gt;
All completed successfully:&lt;/li&gt;
&lt;li&gt;CI — Test and Lint&lt;/li&gt;
&lt;li&gt;Security Audit&lt;/li&gt;
&lt;li&gt;SBOM and Vulnerability Scan&lt;/li&gt;
&lt;li&gt;Continuous Evidence Gate&lt;/li&gt;
&lt;li&gt;MYZ Canonical Ledger Contract&lt;/li&gt;
&lt;li&gt;MYZ-164 Seller Free Policy&lt;/li&gt;
&lt;li&gt;Deploy to Vercel&lt;br&gt;
The deployment workflow also completed successfully, confirming that the runtime migration did not break the production delivery process.&lt;br&gt;
What Is Currently Working&lt;br&gt;
The current baseline includes:&lt;/li&gt;
&lt;li&gt;Public metaverse interface&lt;/li&gt;
&lt;li&gt;Public characters&lt;/li&gt;
&lt;li&gt;Verified explorers&lt;/li&gt;
&lt;li&gt;GitHub-connected contributor identities&lt;/li&gt;
&lt;li&gt;Optional Zorgax character onboarding&lt;/li&gt;
&lt;li&gt;Session-bound authorization development&lt;/li&gt;
&lt;li&gt;Session lease lifecycle enforcement&lt;/li&gt;
&lt;li&gt;Knowledge and profile integration&lt;/li&gt;
&lt;li&gt;Automated security auditing&lt;/li&gt;
&lt;li&gt;SBOM generation&lt;/li&gt;
&lt;li&gt;Continuous evidence validation&lt;/li&gt;
&lt;li&gt;Node.js 24-compatible GitHub Actions&lt;/li&gt;
&lt;li&gt;Automated Vercel deployments&lt;br&gt;
What Still Needs to Be Built&lt;br&gt;
Complete Authentication Enforcement&lt;br&gt;
Every state-changing endpoint must use the same canonical authentication and authorization process.&lt;br&gt;
Required scenarios include:&lt;/li&gt;
&lt;li&gt;Expired sessions&lt;/li&gt;
&lt;li&gt;Revoked leases&lt;/li&gt;
&lt;li&gt;Logout&lt;/li&gt;
&lt;li&gt;Credential rotation&lt;/li&gt;
&lt;li&gt;Concurrent sessions&lt;/li&gt;
&lt;li&gt;Disabled accounts&lt;/li&gt;
&lt;li&gt;Replay attempts&lt;/li&gt;
&lt;li&gt;Cross-account character access&lt;br&gt;
Finish Distributed Rate Limiting&lt;br&gt;
We still need to validate:&lt;/li&gt;
&lt;li&gt;Atomic counters&lt;/li&gt;
&lt;li&gt;Multiple application instances&lt;/li&gt;
&lt;li&gt;Concurrent requests&lt;/li&gt;
&lt;li&gt;Counter expiration&lt;/li&gt;
&lt;li&gt;Boundary conditions&lt;/li&gt;
&lt;li&gt;Backend failure behavior&lt;/li&gt;
&lt;li&gt;Recovery after temporary outages&lt;br&gt;
Complete Realtime Synchronization&lt;br&gt;
The realtime layer must support:&lt;/li&gt;
&lt;li&gt;Presence&lt;/li&gt;
&lt;li&gt;Character movement&lt;/li&gt;
&lt;li&gt;Interactions&lt;/li&gt;
&lt;li&gt;Reconnection&lt;/li&gt;
&lt;li&gt;Missed-event recovery&lt;/li&gt;
&lt;li&gt;Duplicate suppression&lt;/li&gt;
&lt;li&gt;Event ordering&lt;/li&gt;
&lt;li&gt;Backpressure management&lt;br&gt;
Add Privacy-Safe Observability&lt;br&gt;
Operational monitoring should measure:&lt;/li&gt;
&lt;li&gt;Active sessions&lt;/li&gt;
&lt;li&gt;Rejected world actions&lt;/li&gt;
&lt;li&gt;Lease-renewal failures&lt;/li&gt;
&lt;li&gt;Realtime disconnections&lt;/li&gt;
&lt;li&gt;Rate-limit activations&lt;/li&gt;
&lt;li&gt;API latency&lt;/li&gt;
&lt;li&gt;Backend error rates&lt;br&gt;
The system should minimize personal data and avoid storing unnecessary raw identifiers.&lt;br&gt;
Reliability and Load Testing&lt;br&gt;
Before expanding public access, the platform needs tests for:&lt;/li&gt;
&lt;li&gt;Concurrent users&lt;/li&gt;
&lt;li&gt;Movement bursts&lt;/li&gt;
&lt;li&gt;Realtime fan-out&lt;/li&gt;
&lt;li&gt;Session-store latency&lt;/li&gt;
&lt;li&gt;Infrastructure restarts&lt;/li&gt;
&lt;li&gt;Partial failures&lt;/li&gt;
&lt;li&gt;Deployment rollback&lt;/li&gt;
&lt;li&gt;Rate-limit contention&lt;br&gt;
End-to-End Acceptance&lt;br&gt;
The complete flow must be tested as one system:&lt;br&gt;
Sign in&lt;br&gt;
→ select a character&lt;br&gt;
→ create a session lease&lt;br&gt;
→ enter the metaverse&lt;br&gt;
→ perform an authorized action&lt;br&gt;
→ receive a realtime event&lt;br&gt;
→ disconnect&lt;br&gt;
→ reconnect and recover state&lt;br&gt;
→ sign out&lt;br&gt;
→ verify that further actions are rejected&lt;br&gt;
WebXR&lt;br&gt;
WebXR remains part of the roadmap, but it should follow security, realtime communication, and reliability work.&lt;br&gt;
Our planned order is:&lt;ol&gt;
&lt;li&gt;Authentication and session enforcement&lt;/li&gt;
&lt;li&gt;Distributed rate limiting&lt;/li&gt;
&lt;li&gt;Realtime synchronization&lt;/li&gt;
&lt;li&gt;Privacy-safe analytics&lt;/li&gt;
&lt;li&gt;Reliability and load testing&lt;/li&gt;
&lt;li&gt;End-to-end acceptance&lt;/li&gt;
&lt;li&gt;WebXR experience&lt;br&gt;
Conclusion&lt;br&gt;
The MyZubster Metaverse now has a concrete technical foundation: public characters, contributor identity integration, authenticated session work, automated security checks, reproducible deployments, and a modernized CI runtime.&lt;br&gt;
It is not finished, and we do not want to describe experimental components as complete systems.&lt;br&gt;
Every future milestone should be accompanied by:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Public source code&lt;/li&gt;
&lt;li&gt;Automated tests&lt;/li&gt;
&lt;li&gt;Deployment evidence&lt;/li&gt;
&lt;li&gt;Clear limitations&lt;/li&gt;
&lt;li&gt;Reproducible verification steps&lt;br&gt;
Our goal is to build the metaverse transparently, incrementally, and with evidence that contributors can inspect independently.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31679/building-the-myzubster-metaverse-from-public-characters-to-secure-world-actions</guid>
<pubDate>Mon, 19 Oct 2026 04:58:09 +0000</pubDate>
</item>
<item>
<title>Is your coding agent reading the memory you wrote?</title>
<link>https://coderlegion.com/31650/is-your-coding-agent-reading-the-memory-you-wrote</link>
<description>&lt;p&gt;&lt;em&gt;I maintain Selvedge. This article was prepared with AI assistance and checked against the released Selvedge v0.3.17 package from PyPI (tag v0.3.17, commit 3b67b8f) and the repository's main branch at commit 66c51d3.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I saved one decision, read it back six ways, and got nothing from two of them. The record never moved. The reads that missed it had opened a different, empty database.&lt;/p&gt;
&lt;p&gt;This is a reproduction, not an incident report. I installed Selvedge v0.3.17 from PyPI in a fresh virtual environment, pointed &lt;code&gt;HOME&lt;/code&gt; at a throwaway directory, and logged one invented decision from a project root:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;selvedge init
selvedge log src/billing/charge.py::retry_charge reject \
  --reasoning &quot;Rejected automatic retries inside retry_charge; went with a single attempt plus an idempotency key, because a retry after a timeout can charge a customer twice.&quot; \
  --constraint &quot;A payment request must never be submitted twice for one order.&quot; \
  --stale-when &quot;the payment provider offers server-side idempotent retries&quot; \
  --agent synthetic-demo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;--agent&lt;/code&gt; only sets an attribution label.) Then I asked for that record from different places.&lt;/p&gt;
&lt;h3 id=&quot;one-record-six-reads&quot;&gt;One record, six reads&lt;/h3&gt;
&lt;p&gt;From &lt;code&gt;src/billing&lt;/code&gt;, a subdirectory of the project, &lt;code&gt;selvedge prior-attempts src/billing/charge.py::retry_charge --json&lt;/code&gt; returned the record.&lt;/p&gt;
&lt;p&gt;From an unrelated directory, the same lookup (without &lt;code&gt;--json&lt;/code&gt;) printed &quot;No prior attempts found&quot; and exited 0. By the end of the run, the throwaway home directory held a new per-user database with zero events.&lt;/p&gt;
&lt;p&gt;From that same unrelated directory with &lt;code&gt;SELVEDGE_DB=/path/to/demo-app/.selvedge/selvedge.db&lt;/code&gt; set, the command returned the record with the same ID.&lt;/p&gt;
&lt;p&gt;With the same variable set, the Python package returned the same ID:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from selvedge import SelvedgeStorage, get_db_path

store = SelvedgeStorage(get_db_path(read_only=True))
blame = store.get_blame(&quot;src/billing/charge.py::retry_charge&quot;)
print(blame[&quot;id&quot;] if blame else &quot;not found in this store&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, a small test script started &lt;code&gt;selvedge-server&lt;/code&gt; over stdio and called its &lt;code&gt;prior_attempts&lt;/code&gt; tool. With &lt;code&gt;SELVEDGE_DB&lt;/code&gt; set, it returned the same ID. Without it, from the unrelated directory, it returned an empty list.&lt;/p&gt;
&lt;p&gt;Every read that found the database returned one ID. The two misses weren't missing data; they were the wrong file.&lt;/p&gt;
&lt;h3 id=&quot;why-the-empty-answer-happened&quot;&gt;Why the empty answer happened&lt;/h3&gt;
&lt;p&gt;Selvedge stores decisions in local SQLite. In v0.3.17, the CLI and the MCP server both find that database with &lt;code&gt;get_db_path&lt;/code&gt;, which the package also exports. It checks three places, in order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the &lt;code&gt;SELVEDGE_DB&lt;/code&gt; environment variable;&lt;/li&gt;
&lt;li&gt;an existing &lt;code&gt;.selvedge/selvedge.db&lt;/code&gt; file, searched for upward from the current directory;&lt;/li&gt;
&lt;li&gt;the per-user fallback &lt;code&gt;~/.selvedge/selvedge.db&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The subdirectory read found the project file by searching upward. The unrelated directory had no project file above it, so the read fell through to the per-user path.&lt;/p&gt;
&lt;p&gt;The tool didn't hide this. A normal lookup that falls back writes a warning to stderr, once per process, unless &lt;code&gt;SELVEDGE_QUIET&lt;/code&gt; is set. Both the CLI miss and the MCP miss printed it. On its own, &lt;code&gt;get_db_path(read_only=True)&lt;/code&gt; stays silent and creates nothing. But opening that path with &lt;code&gt;SelvedgeStorage&lt;/code&gt;, as the snippet above does, still creates the per-user directory and an empty database, with no warning. In a fresh home directory without &lt;code&gt;SELVEDGE_DB&lt;/code&gt;, the snippet printed &quot;not found in this store&quot;. &lt;code&gt;selvedge doctor&lt;/code&gt; reports the resolved path and which step matched: &quot;via walked up from CWD to project .selvedge/&quot; from the project root, and &quot;via global fallback (~/.selvedge/)&quot; with status WARN from the unrelated directory. That doctor run still exited 0, so a script that checks only exit codes won't notice.&lt;/p&gt;
&lt;p&gt;An MCP server makes this easier to miss. A client starts the server with whatever working directory and environment the client chooses, and whether you see the server's stderr depends on the client. The README says to set &lt;code&gt;SELVEDGE_DB&lt;/code&gt; in the server configuration when the client starts outside your project.&lt;/p&gt;
&lt;h3 id=&quot;checking-your-own-setup-with-or-without-selvedge&quot;&gt;Checking your own setup, with or without Selvedge&lt;/h3&gt;
&lt;p&gt;None of this is specific to one tool. Any file-based memory, whether a decisions folder, a JSON file or a SQLite database, is located by each process for itself. So, before trusting an empty answer, find out how your tool picks its file. Usually it's some mix of explicit path, project discovery and a per-user default.&lt;/p&gt;
&lt;p&gt;Then ask the question from where the reader actually runs. A check in your terminal describes your terminal, not an agent client or a CI job with its own directory and environment. For processes you don't launch yourself, set the path explicitly rather than relying on discovery.&lt;/p&gt;
&lt;p&gt;When a lookup comes back empty, read it as &quot;nothing in this store,&quot; not &quot;nothing was ever decided.&quot; And once, after any change to clients or configuration, write a test record through one route and read it through the others, comparing IDs rather than text. Matching text could be a copy; a matching ID means the same record.&lt;/p&gt;
&lt;h3 id=&quot;what-s-on-main-and-not-released&quot;&gt;What's on main, and not released&lt;/h3&gt;
&lt;p&gt;PR #76, merged into main as commit 66c51d3 on October 17, 2026 (PT), reworks Selvedge's architecture plan. It describes MCP as &quot;one route to the core, not a prerequisite for using it,&quot; and plans a small local core shared by the library, CLI, MCP and optional integrations. The PR also adds a regression check that saves and retrieves a record across isolated Python processes, and rewords the bundled prompt to present CLI and MCP as routes to the same store.&lt;/p&gt;
&lt;p&gt;None of this is in v0.3.17, and the architecture document says the phases add no release dates. It notes that the current package still installs MCP dependencies, and that making them optional needs a separate compatibility assessment. It lists compact review presentation, native GitHub validation and an editor integration as future work.&lt;/p&gt;
&lt;h3 id=&quot;what-this-run-doesn-t-show&quot;&gt;What this run doesn't show&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;It's synthetic: one invented record, one machine, Python 3.13 and a throwaway home directory. The CLI's PyPI update check was turned off, and telemetry was never enabled.&lt;/li&gt;
&lt;li&gt;No agent client was tested. The MCP route used a test script built on the &lt;code&gt;mcp&lt;/code&gt; Python client, so check your own client's configuration, launch directory and activation separately.&lt;/li&gt;
&lt;li&gt;Selvedge stores what a person or agent explicitly saves. It doesn't verify that the reasoning is true, and it doesn't guarantee that an agent will consult it.&lt;/li&gt;
&lt;li&gt;Pinning the path makes routes agree on a file. It doesn't create records nobody saved.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31650/is-your-coding-agent-reading-the-memory-you-wrote</guid>
<pubDate>Sun, 18 Oct 2026 21:21:32 +0000</pubDate>
</item>
<item>
<title>I built a background remover that never sees your photo (and it does GIFs and videos too)!</title>
<link>https://coderlegion.com/31622/i-built-a-background-remover-that-never-sees-your-photo-and-it-does-gifs-and-videos-too</link>
<description>&lt;p&gt;When you’re the only engineer in the family, you automatically become the unpaid IT department, video producer, and resident graphic designer, because to relatives, &quot;computers&quot; is just one big subject (and that includes everything to do with mobile phones too... somehow!). So whenever someone needed a transparent PNG or a quick cutout, it fell on me, and I’d get stuck in the same annoying loop. I'd hunt down a &quot;free&quot; background remover (whatever showed up first on Google), upload the photo, wait, and then get held hostage by a &quot;Buy Credits&quot; button. My only options were settling for a blurry, postage-stamp-sized preview, or making a whole new account if I was desperate enough for the high-res version. Worse, somewhere in that mess, a family photo would get shipped off to a sketchy server I know nothing about.&lt;/p&gt;
&lt;p&gt;For a product shot, that's annoying. For a passport photo, a signature, or a client's photo, it's a bit more than annoying.&lt;/p&gt;
&lt;p&gt;So I added a background remover to &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com&quot;&gt;Fizzdoc&lt;/a&gt;, my open-source toolkit where every tool runs inside the browser tab. It now does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Photos:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-background/&quot;&gt;Remove Background&lt;/a&gt;, saved at the photo's own resolution as PNG, JPG or WebP.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Animated GIFs:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-gif-background/&quot;&gt;Remove GIF Background&lt;/a&gt;, every frame, same timing, same loop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Videos:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-video-background/&quot;&gt;Remove Video Background&lt;/a&gt;, every frame, with the sound kept.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WhatsApp stickers:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/whatsapp-sticker-maker/&quot;&gt;WhatsApp Sticker Maker&lt;/a&gt;, a 512 × 512 WebP with a white outline, kept under 100 KB, with words in fun fonts and emoji you drag into place.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can keep the background clear, or put white, any colour, a blur or another photo behind. It's free, there's no sign-up, no watermark, and no limit on how many photos you do. The size limit comes from your device's memory, not from me.&lt;/p&gt;
&lt;p&gt;And the photo never leaves your device. Not &quot;we delete it after an hour&quot;. It just never goes anywhere.&lt;/p&gt;
&lt;p&gt;Below is how it works, and what broke along the way.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-nothing-gets-uploaded-and-you-can-check&quot;&gt;The rule: nothing gets uploaded, and you can check&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/6ozs6z0kkoy1n1ahqle5.png&quot; alt=&quot;The Remove Background page. The badge under the title says what happens to your photo: 0 bytes uploaded.&quot;&gt;&lt;/p&gt;
&lt;p&gt;Fizzdoc is a static site on Cloudflare Pages. There's no backend, so there's nothing that &lt;em&gt;could&lt;/em&gt; receive a file. The page also ships a Content Security Policy with &lt;code&gt;connect-src 'self'&lt;/code&gt;, which means the browser itself blocks the page from talking to any other website.&lt;/p&gt;
&lt;p&gt;That rule shaped everything below. No CDN for the model, no hosted inference API, no &quot;just this one request&quot;. Open DevTools, go to the Network tab, remove the background, and you'll see the model files coming from fizzdoc.com and nothing going out.&lt;/p&gt;
&lt;h3 id=&quot;picking-a-model&quot;&gt;Picking a model&lt;/h3&gt;
&lt;p&gt;I wanted three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Good edges on hair and fur, because that's where background removers usually show their seams.&lt;/li&gt;
&lt;li&gt;Small enough to download once into a browser.&lt;/li&gt;
&lt;li&gt;A licence that lets anyone use the result, including for work.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I went with &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ZhengPeng7/BiRefNet&quot;&gt;BiRefNet&lt;/a&gt;&lt;/strong&gt; (MIT licence), the lite variant, using the 512 × 512 ONNX export by &lt;a rel=&quot;nofollow&quot; href=&quot;https://huggingface.co/studioludens/birefnet-lite-512&quot;&gt;studioludens&lt;/a&gt;. I store its big weight tensors as 16-bit floats and cast them back to 32-bit when the model loads, which halves the download to about 100 MB without changing the maths it runs.&lt;/p&gt;
&lt;p&gt;For phones there's a second model, &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/xuebinqin/U-2-Net&quot;&gt;U²-Net&lt;/a&gt; small&lt;/strong&gt; (Apache-2.0), just 4.6 MB. I learned this one the hard way: someone sent me a screenshot of an early version on a 6 GB Android phone, where Chrome killed the tab at 5% (&quot;Aw, Snap!&quot;). A browser tab on a phone gets far less memory than the phone has, and when it runs out there's no error to catch, the tab just dies. So now phones always use the small model, and the page says so (&quot;for the cleanest edges on hair and fur, use a laptop&quot;). If the big model ever crashes a tab on a laptop too, the page remembers it for 30 days and uses the small one there.&lt;/p&gt;
&lt;p&gt;And for laptops with a graphics chip there's the opposite: a &lt;strong&gt;Sharper edges (hair, fur)&lt;/strong&gt; button. It reruns the photo through a 1024 × 1024 export of the same BiRefNet-lite model (&lt;a rel=&quot;nofollow&quot; href=&quot;https://huggingface.co/onnx-community/BiRefNet_lite-ONNX&quot;&gt;onnx-community&lt;/a&gt;, MIT). It's a one-time 135 MB download and needs WebGPU, so it's never offered on phones. If the device can't manage it, you keep the result you already had.&lt;/p&gt;
&lt;h3 id=&quot;running-it-webgpu-when-it-can-cpu-when-it-can-t&quot;&gt;Running it: WebGPU when it can, CPU when it can't&lt;/h3&gt;
&lt;p&gt;The model runs with &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/microsoft/onnxruntime&quot;&gt;ONNX Runtime Web&lt;/a&gt;&lt;/strong&gt; inside a Web Worker, so the page never freezes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the device has a graphics chip with &lt;strong&gt;WebGPU&lt;/strong&gt;, the model runs there. This is the fast path.&lt;/li&gt;
&lt;li&gt;If WebGPU isn't there (or the driver says no), the &lt;strong&gt;same model&lt;/strong&gt; runs on the CPU through WebAssembly.&lt;/li&gt;
&lt;li&gt;If that fails too, usually because the device ran out of memory, the small U²-Net model takes over.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the chain is GPU, then the same model on CPU, then the small model. Nobody gets an error just because their laptop is old.&lt;/p&gt;
&lt;h4 id=&quot;two-fun-problems-with-self-host-everything-&quot;&gt;Two fun problems with &quot;self-host everything&quot;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare Pages has a 25 MiB limit per file.&lt;/strong&gt; The model is about 100 MB and ONNX Runtime's WebGPU build is a 27 MB &lt;code&gt;.wasm&lt;/code&gt;. So both live on the server as 20 MiB parts: the models are split by a small script when I add them, and the runtime is split at build time. The worker downloads the parts, joins them, and keeps the result in the browser's Cache Storage, so the next time it starts straight from there, even with the internet off. The build also fails if any file it writes goes over 25 MiB, after one deploy failed on it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A runtime that exists in pieces.&lt;/strong&gt; ONNX Runtime normally fetches its &lt;code&gt;.wasm&lt;/code&gt; as one file. Mine exists just as parts on the server, so the worker joins them from Cache Storage and hands the bytes to the runtime directly with &lt;code&gt;ort.env.wasm.wasmBinary&lt;/code&gt;. It needs no extra hosting and no change to the security policy.&lt;/p&gt;
&lt;h3 id=&quot;the-part-i-m-proudest-of-full-resolution-from-a-512-px-model&quot;&gt;The part I'm proudest of: full resolution from a 512 px model&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/p5aj4yuv4j87y4rxega1.png&quot; alt=&quot;Top: the page loading the model once, and the options panel. Bottom: the original photo, the cut-out, and a new background in one tap.&quot;&gt;&lt;/p&gt;
&lt;p&gt;The model looks at your photo squeezed to 512 × 512 and returns a soft mask. If you stretch that mask back over a 4000 × 3000 photo, the edges look like they were cut with a butter knife.&lt;/p&gt;
&lt;p&gt;So the mask goes through two classic image-processing filters, both plain arithmetic on typed arrays in the worker:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fast guided filter&lt;/strong&gt; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://arxiv.org/abs/1505.00996&quot;&gt;He and Sun, 2015&lt;/a&gt;). It uses the photo itself as a guide to snap the blurry mask onto the real edges, so hair follows the actual strands. The fit is computed on a smaller copy (up to 1280 px) and then applied to every pixel of the full photo in one pass.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blur-fusion foreground estimation&lt;/strong&gt; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1109/ICIP42928.2021.9506164&quot;&gt;Forte, 2021&lt;/a&gt;). Edge pixels are a mix of your subject and the old background. Put a cut-out on a new colour without fixing that, and you get a faint halo of the old background. This step estimates the subject's true colour at the edges, so the new background looks like it was always there.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By default the result is saved at your photo's own size: your own pixels, with a cleaner mask.&lt;/p&gt;
&lt;h3 id=&quot;when-the-ai-gets-it-almost-right&quot;&gt;When the AI gets it almost right&lt;/h3&gt;
&lt;p&gt;It's an AI model, so a busy background or a subject that blends into it can confuse it. For photos there's a small set of touch-ups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Erase / Restore&lt;/strong&gt; brush, with &lt;strong&gt;Undo&lt;/strong&gt;, &lt;strong&gt;Redo&lt;/strong&gt; and &lt;strong&gt;Zoom&lt;/strong&gt; for careful work on a phone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tap to remove&lt;/strong&gt; and &lt;strong&gt;Tap to keep&lt;/strong&gt;: one tap picks the whole connected area of a similar colour and removes it, or brings it back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shadow&lt;/strong&gt; and a white &lt;strong&gt;Outline&lt;/strong&gt;, if you want the cut-out to stand out.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crop&lt;/strong&gt;: the whole photo, fit to the subject, or a square around the subject for online shop listings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hold to see the original&lt;/strong&gt;, to compare before and after.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;gifs-every-frame-same-rhythm&quot;&gt;GIFs: every frame, same rhythm&lt;/h3&gt;
&lt;p&gt;Animated GIFs are sneaky. Many frames aren't full pictures, just small patches drawn over the previous frame, with rules (&quot;disposal methods&quot;) about what to clear afterwards.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/matt-way/gifuct-js&quot;&gt;gifuct-js&lt;/a&gt;&lt;/strong&gt; (MIT) decodes the frames, and Fizzdoc composes each one into a full picture the way a browser would show it.&lt;/li&gt;
&lt;li&gt;Each frame goes through the same model (in a faster mode, since there can be dozens).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mattdesl/gifenc&quot;&gt;gifenc&lt;/a&gt;&lt;/strong&gt; (MIT) writes the new GIF, keeping every frame's delay and the loop.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One catch: a GIF pixel is either fully see-through or fully solid. So on a clear background the edges are a little sharper than in a PNG. Pick a colour or a photo behind and the soft edges are back.&lt;/p&gt;
&lt;h3 id=&quot;videos-every-frame-sound-kept&quot;&gt;Videos: every frame, sound kept&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/m7zws0qibirp7kft78oa.png&quot; alt=&quot;Remove Video Background: every frame gets a new background, and the sound is kept.&quot;&gt;&lt;/p&gt;
&lt;p&gt;For video, &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Vanilagy/mediabunny&quot;&gt;Mediabunny&lt;/a&gt;&lt;/strong&gt; (MPL-2.0) reads the file and the browser's own WebCodecs encoder writes the new one. Every frame goes through the model and gets its new background, and the soundtrack is carried over.&lt;/p&gt;
&lt;p&gt;The output is MP4 (H.264) if the browser can handle it; otherwise, it's WebM (VP9). Before you start, the page shows a preview frame and an estimate of how long the whole clip will take on that device.&lt;/p&gt;
&lt;p&gt;What does &quot;no background&quot; mean for a video? Truly see-through video would need the encoder library to start helper workers from &lt;code&gt;blob:&lt;/code&gt; URLs, which my page's security policy blocks, and most players can't show it anyway. So &quot;None&quot; gives you a &lt;strong&gt;green screen&lt;/strong&gt;, which any video editor removes in one click. I chose to keep the policy strict rather than loosen it for one feature.&lt;/p&gt;
&lt;h3 id=&quot;stickers-words-emoji-and-the-last-step-in-whatsapp&quot;&gt;Stickers: words, emoji, and the last step in WhatsApp&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/3br9eeprohzz237iam1y.png&quot; alt=&quot;The WhatsApp Sticker Maker turns any photo into a 512 × 512 sticker with a white outline, under 100 KB.&quot;&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/whatsapp-sticker-maker/&quot;&gt;WhatsApp Sticker Maker&lt;/a&gt; starts from the same cut-out. It squares it to 512 × 512, adds a white outline, and keeps the WebP under 100 KB by lowering the quality step by step until it fits.&lt;/p&gt;
&lt;p&gt;On top of that, you can add words in five styles (Funny, Cute, Cozy, BIG, and Serious) and emojis. Each one is a layer you drag, pinch, resize and rotate, on a phone or with a mouse. The fonts are self-hosted like everything else: Bangers, Kalam, Teko and Inter, with Baloo 2 covering Hindi letters. Emoji are drawn with your device's own emoji font.&lt;/p&gt;
&lt;p&gt;One fun bug on the way: Vite inlines small files as &lt;code&gt;data:&lt;/code&gt; URLs, and a few of the font files were small enough. My own CSP (&lt;code&gt;default-src 'self'&lt;/code&gt;) refused them, and the end-to-end test that fails on any CSP violation caught it before it shipped. Fonts are now always separate files.&lt;/p&gt;
&lt;p&gt;One limit, though. A web page can't add a sticker to WhatsApp, and a sticker shared from the browser arrives as a normal photo. So instead of a &quot;Share to WhatsApp&quot; button that doesn't do what it says, the page shows WhatsApp's own steps: save the sticker, then in any chat tap Stickers › Create and pick it. On iPhone there's a &quot;Save to Photos&quot; button, because downloads go to Files and WhatsApp picks stickers from Photos.&lt;/p&gt;
&lt;h3 id=&quot;what-it-s-like-on-low-end-phones&quot;&gt;What it's like on low-end phones&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ayc0gdg72seyd3vy5kuc.png&quot; alt=&quot;The same tool on a phone, in dark mode. Phones use the small 4.6 MB model so the tab doesn't crash.&quot;&gt;&lt;/p&gt;
&lt;p&gt;I care about this a lot because many people open Fizzdoc on Android phones.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Phones always get the small model, so it works rather than crashes. Edges are a bit softer than on a laptop, which is a trade I'll take every time.&lt;/li&gt;
&lt;li&gt;The worker keeps the page responsive while it thinks.&lt;/li&gt;
&lt;li&gt;The model downloads once (the page shows the progress in MB). After that it starts right away, and it keeps working with Wi-Fi and mobile data off, which is also the easiest way to check that nothing is uploaded.&lt;/li&gt;
&lt;li&gt;Videos give you a time estimate first: short clips are fine, long ones are a laptop job.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;things-it-doesn-t-do-yet-&quot;&gt;Things it doesn't do (yet)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Truly transparent video (see above, green screen instead).&lt;/li&gt;
&lt;li&gt;Many photos in one go: the background remover takes one photo at a time.&lt;/li&gt;
&lt;li&gt;Several photos in one sticker. That's next.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;credits&quot;&gt;Credits&lt;/h3&gt;
&lt;p&gt;None of this would exist without these people and projects. Thank you:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Project &lt;/th&gt;&lt;th&gt;What it does in Fizzdoc &lt;/th&gt;&lt;th&gt;Licence&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ZhengPeng7/BiRefNet&quot;&gt;BiRefNet&lt;/a&gt; by Peng Zheng and co-authors &lt;/td&gt;&lt;td&gt;The background model &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://huggingface.co/studioludens/birefnet-lite-512&quot;&gt;birefnet-lite-512&lt;/a&gt; by studioludens &lt;/td&gt;&lt;td&gt;The 512 px ONNX export &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://huggingface.co/onnx-community/BiRefNet_lite-ONNX&quot;&gt;BiRefNet_lite-ONNX&lt;/a&gt; by onnx-community &lt;/td&gt;&lt;td&gt;The 1024 px export for Sharper edges &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/xuebinqin/U-2-Net&quot;&gt;U²-Net&lt;/a&gt; by Xuebin Qin and co-authors &lt;/td&gt;&lt;td&gt;The small model for phones &lt;/td&gt;&lt;td&gt;Apache-2.0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/microsoft/onnxruntime&quot;&gt;ONNX Runtime Web&lt;/a&gt; by Microsoft &lt;/td&gt;&lt;td&gt;Runs the models on WebGPU or WebAssembly &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/matt-way/gifuct-js&quot;&gt;gifuct-js&lt;/a&gt; by Matt Way &lt;/td&gt;&lt;td&gt;Reads animated GIFs &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mattdesl/gifenc&quot;&gt;gifenc&lt;/a&gt; by Matt DesLauriers &lt;/td&gt;&lt;td&gt;Writes animated GIFs &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Vanilagy/mediabunny&quot;&gt;Mediabunny&lt;/a&gt; by Vanilagy &lt;/td&gt;&lt;td&gt;Reads and writes videos &lt;/td&gt;&lt;td&gt;MPL-2.0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://arxiv.org/abs/1505.00996&quot;&gt;Kaiming He and Jian Sun&lt;/a&gt; &lt;/td&gt;&lt;td&gt;The (fast) guided filter &lt;/td&gt;&lt;td&gt;Paper&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Photoroom/fast-foreground-estimation&quot;&gt;Marco Forte&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Blur-fusion foreground estimation &lt;/td&gt;&lt;td&gt;Paper&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The full licence texts are in Fizzdoc's &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/third-party-licenses.txt&quot;&gt;third-party licences&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;try-it-break-it-tell-me&quot;&gt;Try it, break it, tell me&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Photo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-background/&quot;&gt;https://fizzdoc.com/remove-background/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GIF: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-gif-background/&quot;&gt;https://fizzdoc.com/remove-gif-background/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Video: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-video-background/&quot;&gt;https://fizzdoc.com/remove-video-background/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;WhatsApp sticker: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/whatsapp-sticker-maker/&quot;&gt;https://fizzdoc.com/whatsapp-sticker-maker/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Code (Apache-2.0): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc&quot;&gt;https://github.com/kingrishabdugar/fizzdoc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's part of a bigger toolkit of 68 free tools (PDF, Word, Excel, PowerPoint, images and audio), all running in your browser, in 16 languages. If something looks off on your device, there's a &quot;Report it&quot; link on every tool. And if you like it, a star on GitHub genuinely makes my day.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31622/i-built-a-background-remover-that-never-sees-your-photo-and-it-does-gifs-and-videos-too</guid>
<pubDate>Sun, 18 Oct 2026 14:58:30 +0000</pubDate>
</item>
<item>
<title>Stop your AI agent from redoing finished work</title>
<link>https://coderlegion.com/31556/stop-your-ai-agent-from-redoing-finished-work</link>
<description>&lt;p&gt;I maintain Selvedge. This article was prepared with AI assistance and checked against the released v0.3.17 documentation.&lt;/p&gt;
&lt;p&gt;A recent question in the OpenAI Codex discussions describes a familiar failure. After months of AI-assisted development on an internal business app, the author asks: &quot;How do experienced developers prevent AI coding agents from producing disconnected code and repeatedly rebuilding or breaking previously completed functionality?&quot; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/openai/codex/discussions/52835&quot;&gt;openai/codex #52835&lt;/a&gt;). I'm citing it as a clear public statement of the problem, not as a Selvedge user story.&lt;/p&gt;
&lt;p&gt;A new session sees the code but not its history. It doesn't know which feature was signed off last week, or which &quot;obvious&quot; improvement was already tried and dropped. From the agent's point of view, rewriting a working module is as reasonable as extending it.&lt;/p&gt;
&lt;p&gt;Most of the fix is process, not tooling. These tactics work with any agent.&lt;/p&gt;
&lt;h3 id=&quot;1-write-done-down-where-the-agent-will-read-it&quot;&gt;1. Write &quot;done&quot; down where the agent will read it&lt;/h3&gt;
&lt;p&gt;An agent can't respect a boundary it never sees. Keep a short, maintained list of completed areas in the instruction file your agent loads at startup (for example &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;GEMINI.md&lt;/code&gt; or a Cursor rules file, depending on the client). For each entry, name the files or modules, the behavior that is finished, and the test that proves it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DONE: approval flow (src/approvals.py). Covered by tests/test_approvals.py.
Extend it; do not restructure it without asking.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prune it regularly; a stale list gets skimmed.&lt;/p&gt;
&lt;h3 id=&quot;2-make-finished-behavior-executable&quot;&gt;2. Make finished behavior executable&lt;/h3&gt;
&lt;p&gt;A sentence saying &quot;the approval flow works&quot; is weaker than a test that fails when it stops working. Before you call a feature done, make sure at least one test exercises it from the user's side, and ask the agent to run the full suite before declaring any later task complete. A rebuild that breaks finished work then shows up as a red test rather than a surprise.&lt;/p&gt;
&lt;h3 id=&quot;3-record-what-you-rejected-not-just-what-you-chose&quot;&gt;3. Record what you rejected, not just what you chose&lt;/h3&gt;
&lt;p&gt;Code shows the approach you picked. It doesn't show the alternative you ruled out, and that is exactly what a fresh session is most likely to propose again. When you decline an approach, write down three things: what you rejected, why, and what would make it worth reconsidering. An architecture decision record (ADR) or a section of your project docs works. The revisit condition matters because it stops a reasonable rejection from becoming a permanent rule.&lt;/p&gt;
&lt;h3 id=&quot;4-scope-every-task-including-what-not-to-touch&quot;&gt;4. Scope every task, including what not to touch&lt;/h3&gt;
&lt;p&gt;&quot;Add CSV export to the dashboard&quot; invites the agent to reorganize the dashboard. &quot;Add CSV export in &lt;code&gt;src/export.py&lt;/code&gt;; do not modify &lt;code&gt;src/approvals.py&lt;/code&gt; or the database schema&quot; does not. Name the files in scope and the finished areas out of scope, and keep each diff small enough to review in full.&lt;/p&gt;
&lt;h3 id=&quot;5-look-before-editing-and-review-against-the-record&quot;&gt;5. Look before editing, and review against the record&lt;/h3&gt;
&lt;p&gt;Start each task by asking the agent to read the relevant notes and say what it will leave unchanged. Then review the diff against that statement. A change to a &quot;done&quot; file isn't automatically wrong, but it should come with a reason you can check.&lt;/p&gt;
&lt;h3 id=&quot;where-selvedge-fits&quot;&gt;Where Selvedge fits&lt;/h3&gt;
&lt;p&gt;These steps work with plain files. Their weak point is lookup: as notes grow, the right one must surface just as the agent edits a specific function or column. Selvedge is one way to handle step 3 and the lookup in step 5.&lt;/p&gt;
&lt;p&gt;Selvedge is an open-source local decision log for coding agents. It runs as a local stdio MCP server or a CLI, stores records in SQLite under &lt;code&gt;.selvedge/&lt;/code&gt;, needs no hosted account, and makes no LLM calls in core storage and retrieval. You or your agent save a decision, its reason and any rejected approach against an entity such as &lt;code&gt;src/approvals.py::submit_for_review&lt;/code&gt;, &lt;code&gt;users.email&lt;/code&gt; or &lt;code&gt;deps/stripe&lt;/code&gt;. A later session queries that entity before editing it.&lt;/p&gt;
&lt;p&gt;Here is a synthetic example I ran with Selvedge 0.3.17 in a temporary project. The rejection is invented for illustration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;selvedge init
selvedge log src/approvals.py::submit_for_review reject \
  --reasoning &quot;Rejected rebuilding approvals on browser localStorage; went with the server-side approvals table instead, which passes tests/test_approvals.py. A manager on another device would not see a pending submission.&quot; \
  --constraint &quot;Approval state must be visible to every reviewer on any device.&quot; \
  --stale-when &quot;approval state moves to a different shared store&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before the next edit, an agent with shell access can run &lt;code&gt;selvedge prior-attempts src/approvals.py::submit_for_review&lt;/code&gt;, or call the &lt;code&gt;prior_attempts&lt;/code&gt; MCP tool. In the synthetic run, the CLI returned the record labeled &lt;code&gt;rejected (exact)&lt;/code&gt; along with the saved reasoning. The summary line still reads &lt;code&gt;current status … reverted&lt;/code&gt;; in 0.3.17 that status covers both reverted and rejected records, and the row label shows which one it was. &quot;Exact&quot; means an explicit rejection was recorded. It doesn't mean the rejection was correct or still applies. &lt;code&gt;selvedge blame&lt;/code&gt; showed the constraint and revisit condition, and &lt;code&gt;selvedge search localStorage&lt;/code&gt; found the record. For an entity with no qualifying records, the CLI reported &quot;No prior attempts found&quot;. That means nothing was recorded, not that nothing was ever tried.&lt;/p&gt;
&lt;p&gt;For reviewers, &lt;code&gt;selvedge export --format markdown -o DECISIONS.md&lt;/code&gt; writes a digest that lists explicit rejections under &quot;Considered and rejected&quot;. Since 0.3.17, adding &lt;code&gt;--check&lt;/code&gt; exits 0 silently when the file is current and 1 with a diff when it is stale or missing. In the synthetic run, logging a second rejection made the check exit 1 and print the missing entry. A hook or CI job can use this to catch a stale copy.&lt;/p&gt;
&lt;p&gt;Tip: name the chosen approach inside the rejection itself. If you log a finished change and then a rejection on the same entity, &lt;code&gt;prior-attempts&lt;/code&gt; shows the finished change as tried, then reverted.&lt;/p&gt;
&lt;p&gt;If a rejection stops applying, &lt;code&gt;selvedge supersede&lt;/code&gt; reopens it and keeps the original record in the history.&lt;/p&gt;
&lt;h3 id=&quot;limits&quot;&gt;Limits&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Only explicitly saved decisions can be retrieved. Connecting the MCP server doesn't capture every decision automatically.&lt;/li&gt;
&lt;li&gt;Selvedge stores the reasoning a person or agent supplies. It doesn't verify that the reasoning is true or complete.&lt;/li&gt;
&lt;li&gt;Nothing guarantees an agent will consult the record. The built-in &lt;code&gt;selvedge demo&lt;/code&gt; runs on a temporary database; it doesn't verify your agent's configuration, activation or hooks. Check those separately in your own client.&lt;/li&gt;
&lt;li&gt;It doesn't replace tests, scoped tasks, code review or a single maintained codebase.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you try it, the useful test is one real rejection: save it, close the session, and check whether a new session retrieves it before proposing the same idea. Setup is in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://selvedge.sh/start/quickstart/&quot;&gt;quickstart&lt;/a&gt;, and the source is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/masondelan/selvedge&quot;&gt;masondelan/selvedge&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31556/stop-your-ai-agent-from-redoing-finished-work</guid>
<pubDate>Sat, 17 Oct 2026 19:17:43 +0000</pubDate>
</item>
<item>
<title>Open Source vs. Proprietary: Why We Built Parts of AnimeArray with Open-Source Tools</title>
<link>https://coderlegion.com/31541/open-source-vs-proprietary-why-we-built-parts-of-animearray-with-open-source-tools</link>
<description>&lt;p&gt;There's a question we get every time someone finds the site for the first time. It usually arrives about ten seconds after they realise AnimeArray is free:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&quot;So who's paying for all this? What's the catch?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It's a fair question, and for a while I didn't have a good answer beyond &quot;advertising covers the hosting.&quot; That was true but it was the boring half of the story. The interesting half is why we built most of what runs the site ourselves, when we could have assembled the same product out of almost entirely commercial services and shipped it in a fraction of the time.&lt;/p&gt;
&lt;p&gt;I've been chewing on that question for long enough that I figured the answer was worth writing down properly — including the parts where we made the wrong call.&lt;/p&gt;
&lt;h3 id=&quot;what-owning-it-actually-got-us&quot;&gt;What &quot;owning it&quot; actually got us&lt;/h3&gt;
&lt;p&gt;The first thing you get is the ability to read the code. Not documentation &lt;em&gt;about&lt;/em&gt; it — the code.&lt;/p&gt;
&lt;p&gt;This sounds obvious once you say it, but I only appreciated it properly during an incident a few months in. Something in the request-handling layer started throwing intermittently. Not consistently enough to be obvious, not rarely enough to ignore. I lost a couple of hours reading the source of the component that was misbehaving, found the logic error, and fixed it myself.&lt;/p&gt;
&lt;p&gt;The alternative was a support ticket with a stack trace I didn't fully understand, waiting on a response from someone who would need my description of the problem to even start investigating. That's not a criticism of vendors — it's just a real difference in how long the problem takes to die. On a small project, an hour versus a week is the whole ballgame.&lt;/p&gt;
&lt;p&gt;The second thing is exit cost, and this is the one I underweighted for a long time.&lt;/p&gt;
&lt;p&gt;Every proprietary tool in a stack is a dependency you don't control. Pricing changes. Features get deprecated. APIs shift under you. The vendor pivots toward a different market. Individually these are survivable. Stacked across a dozen tools, they add up to something you can no longer actually move, even if every individual part is replaceable on paper.&lt;/p&gt;
&lt;p&gt;The reason we care is specific: the expensive, hard-won parts of what we built — the recommendation logic that understands what you actually watched, the systems that keep twelve characters sounding like twelve different people — those live in our code, not inside somebody else's product. If we changed direction tomorrow, we'd carry that work with us. That was deliberate, even if I didn't have this vocabulary for it at the time.&lt;/p&gt;
&lt;p&gt;The third is predictable cost, which sounds boring until you have to present a budget.&lt;/p&gt;
&lt;p&gt;Open source tooling isn't free. It's labour and hardware, both of which you can see coming and forecast. What it avoids is per-seat licences and metered APIs that scale unpredictably and tend to punish you precisely when you start succeeding — which is a genuinely unpleasant property to own.&lt;/p&gt;
&lt;p&gt;And the fourth is privacy, though I want to frame it carefully. It's not that open source is more private by some moral property. It's structural. When you run the software, the data stays where you put it. You don't need to publish a policy about what you &lt;em&gt;won't&lt;/em&gt; collect, because there's no third party standing in the path to collect it.&lt;/p&gt;
&lt;h3 id=&quot;the-part-nobody-wants-to-hear&quot;&gt;The part nobody wants to hear&lt;/h3&gt;
&lt;p&gt;Open source is not free, and &quot;we built it ourselves&quot; is not a synonym for &quot;we did nothing for it.&quot;&lt;/p&gt;
&lt;p&gt;Some of the most valuable software in our stack was donated by someone, and I'm grateful for it every day. But the hours we spent configuring, debugging, updating, monitoring and repairing it are absolutely real, and they come out of the same revenue that has to cover hosting and bandwidth. On a small team that's the largest cost we carry, and it's invisible if you only look at the licensing line and conclude we're saving money.&lt;/p&gt;
&lt;p&gt;We own the maintenance. Every dependency is something we chose to run, which means somebody is responsible for watching it, updating it, and dealing with the moment an upstream release breaks something we rely on. A company selling commercial software does that work as part of the price. We do it as part of the job. If you're building something small and reading this thinking it's a free shortcut, that's the misunderstanding I most want to correct.&lt;/p&gt;
&lt;p&gt;There's a version of this that only works at a certain ratio. If our traffic grew a hundredfold, we'd need to make completely different calls — probably more managed services, more outsourced operations, and a much smaller independent stack. We don't pretend the current approach scales forever. We think it's honest about what it costs and right for where we are today, and those are different questions.&lt;/p&gt;
&lt;h3 id=&quot;where-we-deliberately-chose-proprietary&quot;&gt;Where we deliberately chose proprietary&lt;/h3&gt;
&lt;p&gt;I'm not going to pretend open source wins every round, because it doesn't, and an article that only celebrates its own decisions is an advertisement.&lt;/p&gt;
&lt;p&gt;We use paid tools in a few categories, and I'd make the same call again:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Things where a support contract is worth more than the licence.&lt;/strong&gt; When a problem has a clear escalation path and a known owner, it gets solved faster than anything we could diagnose ourselves. That's not a bad thing to buy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Services with no credible open equivalent&lt;/strong&gt; at the quality level we need. Rebuilding a search index or a video pipeline in order to avoid a subscription isn't virtue, it's waste. Not everything important is a matter of principle.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Anything where our own time is the scarcest resource&lt;/strong&gt; and licensing it is genuinely cheaper than building it. This one is easy to get wrong because founders tend to over-value building. Sometimes building is the right call and it's satisfying. Sometimes it's ego with a maintenance bill attached.&lt;/p&gt;
&lt;p&gt;The other thing worth saying: we don't treat open source as a purity test. A permissive licence is legal permission, not a guarantee of quality, security, or maintenance. Popularity on a package registry tells you how many people use something. It does not tell you whether anybody is looking after it.&lt;/p&gt;
&lt;h3 id=&quot;the-four-questions-we-ask&quot;&gt;The four questions we ask&lt;/h3&gt;
&lt;p&gt;When a new tool comes up, we run it through a checklist. It's short on purpose, because a long checklist doesn't get used.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Can we read it?&lt;/strong&gt; If we can't inspect what we're running, we can't honestly reason about whether it belongs anywhere near a stack that handles user accounts. This is a threshold question — a tool that fails it usually doesn't get a second conversation, regardless of price.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What's the exit?&lt;/strong&gt; If this vanished tomorrow, how long would it take to replace it? High exit cost on something load-bearing is a decision we make deliberately, or not at all.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Who pays when it breaks?&lt;/strong&gt; If the honest answer is &quot;we do&quot; and we're not in a position to staff that, we shouldn't choose it. No amount of being right about the long term compensates for a small team quietly becoming the on-call rotation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does it compound?&lt;/strong&gt; Does owning this make the next thing easier to build, or does it create a brand new category of problem? Tools that make later work faster are worth more than tools that are marginally better today.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The real output of this isn't a score. It's that it forces us to be honest about which category a decision falls into, which is where most bad choices actually get made — in the gap between &quot;it's free&quot; and &quot;we can maintain it.&quot;&lt;/p&gt;
&lt;h3 id=&quot;what-this-actually-means-for-the-people-who-use-the-site&quot;&gt;What this actually means for the people who use the site&lt;/h3&gt;
&lt;p&gt;This isn't infrastructure trivia. It shows up in things people notice.&lt;/p&gt;
&lt;p&gt;Because we own the software, we can promise the things that matter and keep them. Everything stays free and usable without an account, because there's no per-user cost to pass on — you can try &lt;a rel=&quot;nofollow&quot; href=&quot;https://animearray.site/&quot;&gt;AnimeArray&lt;/a&gt; for as long as you like and never be asked for a card.&lt;/p&gt;
&lt;p&gt;It also means we can be straight about what the site is and isn't. It's an independent fan project. It hosts no video. It isn't affiliated with, endorsed by, or connected to any studio, publisher, or licensor. There's no commercial arrangement making any of that awkward to say, and I'd rather be boringly accurate than vaguely impressive.&lt;/p&gt;
&lt;p&gt;And it means we can fix things without asking anyone's permission. When something wasn't working well — and things have, repeatedly — we changed it. Nobody had to approve a roadmap.&lt;/p&gt;
&lt;p&gt;If you want to see how this reads in practice, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://roleplay.animearray.site/&quot;&gt;character chats&lt;/a&gt; are the part I'm proudest of, mostly because keeping twelve distinct voices distinct is much harder than it sounds and much more satisfying than I expected. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://otaku.animearray.site/&quot;&gt;Otaku AI assistant&lt;/a&gt; is the other one, and it exists because we kept wanting a thing that didn't exist.&lt;/p&gt;
&lt;h3 id=&quot;if-you-re-starting-something-similar&quot;&gt;If you're starting something similar&lt;/h3&gt;
&lt;p&gt;The question isn't open source versus proprietary. Anyone who frames it that way has already decided and is looking for permission.&lt;/p&gt;
&lt;p&gt;The question is: &lt;strong&gt;do you want to own your dependencies, and are you prepared to pay for that honestly?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If yes, the tooling is the easy part. You'll find the shape of your stack follows fairly quickly from there.&lt;/p&gt;
&lt;p&gt;If no — and that's a completely legitimate answer for many projects — you should lean toward managed services, accept the lock-in consciously, and spend the time you saved on the part of the product that only you can build.&lt;/p&gt;
&lt;p&gt;The mistake I'd avoid is picking one and then resenting it later. Either position is defensible. Being unable to afford the one you chose is not.&lt;/p&gt;
&lt;p&gt;AnimeArray is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://animearray.site/&quot;&gt;animearray.site&lt;/a&gt; if you want to see what the outcome looks like. It's free, it needs no account, and every decision I described above is visible in the finished product — including the compromises.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31541/open-source-vs-proprietary-why-we-built-parts-of-animearray-with-open-source-tools</guid>
<pubDate>Sat, 17 Oct 2026 16:56:37 +0000</pubDate>
</item>
<item>
<title>Selvedge 0.3.17: test your agent memory before you rely on it</title>
<link>https://coderlegion.com/31521/selvedge-0-3-17-test-your-agent-memory-before-you-rely-on-it</link>
<description>&lt;p&gt;I maintain Selvedge. This article was prepared with AI assistance and checked against the released v0.3.17 documentation.&lt;/p&gt;
&lt;p&gt;A coding agent can tell you it saved a decision. The useful test comes later: close the session, start again, and retrieve the same record. Can you recover the reason you rejected an approach, or just a convincing explanation of the code that remains?&lt;/p&gt;
&lt;p&gt;Selvedge gives compatible AI agents persistent decision memory through a local MCP server or the CLI. You save a choice, its reason, and the alternatives you ruled out against a named part of your project: a function, database column, API route, or dependency. Those records live in SQLite under &lt;code&gt;.selvedge/&lt;/code&gt;, so another session can query the same history.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/masondelan/selvedge/releases/tag/v0.3.17&quot;&gt;Version 0.3.17&lt;/a&gt; makes two parts of that workflow easier to check: whether the local MCP server can retrieve a decision after restarting, and whether the Markdown digest you bring to review still matches the stored history.&lt;/p&gt;
&lt;h3 id=&quot;test-the-local-mcp-server&quot;&gt;Test the local MCP server&lt;/h3&gt;
&lt;p&gt;With &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.astral.sh/uv/getting-started/installation/&quot;&gt;uv installed&lt;/a&gt;, you can try the first check without connecting an agent or touching a project's decisions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;uv tool install --upgrade selvedge==0.3.17
selvedge demo --mcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command starts a real local stdio MCP server, saves a synthetic rejected approach through it, shuts that server down, and starts another. It then checks that retrieval returns the same record ID and content. Add &lt;code&gt;--json&lt;/code&gt; to get the checked IDs and version information in machine-readable form.&lt;/p&gt;
&lt;p&gt;The data is disposable. The demo uses a temporary database, skips background update and telemetry checks, and has a deadline followed by subprocess cleanup. That makes it a useful first step when evaluating the package: a small, concrete persistence check with no invented project history left behind.&lt;/p&gt;
&lt;p&gt;It also has a precise boundary. A passing result checks the Python installation running Selvedge and that local MCP exchange. Your editor may launch a different executable, point at a different database, or require you to enable tools. The demo cannot establish that your agent's configuration or lifecycle hooks are active.&lt;/p&gt;
&lt;h3 id=&quot;try-one-real-decision-across-two-sessions&quot;&gt;Try one real decision across two sessions&lt;/h3&gt;
&lt;p&gt;The next test belongs in your own workflow. Follow the &lt;a rel=&quot;nofollow&quot; href=&quot;https://selvedge.sh/start/quickstart/&quot;&gt;quickstart&lt;/a&gt;, connect your agent, and ask it to save one real decision with the reason and any rejected alternative. Keep the entity path and record ID. Then open a fresh session in the same project and ask it to retrieve that decision. Compare the result with what you saved.&lt;/p&gt;
&lt;p&gt;That is also where to test usefulness. Suppose you kept a dependency because its replacement lacked a feature you needed. Retrieving that constraint gives the next session something specific to check before attempting the replacement again. Only explicitly saved decisions can be recovered, and recorded reasoning still needs judgment: Selvedge stores the account supplied by you or your agent; it cannot establish that the account is correct or complete.&lt;/p&gt;
&lt;h3 id=&quot;check-the-digest-before-review&quot;&gt;Check the digest before review&lt;/h3&gt;
&lt;p&gt;The second addition addresses what happens when those records reach a review. Selvedge already exports a deterministic Markdown digest. Once you have project history, generate it with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;selvedge export --format markdown -o DECISIONS.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Version 0.3.17 adds a check you can run before review or in CI wherever that same history is available:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;selvedge export --format markdown -o DECISIONS.md --check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the file matches, the command exits successfully without output. If it is stale or missing, it exits with status 1 and a unified diff. The check preserves both the digest and the stored records; it does not initialize or migrate the database. Read failures produce guidance, and invalid arguments return status 2.&lt;/p&gt;
&lt;p&gt;This gives a reviewer a straightforward consistency check. A digest can look reasonable while omitting a recently recorded decision. The check makes that drift visible. It does not validate the decisions themselves or fill gaps in capture. Review the exported reasoning before sharing or committing it.&lt;/p&gt;
&lt;p&gt;There are smaller fixes with the same aim of making the record easier to interpret. Human-readable output now distinguishes an approach considered and rejected from a change tried and reverted. Literal bracket characters in logged text no longer cause the affected CLI rendering failures. The release adds no runtime dependencies, migrations, or MCP tools.&lt;/p&gt;
&lt;p&gt;If you use a coding agent on a project you regularly return to, try one decision across two sessions. A local stdio MCP connection works; an agent with shell access can use the CLI. No hosted account is required, and core storage and retrieval make no LLM calls. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/masondelan/selvedge&quot;&gt;repository&lt;/a&gt; has the source and release notes. Feedback about where saving or retrieving that first real decision breaks is especially useful—share it in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/masondelan/selvedge/discussions/49&quot;&gt;existing trial discussion&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31521/selvedge-0-3-17-test-your-agent-memory-before-you-rely-on-it</guid>
<pubDate>Sat, 17 Oct 2026 16:10:57 +0000</pubDate>
</item>
<item>
<title># 🧅 MyZubster: Our First Tor v3 Onion Pilot Is Running!</title>
<link>https://coderlegion.com/31502/myzubster-our-first-tor-v3-onion-pilot-is-running</link>
<description>&lt;h2 id=&quot;-myzubster-our-first-tor-v3-onion-pilot-is-running-&quot;&gt;🧅 MyZubster: Our First Tor v3 Onion Pilot Is Running!&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;From an open-source marketplace to a network of independent services. One step at a time.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Hey Coder Legion community! 👋&lt;/p&gt;
&lt;p&gt;We've reached a new technical milestone with &lt;strong&gt;MyZubster&lt;/strong&gt;, our open-source ecosystem focused on connecting people, projects, knowledge, and independent infrastructure.&lt;/p&gt;
&lt;p&gt;We recently deployed our first experimental &lt;strong&gt;Tor v3 Onion Service&lt;/strong&gt; using Docker and HAProxy.&lt;/p&gt;
&lt;h4 id=&quot;-what-works-so-far-&quot;&gt;🚀 What works so far?&lt;/h4&gt;
&lt;p&gt;✅ Tor successfully bootstrapped to 100%&lt;br&gt;
✅ A dedicated Onion v3 identity was generated&lt;br&gt;
✅ An isolated HAProxy service serves our experimental page&lt;br&gt;
✅ Docker networks separate the pilot from production services&lt;br&gt;
✅ The test proxy returns HTTP 404 for &lt;code&gt;/api&lt;/code&gt; and &lt;code&gt;/admin&lt;/code&gt;&lt;br&gt;
✅ The experimental page was displayed during testing&lt;/p&gt;
&lt;p&gt;Our architecture:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Tor Browser
     ↓
Tor v3 Onion Service
     ↓
Isolated HAProxy
     ↓
MyZubster Pilot Page
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This is an experimental proof of concept, not a production-ready or independently audited service. Marketplace, Zorgax, wallets, and payment systems are not part of this pilot.&lt;/p&gt;
&lt;h4 id=&quot;-why-are-we-building-this-&quot;&gt;🌍 Why are we building this?&lt;/h4&gt;
&lt;p&gt;We want MyZubster to evolve into an ecosystem where contributors can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connect independent open-source projects.&lt;/li&gt;
&lt;li&gt;Host services on their own infrastructure.&lt;/li&gt;
&lt;li&gt;Share verifiable technical knowledge.&lt;/li&gt;
&lt;li&gt;Build interoperable tools.&lt;/li&gt;
&lt;li&gt;Participate in community-driven governance.&lt;/li&gt;
&lt;li&gt;Explore privacy-conscious access through Tor.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We believe decentralization should be demonstrated through working infrastructure, not just promises.&lt;/p&gt;
&lt;h4 id=&quot;-what-s-next-&quot;&gt;👨💻 What's next?&lt;/h4&gt;
&lt;p&gt;We're collaborating with our contributor community, including &lt;strong&gt;@wasim-builds&lt;/strong&gt;, on the next proposed development phase:&lt;/p&gt;
&lt;p&gt;🔹 Reproducible Docker Compose deployment&lt;br&gt;
🔹 Tor and HAProxy resilience testing&lt;br&gt;
🔹 Network isolation and security documentation&lt;br&gt;
🔹 Independent code reviews&lt;br&gt;
🔹 Better monitoring and recovery procedures  &lt;/p&gt;
&lt;h4 id=&quot;-developers-wanted-&quot;&gt;🤝 Developers wanted!&lt;/h4&gt;
&lt;p&gt;Are you interested in Docker, Tor, cybersecurity, privacy, distributed systems, or open-source collaboration?&lt;/p&gt;
&lt;p&gt;You can contribute through documentation, testing, code review, or pull requests.&lt;/p&gt;
&lt;p&gt;No VPS or DAO membership is required to get started.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Explore the project:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;🔗 GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;🧅 Tor Pilot Discussion: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem/myzubster/issues/1584&quot;&gt;https://github.com/MyZubster-Ecosystem/myzubster/issues/1584&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;🌐 Website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/&quot;&gt;https://www.myzubster.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Let's build independent infrastructure together, one verified component at a time.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31502/myzubster-our-first-tor-v3-onion-pilot-is-running</guid>
<pubDate>Sat, 17 Oct 2026 07:22:23 +0000</pubDate>
</item>
<item>
<title>A browser editor needs a file workflow, not just a canvas</title>
<link>https://coderlegion.com/31485/a-browser-editor-needs-a-file-workflow-not-just-a-canvas</link>
<description>&lt;p&gt;A browser application can look ready long before you know whether it will work for your files. The canvas loads, the panels look familiar, and the temptation is to import an important project immediately. A better first test follows the file all the way from an untouched source to a result you can reopen.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://craftapps.me/&quot;&gt;Craft Apps&lt;/a&gt; is a useful setting for that discussion. Its public collection offers 13 browser-based tool entries, and its PhotoCraft page explains both the intended image workflow and several limits of the current web build. This article draws on those pages and an inspection of the loaded editor. No edit-and-export round trip was completed, so the checklist below is a proposed test, rather than a compatibility result.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1600/1*QN3Pb_1aiVO33PmEf_k3sQ.png&quot; alt=&quot;Craft Apps homepage with an attributed upstream editor example&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;define-an-observable-result-before-importing-a-file&quot;&gt;Define an observable result before importing a file&lt;/h3&gt;
&lt;p&gt;Choose one task whose outcome can be checked outside the editor. For example, a small image with a readable title at its intended display size gives you a concrete question. “Try all the features” does not. Record the source dimensions and keep the original separate from the experiment.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://craftapps.me/photocraft&quot;&gt;photocraft&lt;/a&gt; landing page discusses layered image editing, text and composition, and a PSD-oriented workflow. It also cautions that a PSD can contain features that do not translate perfectly. A reasonable test therefore starts with a modest file and a known expectation. The extension alone does not tell you whether a particular layer effect will survive a round trip.&lt;/p&gt;
&lt;p&gt;If your task depends on editable text, check editable text. If it depends on transparency, check transparency. An attractive preview can hide a loss that only becomes obvious when somebody tries to use the file later.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1600/1*7xuJO36-7IIIB-Jy-Cloxw.png&quot; alt=&quot;PhotoCraft product page; workspace artwork is labeled as an upstream example&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;separate-the-landing-page-examples-from-the-running-build&quot;&gt;Separate the landing-page examples from the running build&lt;/h3&gt;
&lt;p&gt;Documentation screenshots are useful for explaining an interface. They are weaker evidence for what a particular hosted version can do. PhotoCraft’s product page explicitly identifies its images as upstream examples and says that the browser integration may expose different controls and output options.&lt;/p&gt;
&lt;p&gt;In the actual hosted editor, the opening workspace loaded with New document and Open controls. Opening the new-document dialog showed dimensions, resolution, RGB color mode, bit depth and background settings. That observation establishes that the controls were visible. It does not establish successful import, rendering fidelity or export behavior.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1600/1*40N_8hxP4m78Rgr8d4zyfA.png&quot; alt=&quot;PhotoCraft live new-document dialog with dimensions and color settings&quot;&gt;&lt;/p&gt;
&lt;p&gt;This separation is useful when reporting a bug, too. Name the current build and the exact file behavior you observed. A screenshot from a different release can help explain your expectation, but the report should still identify what happened in your session.&lt;/p&gt;
&lt;h3 id=&quot;treat-saving-as-a-separate-acceptance-step&quot;&gt;Treat saving as a separate acceptance step&lt;/h3&gt;
&lt;p&gt;The live product page says the web build opens files through the browser picker and saves by download. It also states that there is no web autosave or crash recovery. Those details change how you should structure a first test: keep it short enough to save and inspect before investing substantial work.&lt;/p&gt;
&lt;p&gt;After using an available save or export command, locate the downloaded file. Reopen it in the application that will receive the work, or in a fresh editing session when continued editing is the requirement. Check the dimensions and the properties that mattered in your original task.&lt;/p&gt;
&lt;p&gt;Keep a working version separate from a sharing copy when the available formats support that distinction. A flattened image and a reusable layered project serve different purposes. A file that opens successfully can still be unsuitable for the next step, so acceptance should follow the intended use.&lt;/p&gt;
&lt;h3 id=&quot;write-down-what-passed-and-what-remains-unknown&quot;&gt;Write down what passed and what remains unknown&lt;/h3&gt;
&lt;p&gt;A small test record can be a few lines: the source file, the intended change, the save option selected, the location of the download and the result of reopening it. Add any mismatch you saw. This makes the next decision easier than relying on whether the canvas looked convincing.&lt;/p&gt;
&lt;p&gt;For this inspection, the public product notes, loaded workspace and new-document controls were observed. Import fidelity, export fidelity and recovery behavior were not independently tested. The site’s stated limits remain part of the decision.&lt;/p&gt;
&lt;p&gt;A browser photo editor becomes useful for a particular job when its file workflow supports that job. Start with a source you can afford to experiment on, define a visible result, and finish the saving check before scaling up the work.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31485/a-browser-editor-needs-a-file-workflow-not-just-a-canvas</guid>
<pubDate>Sat, 17 Oct 2026 03:15:25 +0000</pubDate>
</item>
<item>
<title>I Replaced $40/mo AI Job Trackers with a Local Python &amp; Web Scraping Command Center (Zero API Key)</title>
<link>https://coderlegion.com/31446/replaced-40-mo-ai-job-trackers-with-local-python-web-scraping-command-center-zero-api-key</link>
<description>&lt;p&gt;If you have looked for a remote role recently, you know the routine.&lt;/p&gt;
&lt;p&gt;You wake up, open 40 browser tabs across Ashby, Lever, Greenhouse, Remotive, Welcome to the Jungle, We Work Remotely, and the monthly Hacker News &lt;em&gt;Who is Hiring&lt;/em&gt; thread. Half the &quot;remote&quot; listings secretly say &lt;em&gt;US Only - Must reside in Texas&lt;/em&gt; in paragraph four. Another third are ghost jobs reposted every Tuesday.&lt;/p&gt;
&lt;p&gt;When I looked for tools to automate this, every commercial &quot;AI job copilot&quot; had the same three problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Another $29 to $49/month subscription&lt;/strong&gt; at the exact moment you are looking for work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your resume and personal data live on someone else's cloud.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The double-billing trap:&lt;/strong&gt; if you look at open-source tools, almost all of them require a paid, pay-per-token OpenAI or Anthropic API key, even though you &lt;em&gt;already&lt;/em&gt; pay $20/month for a ChatGPT Plus, Claude Pro, or Gemini subscription.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I wanted something local, fast, and private that ran on my own machine. So over the last few weeks, I built and open-sourced &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/manny-ruesch/remote-job-hunter&quot;&gt;Remote Job Hunter (&lt;code&gt;rjh&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/manny-ruesch/remote-job-hunter/main/docs/assets/noir-webling-demo.gif&quot; alt=&quot;Remote Job Hunter Command Center Demo&quot;&gt;&lt;/p&gt;
&lt;p&gt;It is a local command center written in Python (standard library for the core engine + FastAPI and SQLite WAL) that scans 51+ job sources in parallel, scores every offer against a single &lt;code&gt;profile.yaml&lt;/code&gt;, generates a 1-page ATS-safe PDF resume per company, and works &lt;strong&gt;even if you do not have an API key&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt; I just pushed &lt;code&gt;v0.2&lt;/code&gt; to GitHub, and &lt;strong&gt;I am looking for developers and job seekers to test it and tear it apart&lt;/strong&gt; so I can make &lt;code&gt;v0.3&lt;/code&gt; better.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;built-for-any-profession-tech-or-non-tech-remote-or-on-site-&quot;&gt;Built for Any Profession (Tech or Non-Tech, Remote or On-Site)&lt;/h3&gt;
&lt;p&gt;Despite the repo name (&lt;code&gt;remote-job-hunter&lt;/code&gt;), the engine is completely domain-agnostic. It does not assume you are a software engineer, and it does not force you to look for remote-only roles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;search:                                    # A nurse in Lyon (on-site)                # A remote RevOps / Python engineer
  titles: {primary: [infirmier, IDE]}      #   healthcare, finance, sales, HR...      #   [automation engineer, revops]
  work_mode: {remote: any, cities: [Lyon]} #   on-site, hybrid, or 100% remote        #   {remote: required}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whether you are a nurse in Lyon (&lt;code&gt;profile.non-tech.example.yaml&lt;/code&gt;), an accountant in Paris, a sales manager, or a backend engineer, dropping your PDF resume into the Command Center automatically extracts your domain skills and calibrates the search and 1-page ATS resume generator for your field.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-wait-why-do-i-need-an-api-key-realization&quot;&gt;1. The &quot;Wait, why do I need an API key?&quot; realization&lt;/h3&gt;
&lt;p&gt;When I shared the first prototype with a friend who is a nurse in Lyon (not a developer), she asked a simple question: &lt;em&gt;&quot;I already have ChatGPT on my phone. Why do I have to create a developer account and put in a credit card for an API key?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;She was right. API keys are great for unattended cron jobs, terrible for normal people.&lt;/p&gt;
&lt;p&gt;That conversation led to &lt;strong&gt;Zero-API-Key Manual Mode&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;  51+ job sources ─▶ deterministic heuristic filter ─▶ 10 offers bundled into ONE prompt
                                                                      │
     scores, pitches &amp;amp; PoW diagrams ◀─ paste JSON back ◀─ paste into ChatGPT / Claude / Gemini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is how it works in practice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;rjh scan&lt;/code&gt; pulls offers from all your configured sources in parallel and runs a fast, deterministic local scorer (&lt;code&gt;rjh/heuristic.py&lt;/code&gt;) that checks title match, location eligibility (&lt;code&gt;Europe&lt;/code&gt;, &lt;code&gt;Global&lt;/code&gt;, &lt;code&gt;US Restricted&lt;/code&gt;), seniority, language, and your custom hard rules.&lt;/li&gt;
&lt;li&gt;If you have an API key (&lt;code&gt;Gemini&lt;/code&gt;, &lt;code&gt;OpenAI&lt;/code&gt;, &lt;code&gt;Anthropic&lt;/code&gt;, &lt;code&gt;Groq&lt;/code&gt;, &lt;code&gt;Mistral&lt;/code&gt;, &lt;code&gt;OpenRouter&lt;/code&gt;) or a local &lt;code&gt;Ollama&lt;/code&gt; model, it evaluates the top candidates automatically.&lt;/li&gt;
&lt;li&gt;If you &lt;strong&gt;don't&lt;/strong&gt; have an API key (or if your free quota hits &lt;code&gt;429&lt;/code&gt;), it automatically falls back to Manual Mode. You click &lt;strong&gt;Export a batch&lt;/strong&gt; in the UI, paste the prompt into the ChatGPT/Claude/Gemini tab you already use, and paste the JSON reply back.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;handling-truncated-chat-responses-without-losing-work&quot;&gt;Handling truncated chat responses without losing work&lt;/h4&gt;
&lt;p&gt;Anyone who has pasted a large prompt into a web chat knows the main failure mode: the model stops generating at token 3,800 and leaves you with half a JSON array.&lt;/p&gt;
&lt;p&gt;Instead of failing with &lt;code&gt;JSONDecodeError&lt;/code&gt;, the parser in &lt;code&gt;rjh/analysis.py&lt;/code&gt; salvages every completed JSON object from the truncated stream, saves those scores to SQLite immediately, and generates a follow-up ticket containing &lt;strong&gt;only the missing job IDs&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def salvage_json_objects(raw_text: str) -&amp;gt; list[dict]:
    &quot;&quot;&quot;Extracts all complete top-level JSON objects from a truncated LLM array.&quot;&quot;&quot;
    decoder = json.JSONDecoder()
    results = []
    idx = 0
    length = len(raw_text)

    while idx &amp;lt; length:
        brace = raw_text.find(&quot;{&quot;, idx)
        if brace == -1:
            break
        try:
            obj, end = decoder.raw_decode(raw_text, brace)
            if isinstance(obj, dict) and &quot;id&quot; in obj:
                results.append(obj)
            idx = end
        except json.JSONDecodeError:
            idx = brace + 1
    return results
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing gets lost, and you never re-run offers that were already scored.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-two-stage-triage-proof-of-work-architecture-diagrams&quot;&gt;2. Two-Stage Triage + Proof-of-Work Architecture Diagrams&lt;/h3&gt;
&lt;p&gt;Early on, I tried asking the LLM to score a job &lt;em&gt;and&lt;/em&gt; write a custom pitch in a single pass. It was slow, burned tokens on mediocre roles, and produced generic pitches.&lt;/p&gt;
&lt;p&gt;I split the pipeline into two distinct stages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stage 1 (Fast Triage):&lt;/strong&gt; Evaluates up to 20 pre-filtered jobs per batch. Returns a &lt;code&gt;fit_score&lt;/code&gt; (&lt;code&gt;0.0&lt;/code&gt; to &lt;code&gt;10.0&lt;/code&gt;), detected tech stack, and a 1-sentence reason.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stage 2 (Deep Dossier for scores &amp;gt;= 7.5):&lt;/strong&gt; Only for the roles worth applying to, the engine generates:&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;3-paragraph tailored pitch&lt;/strong&gt; grounded in your actual metrics from &lt;code&gt;profile.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Proof-of-Work (&lt;code&gt;Mermaid.js&lt;/code&gt;) architecture diagram&lt;/strong&gt; proposing a concrete technical workflow for that company's stack, rendered directly inside the modal so you can walk through it during an interview.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;short founder/hiring-manager DM&lt;/strong&gt; with a 1-click LinkedIn X-Ray search link.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-solving-the-ats-resume-problem-cv-tailor-py-&quot;&gt;3. Solving the ATS Resume Problem (&lt;code&gt;cv_tailor.py&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;When you apply on Workday, Taleo, Greenhouse, Lever, or Ashby, fancy two-column resumes with sidebars, progress bars, or floating boxes often get scrambled into unreadable text streams.&lt;/p&gt;
&lt;p&gt;On the other hand, plain &lt;code&gt;.txt&lt;/code&gt;-style resumes look awful when a human recruiter actually opens the PDF.&lt;/p&gt;
&lt;p&gt;I built &lt;code&gt;cv_tailor.py&lt;/code&gt; to solve both at once:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;100% single-column linear DOM flow&lt;/strong&gt; (&lt;code&gt;Header -&amp;gt; Summary -&amp;gt; Core Stack -&amp;gt; Experience -&amp;gt; Education&lt;/code&gt;) so legacy and modern ATS parsers read every line in the exact right order.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Editorial typography&lt;/strong&gt; (&lt;em&gt;Newsreader&lt;/em&gt; serif headers + &lt;em&gt;Plus Jakarta Sans&lt;/em&gt; body) that fits strictly on &lt;strong&gt;1 page A4&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero hallucination rule:&lt;/strong&gt; It extracts the exact technical keywords from the job description (&lt;code&gt;n8n&lt;/code&gt;, &lt;code&gt;Python&lt;/code&gt;, &lt;code&gt;PostgreSQL&lt;/code&gt;, &lt;code&gt;HubSpot&lt;/code&gt;, &lt;code&gt;REST APIs&lt;/code&gt;), reorders your existing bullet points so the most relevant ones sit at the top, and bolds matching terms without inventing skills or metrics you don't have.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in 7-point ATS self-audit:&lt;/strong&gt; Every generated PDF is inspected with &lt;code&gt;pypdf&lt;/code&gt; before returning a score (&lt;code&gt;100/100&lt;/code&gt;, 1 page A4 verified) and automatically syncs with the 1-click Playwright form pre-filler.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-killing-my-own-cyberpunk-terminal-ui&quot;&gt;4.Killing my own &quot;Cyberpunk Terminal&quot; UI&lt;/h3&gt;
&lt;p&gt;I have a confession: the first version of the dashboard looked like a 1990s hacker movie. Everything was neon green, uppercase monospace, glowing text shadows, &lt;code&gt;[ BRACKETS EVERYWHERE ]&lt;/code&gt;, and emojis on every button.&lt;/p&gt;
&lt;p&gt;After staring at it for three days, my eyes hurt. A tool you use every morning shouldn't feel like a video game HUD.&lt;/p&gt;
&lt;p&gt;I stripped out all the neon glow, brackets, and fake hacker jargon, and rebuilt the interface around a calm &lt;strong&gt;Linear / Raycast / Editorial&lt;/strong&gt; aesthetic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dark Mode (&lt;code&gt;#09090B&lt;/code&gt;)&lt;/strong&gt; and &lt;strong&gt;Warm Paper Light Mode (&lt;code&gt;#FAF8F5&lt;/code&gt;)&lt;/strong&gt; switched via the browser View Transitions API circular ripple.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plus Jakarta Sans&lt;/strong&gt; for UI legibility, &lt;strong&gt;Newsreader&lt;/strong&gt; for editorial hooks, and &lt;strong&gt;JetBrains Mono (&lt;code&gt;tabular-nums&lt;/code&gt;)&lt;/strong&gt; strictly reserved for scores, counters, and keyboard shortcuts (&lt;code&gt;J&lt;/code&gt; / &lt;code&gt;K&lt;/code&gt; to navigate cards, &lt;code&gt;Enter&lt;/code&gt; for details, &lt;code&gt;A&lt;/code&gt; to pre-fill, &lt;code&gt;/&lt;/code&gt; to search).&lt;/li&gt;
&lt;li&gt;I kept our mascot (&lt;em&gt;Noir Webling&lt;/em&gt;) and the interactive dual-layer HTML5 Canvas spider web in the background, toned down into a subtle horology-inspired filigree with an elastic silk thread that connects to whichever job card you are inspecting.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it-in-60-seconds&quot;&gt;Try it in 60 seconds&lt;/h3&gt;
&lt;p&gt;The entire project is MIT-licensed and runs locally on Linux, macOS, and Windows (Python 3.11+):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/manny-ruesch/remote-job-hunter.git
cd remote-job-hunter
pip install -e .

rjh init      # 4 quick questions (creates your profile.yaml)
rjh doctor    # verifies your sources, SQLite WAL database, and optional LLM key
rjh serve     # opens the Command Center on http://127.0.0.1:5070
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt; GitHub Repository:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/manny-ruesch/remote-job-hunter&quot;&gt;https://github.com/manny-ruesch/remote-job-hunter&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;i-need-your-feedback-help-me-break-v0-2-&quot;&gt;I Need Your Feedback (Help Me Break &lt;code&gt;v0.2&lt;/code&gt;!)&lt;/h3&gt;
&lt;p&gt;This is a &lt;code&gt;v0.2&lt;/code&gt; release, and I want to shape the roadmap around real usage rather than guessing in a vacuum.&lt;/p&gt;
&lt;p&gt;If you have 3 minutes to test it out, I would love your honest feedback in the comments (or via a GitHub Issue):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Try &lt;code&gt;rjh init&lt;/code&gt; on your own job search&lt;/strong&gt; (whether you are looking for Backend, DevOps, AI/Automation, Frontend, or even a non-tech role). Did the heuristic scorer rank offers the way you expected, or did noisy offers slip through?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test the Zero-API-Key Manual Mode&lt;/strong&gt; with your regular ChatGPT / Claude / Gemini chat. Was the copy-paste flow smooth, or is there friction I should remove?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test the 1-Page ATS CV Tailor&lt;/strong&gt; on a real job offer. How did the generated PDF look with your profile?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which job boards or regional sources are missing?&lt;/strong&gt; Right now it ships with Greenhouse, Ashby, Lever, Welcome to the Jungle, Remotive, We Work Remotely, Jobicy, Adzuna, and Hacker News. Tell me which board you check every week and I will add a connector for it in &lt;code&gt;v0.3&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you find this useful, a ⭐ on &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/manny-ruesch/remote-job-hunter&quot;&gt;GitHub&lt;/a&gt;&lt;/strong&gt; helps a ton, and every bug report or critique in the comments below goes straight into the next release!&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/31446/replaced-40-mo-ai-job-trackers-with-local-python-web-scraping-command-center-zero-api-key</guid>
<pubDate>Fri, 16 Oct 2026 19:26:24 +0000</pubDate>
</item>
<item>
<title>What habits daily make you a really great software engineer?</title>
<link>https://coderlegion.com/31423/what-habits-daily-make-you-a-really-great-software-engineer</link>
<description>&lt;p&gt;Well, what do you think what are some top notch habits that makes you in top zone of engineering&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/31423/what-habits-daily-make-you-a-really-great-software-engineer</guid>
<pubDate>Fri, 16 Oct 2026 17:09:05 +0000</pubDate>
</item>
</channel>
</rss>