<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Codango® / Codango.Com</title>
	<atom:link href="https://codango.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://codango.com</link>
	<description></description>
	<lastBuildDate>Thu, 29 Oct 2026 15:32:43 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://codango.com/wp-content/uploads/cropped-faviconpng-32x32.png</url>
	<title>Codango® / Codango.Com</title>
	<link>https://codango.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Feedback as Growth</title>
		<link>https://codango.com/feedback-as-growth/</link>
					<comments>https://codango.com/feedback-as-growth/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 15:32:43 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/feedback-as-growth/</guid>

					<description><![CDATA[What shifts when feedback stops feeling external and starts shaping who you are? This week&#8217;s reflection isn&#8217;t about code style or syntax. It&#8217;s about how we respond when the review <a class="more-link" href="https://codango.com/feedback-as-growth/">Continue reading <span class="screen-reader-text">  Feedback as Growth</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>What shifts when feedback stops feeling external and starts shaping who you are?</p>
<p>This week&#8217;s reflection isn&#8217;t about code style or syntax. It&#8217;s about how we respond when the review hits closer to home. When the silence after a merge says more than the comments. When critique stops stinging because you&#8217;ve already seen it in yourself.</p>
<p>Day 141 to 147 explored:</p>
<ul>
<li>Staying present when critique lands</li>
<li>Letting friction sharpen you</li>
<li>Leading without defense</li>
<li>Listening for what silence reveals</li>
<li>You act on what you&#8217;ve already internalized.</li>
</ul>
<p>These aren&#8217;t just habits. They&#8217;re posture. The kind that changes how you show up on the commit and in the room.</p>
<p>If you&#8217;re building more than features, if you&#8217;re building yourself, this week&#8217;s reflection is for you.</p>
<p>Read the full post on Substack<br />
<a href="https://tonystpierre.substack.com/p/week-21-feedback-as-growth" rel="noopener noreferrer">Week 21 &#8211; Feedback as Growth</a></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/feedback-as-growth/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Random Number Generator: Picking 10 Giveaway Winners Nobody Can Predict (Python, Node, and the Browser)</title>
		<link>https://codango.com/random-number-generator-picking-10-giveaway-winners-nobody-can-predict-python-node-and-the-browser/</link>
					<comments>https://codango.com/random-number-generator-picking-10-giveaway-winners-nobody-can-predict-python-node-and-the-browser/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 02:03:01 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/random-number-generator-picking-10-giveaway-winners-nobody-can-predict-python-node-and-the-browser/</guid>

					<description><![CDATA[Our marketing team ran a webinar in October and promised ten $200 vouchers to ten randomly chosen registrants. The export hit my inbox at 09:40, a CSV with 8,453 rows. <a class="more-link" href="https://codango.com/random-number-generator-picking-10-giveaway-winners-nobody-can-predict-python-node-and-the-browser/">Continue reading <span class="screen-reader-text">  Random Number Generator: Picking 10 Giveaway Winners Nobody Can Predict (Python, Node, and the Browser)</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Our marketing team ran a webinar in October and promised ten $200 vouchers to ten randomly chosen registrants. The export hit my inbox at 09:40, a CSV with 8,453 rows.</p>
<p>Legal added two conditions before I touched it. Nobody may predict the winners, and we have to reproduce the exact draw six months later if anyone contests it.</p>
<p>Those two conditions fight each other. Reproducible means a seed. A seed published before the draw lets anybody compute all ten names in advance. It took most of an afternoon to satisfy both, and the mistakes along the way are the interesting part.</p>
<h2>
<p>  Method 1: Python 3.11.1 — draw now, publish the seed afterwards<br />
</p></h2>
<p>The trick that settled it: mint a 64-bit seed from the OS entropy pool at the moment you run the draw, use it, then publish it <em>after</em> the winners go out. Until that instant nobody can know the outcome, because <code>secrets.randbits(64)</code> reads from <code>/dev/urandom</code>. Afterwards anyone with the CSV and the recorded seed replays the identical list.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="c1"># draw_winners.py — Python 3.11.1
</span><span class="kn">import</span> <span class="n">csv</span><span class="p">,</span> <span class="n">json</span><span class="p">,</span> <span class="n">random</span><span class="p">,</span> <span class="n">secrets</span>
<span class="kn">from</span> <span class="n">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timezone</span>

<span class="n">rows</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span><span class="n">csv</span><span class="p">.</span><span class="nc">DictReader</span><span class="p">(</span><span class="nf">open</span><span class="p">(</span><span class="sh">"</span><span class="s">registrants.csv</span><span class="sh">"</span><span class="p">)))</span>

<span class="n">seen</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">rows</span><span class="p">:</span>                          <span class="c1"># last registration wins
</span>    <span class="n">seen</span><span class="p">[</span><span class="n">row</span><span class="p">[</span><span class="sh">"</span><span class="s">email</span><span class="sh">"</span><span class="p">].</span><span class="nf">strip</span><span class="p">().</span><span class="nf">lower</span><span class="p">()]</span> <span class="o">=</span> <span class="n">row</span>
<span class="n">pool</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span><span class="n">seen</span><span class="p">.</span><span class="nf">values</span><span class="p">(),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">r</span><span class="p">:</span> <span class="nf">int</span><span class="p">(</span><span class="n">r</span><span class="p">[</span><span class="sh">"</span><span class="s">registration_id</span><span class="sh">"</span><span class="p">]))</span>

<span class="n">seed</span> <span class="o">=</span> <span class="n">secrets</span><span class="p">.</span><span class="nf">randbits</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>
<span class="n">rng</span> <span class="o">=</span> <span class="n">random</span><span class="p">.</span><span class="nc">Random</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
<span class="n">winners</span> <span class="o">=</span> <span class="n">rng</span><span class="p">.</span><span class="nf">sample</span><span class="p">(</span><span class="n">pool</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">rows </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">rows</span><span class="p">)</span><span class="si">}</span><span class="s"> -&gt; unique people </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">pool</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">seed </span><span class="si">{</span><span class="n">seed</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">n</span><span class="p">,</span> <span class="n">w</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">winners</span><span class="p">,</span> <span class="mi">1</span><span class="p">):</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">n</span><span class="si">:</span><span class="mi">2</span><span class="n">d</span><span class="si">}</span><span class="s">. </span><span class="si">{</span><span class="n">w</span><span class="p">[</span><span class="sh">'</span><span class="s">registration_id</span><span class="sh">'</span><span class="p">]</span><span class="si">:</span><span class="o">&gt;</span><span class="mi">5</span><span class="si">}</span><span class="s">  </span><span class="si">{</span><span class="n">w</span><span class="p">[</span><span class="sh">'</span><span class="s">email</span><span class="sh">'</span><span class="p">]</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

<span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="sh">"</span><span class="s">draw-record.json</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">w</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
    <span class="n">json</span><span class="p">.</span><span class="nf">dump</span><span class="p">({</span><span class="sh">"</span><span class="s">drawn_at</span><span class="sh">"</span><span class="p">:</span> <span class="n">datetime</span><span class="p">.</span><span class="nf">now</span><span class="p">(</span><span class="n">timezone</span><span class="p">.</span><span class="n">utc</span><span class="p">).</span><span class="nf">isoformat</span><span class="p">(),</span>
               <span class="sh">"</span><span class="s">seed</span><span class="sh">"</span><span class="p">:</span> <span class="n">seed</span><span class="p">,</span>
               <span class="sh">"</span><span class="s">pool_size</span><span class="sh">"</span><span class="p">:</span> <span class="nf">len</span><span class="p">(</span><span class="n">pool</span><span class="p">),</span>
               <span class="sh">"</span><span class="s">winners</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="n">w</span><span class="p">[</span><span class="sh">"</span><span class="s">email</span><span class="sh">"</span><span class="p">]</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">winners</span><span class="p">]},</span> <span class="n">f</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</code></pre>
</div>
<p>My run:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>rows 8453 -&gt; unique people 7912
seed 6803436325215821286
 1.  5218  hugo.1007@proton.me
 2.  2644  jonas.2949@fastmail.com
 3.  3899  omar.4274@fastmail.com
 4.  7623  ines.5768@acme.io
 5.  5788  grace.1486@outlook.com
 6.  3998  quentin.1796@outlook.com
 7.  5236  kira.3290@gmail.com
 8.  6944  chloe.4702@proton.me
 9.  7197  hugo.4107@proton.me
10.  6047  rosa.2557@proton.me
</code></pre>
</div>
<p>Two details worth copying. The dedupe runs before the draw, keyed on the lowercased email with the last registration winning, so someone who filled the form twice still gets one entry. And <code>random.sample</code> draws without replacement, unlike <code>random.choices</code>, which is pitfall 3.</p>
<p>Replaying six months later is short enough to paste into a dispute email:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">import</span> <span class="n">json</span><span class="p">,</span> <span class="n">random</span>
<span class="n">record</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="nf">open</span><span class="p">(</span><span class="sh">"</span><span class="s">draw-record.json</span><span class="sh">"</span><span class="p">))</span>
<span class="n">pool</span> <span class="o">=</span> <span class="nf">build_pool</span><span class="p">(</span><span class="sh">"</span><span class="s">registrants.csv</span><span class="sh">"</span><span class="p">)</span>       <span class="c1"># same dedupe as above
</span><span class="nf">print</span><span class="p">([</span><span class="n">w</span><span class="p">[</span><span class="sh">"</span><span class="s">email</span><span class="sh">"</span><span class="p">]</span> <span class="k">for</span> <span class="n">w</span> <span class="ow">in</span> <span class="n">random</span><span class="p">.</span><span class="nc">Random</span><span class="p">(</span><span class="n">record</span><span class="p">[</span><span class="sh">"</span><span class="s">seed</span><span class="sh">"</span><span class="p">]).</span><span class="nf">sample</span><span class="p">(</span><span class="n">pool</span><span class="p">,</span> <span class="mi">10</span><span class="p">)]</span> <span class="o">==</span> <span class="n">record</span><span class="p">[</span><span class="sh">"</span><span class="s">winners</span><span class="sh">"</span><span class="p">])</span>
</code></pre>
</div>
<p>Seeding also answers the &#8220;who verified this&#8221; question in a way that logging ten email addresses never does. If you would rather skip seeding, <code>secrets.choice(pool)</code> per winner is fine too, but keep the printed list as your evidence since there is no number to replay.</p>
<h2>
<p>  Method 2: Node 22.13.1 — crypto.randomInt with a partial Fisher-Yates<br />
</p></h2>
<p>Same job with zero dependencies. <a href="https://nodejs.org/api/crypto.html#cryptorandomintmin-max-callback" rel="noopener noreferrer"><code>crypto.randomInt(min, max)</code></a> draws from the CSPRNG and handles rejection sampling internally, so unlike <code>crypto.randomBytes(4).readUInt32BE() % n</code> it does not skew the low end of the range.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="c1">// drawWinners.mjs — Node 22.13.1</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">readFileSync</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:fs</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">randomInt</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:crypto</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">lines</span> <span class="o">=</span> <span class="nf">readFileSync</span><span class="p">(</span><span class="dl">"</span><span class="s2">registrants.csv</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">utf8</span><span class="dl">"</span><span class="p">).</span><span class="nf">trim</span><span class="p">().</span><span class="nf">split</span><span class="p">(</span><span class="dl">"</span><span class="se">n</span><span class="dl">"</span><span class="p">).</span><span class="nf">slice</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">byEmail</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Map</span><span class="p">();</span>
<span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">line</span> <span class="k">of</span> <span class="nx">lines</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">id</span><span class="p">,</span> <span class="nx">email</span><span class="p">]</span> <span class="o">=</span> <span class="nx">line</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">,</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">byEmail</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="nx">email</span><span class="p">.</span><span class="nf">trim</span><span class="p">().</span><span class="nf">toLowerCase</span><span class="p">(),</span> <span class="nx">id</span><span class="p">);</span>       <span class="c1">// last one wins</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">pool</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">byEmail</span><span class="p">.</span><span class="nf">entries</span><span class="p">()].</span><span class="nf">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">a</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">-</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">b</span><span class="p">[</span><span class="mi">1</span><span class="p">]));</span>

<span class="kd">function</span> <span class="nf">pick</span><span class="p">(</span><span class="nx">pool</span><span class="p">,</span> <span class="nx">k</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">remaining</span> <span class="o">=</span> <span class="nx">pool</span><span class="p">.</span><span class="nf">slice</span><span class="p">();</span>
  <span class="kd">const</span> <span class="nx">out</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">k</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">j</span> <span class="o">=</span> <span class="nf">randomInt</span><span class="p">(</span><span class="nx">i</span><span class="p">,</span> <span class="nx">remaining</span><span class="p">.</span><span class="nx">length</span><span class="p">);</span>        <span class="c1">// crypto-backed, no modulo bias</span>
    <span class="p">[</span><span class="nx">remaining</span><span class="p">[</span><span class="nx">i</span><span class="p">],</span> <span class="nx">remaining</span><span class="p">[</span><span class="nx">j</span><span class="p">]]</span> <span class="o">=</span> <span class="p">[</span><span class="nx">remaining</span><span class="p">[</span><span class="nx">j</span><span class="p">],</span> <span class="nx">remaining</span><span class="p">[</span><span class="nx">i</span><span class="p">]];</span>
    <span class="nx">out</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">remaining</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">out</span><span class="p">;</span>
<span class="p">}</span>

<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`rows: </span><span class="p">${</span><span class="nx">lines</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2"> unique: </span><span class="p">${</span><span class="nx">pool</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="p">[</span><span class="nx">n</span><span class="p">,</span> <span class="p">[</span><span class="nx">email</span><span class="p">,</span> <span class="nx">id</span><span class="p">]]</span> <span class="k">of</span> <span class="nf">pick</span><span class="p">(</span><span class="nx">pool</span><span class="p">,</span> <span class="mi">10</span><span class="p">).</span><span class="nf">entries</span><span class="p">())</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nc">String</span><span class="p">(</span><span class="nx">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">).</span><span class="nf">padStart</span><span class="p">(</span><span class="mi">2</span><span class="p">)}</span><span class="s2">. </span><span class="p">${</span><span class="nx">id</span><span class="p">.</span><span class="nf">padStart</span><span class="p">(</span><span class="mi">5</span><span class="p">)}</span><span class="s2">  </span><span class="p">${</span><span class="nx">email</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">}</span>
</code></pre>
</div>
<p>On the same CSV:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>rows: 8453 unique: 7912
 1.  1514  farid.7145@gmail.com
 2.  7382  sven.4378@acme.io
 3.  7162  kira.3630@gmail.com
 4.  7934  quentin.4656@outlook.com
 5.  3338  quentin.3296@outlook.com
 6.  1595  tara.3459@fastmail.com
 7.  8364  ana.7180@gmail.com
 8.  4519  dmitri.1763@acme.io
 9.  4791  grace.8866@outlook.com
10.  6326  grace.6346@outlook.com
</code></pre>
</div>
<p>Note that <code>pick</code> consumes only <code>k</code> values and then stops, so the cost stays flat whether the pool holds thousands of rows or millions.</p>
<p>What Node will not give you is a seedable, reproducible CSPRNG stream. <code>Math.random()</code> is seeded once per context and cannot be rewound, so replaying an old draw in JS means rolling your own PRNG on top of a <code>randomInt</code>-generated seed, or simply doing the draw elsewhere.</p>
<h2>
<p>  Method 3: when you just want ten numbers right now<br />
</p></h2>
<p>Plenty of the time nobody needs a script. Meetup raffle with 200 tickets, splitting a workshop into four groups live on a call, seeding a few fixture values while writing tests. I open <a href="https://utilvy.com/en/tools/random-number-generator/" rel="noopener noreferrer">this browser-based random picker</a>, set the range to 1 through 7912, switch on no-duplicates, ask for 10, and copy them into the sheet.</p>
<p>Everything runs client-side, so the registrant list never travels to a server. That matters more than it sounds: most giveaway exports contain emails you have no permission to push through somebody else&#8217;s API. Dedupe and sorting are checkboxes there rather than lines you can forget, which makes the third pitfall impossible on that path.</p>
<p>It also covers the boring asks: seat numbers, ticket stubs, ten integers between 1 and 100 to pad a test fixture. For those I use <a href="https://utilvy.com/en/tools/random-number-generator/" rel="noopener noreferrer">the random number generator at utilvy</a> and never open an editor.</p>
<h2>
<p>  Pitfalls I actually hit<br />
</p></h2>
<h3>
<p>  1. The old results page leaked its seed in 10 ms<br />
</p></h3>
<p>Before rewriting anything I read the script marketing had been using for previous draws. It was a MINSTD generator seeded from the clock:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">let</span> <span class="nx">state</span> <span class="o">=</span> <span class="nc">BigInt</span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nf">now</span><span class="p">());</span>
<span class="kd">const</span> <span class="nx">next</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">state</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span> <span class="o">*</span> <span class="mi">48271</span><span class="nx">n</span><span class="p">)</span> <span class="o">%</span> <span class="mi">2147483647</span><span class="nx">n</span><span class="p">;</span>      <span class="c1">// Lehmer / MINSTD</span>
  <span class="k">return</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">state</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2147483647</span><span class="p">;</span>
<span class="p">};</span>
</code></pre>
</div>
<p>Once three consecutive outputs are visible anywhere, whether on a results page or inside sequential promo codes, the seed falls out by brute force over plausible millisecond values:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>observed: [ 3285, 2106, 5622 ]
recovered seed (epoch ms): 1793239041039  in 10.1 ms
</code></pre>
</div>
<p>Ten milliseconds, and every later number from that page was already determined when it was written. MDN puts it plainly: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random" rel="noopener noreferrer"><code>Math.random()</code> does not provide cryptographically secure random numbers</a>. <code>randomInt</code> and <code>secrets</code> do.</p>
<h3>
<p>  2. Modulo bias is real, and usually far smaller than you fear<br />
</p></h3>
<p>The classic shape:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="n">idx</span> <span class="o">=</span> <span class="nb">int</span><span class="p">.</span><span class="nf">from_bytes</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="nf">urandom</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="sh">"</span><span class="s">big</span><span class="sh">"</span><span class="p">)</span> <span class="o">%</span> <span class="mi">100</span>
</code></pre>
</div>
<p>A byte spans 0 to 255, which is not a multiple of 100, so 0 through 55 are reachable three ways and 56 through 99 two ways. Across 200,000 draws the low group totalled 131,253 against an expected 131,250, averaging 2,344 hits per slot against 1,562. The busiest slot (46) came up 2,461 times and the quietest (92) only 1,456. That gap is 1.5001x, precisely 3/2.</p>
<p>Scale it to the real pool and it nearly disappears. <code>2**32 % 8453</code> leaves a remainder of 6,449, so the first 6,449 people get 508,100 chances each while everyone else gets 508,099. An edge of 0.000197%.</p>
<p>My takeaway after measuring both ends: call the library function because it costs nothing, and stop suspecting <code>%</code> of rigging a draw once the pool climbs past a few hundred thousand.</p>
<h3>
<p>  3. Ten winners can be nine people<br />
</p></h3>
<p>My first draft used <code>random.choices(pool, k=10)</code>, which samples with replacement. Over 10,000 simulated draws, 55 of them announced the same email twice. The same test with <code>random.sample</code> on the deduped pool produced zero.</p>
<p>Reach for a set to fix it and Python refuses outright:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>&gt;&gt;&gt; random.sample(set(emails), 10)
TypeError: Population must be a sequence.  For dicts or sets, use sorted(d).
</code></pre>
</div>
<p>Set support was deprecated in 3.9 and removed in 3.11, and yes, the double space after the period is in the real message. Sort first and it behaves:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="n">random</span><span class="p">.</span><span class="nf">sample</span><span class="p">(</span><span class="nf">sorted</span><span class="p">(</span><span class="nf">set</span><span class="p">(</span><span class="n">emails</span><span class="p">)),</span> <span class="mi">10</span><span class="p">)</span>
</code></pre>
</div>
<p>The same trap exists one step upstream. My file had 8,453 rows for 7,912 people, so drawing from rows lets even a correct winner list hand two entries to one registrant. Dedupe the people before you draw them.</p>
<h2>
<p>  Short version<br />
</p></h2>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Goal</th>
<th>Use</th>
<th>Skip</th>
</tr>
</thead>
<tbody>
<tr>
<td>Unpredictable and replayable</td>
<td>
<code>secrets.randbits(64)</code> seed, published after the draw</td>
<td>any seed derived from the clock</td>
</tr>
<tr>
<td>Genuinely distinct winners</td>
<td>dedupe first, then <code>random.sample</code>
</td>
<td>
<code>random.choices</code>, or a bare <code>set</code> passed to <code>sample</code>
</td>
</tr>
<tr>
<td>Unbiased range</td>
<td>
<code>secrets.randbelow</code>, <code>crypto.randomInt</code>
</td>
<td>rolling your own <code>%</code>
</td>
</tr>
<tr>
<td>Numbers during a call</td>
<td><a href="https://utilvy.com/en/tools/random-number-generator/" rel="noopener noreferrer">a picker that dedupes before it draws</a></td>
<td>uploading the CSV to someone else&#8217;s API</td>
</tr>
</tbody>
</table>
</div>
<p>More free browser-based utilities at utilvy.com</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/random-number-generator-picking-10-giveaway-winners-nobody-can-predict-python-node-and-the-browser/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Weaviate&#8217;s anonymous default makes exposure discovery worth a second look</title>
		<link>https://codango.com/weaviates-anonymous-default-makes-exposure-discovery-worth-a-second-look/</link>
					<comments>https://codango.com/weaviates-anonymous-default-makes-exposure-discovery-worth-a-second-look/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 02:02:26 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/weaviates-anonymous-default-makes-exposure-discovery-worth-a-second-look/</guid>

					<description><![CDATA[Weaviate&#8217;s anonymous default makes exposure discovery worth a second look Weaviate is a vector database with REST and GraphQL interfaces. Its documentation states that anonymous access is enabled by default <a class="more-link" href="https://codango.com/weaviates-anonymous-default-makes-exposure-discovery-worth-a-second-look/">Continue reading <span class="screen-reader-text">  Weaviate&#8217;s anonymous default makes exposure discovery worth a second look</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<h1>
<p>  Weaviate&#8217;s anonymous default makes exposure discovery worth a second look<br />
</p></h1>
<p>Weaviate is a vector database with REST and GraphQL interfaces. Its documentation states that anonymous access is enabled by default in Docker and that requests without explicit authentication run as the <code>anonymous</code> user. The setting is intended for development and evaluation. A deployment that keeps the default after it begins holding production data has an authorization problem regardless of the number of internet matches.</p>
<p>A count-only ZoomEye measurement on 2026-10-29 returned 59 matches for <code>title="Weaviate"</code> and 0 for <code>app="Weaviate"</code>. Both used <code>sub_type=all</code>, page 1, and page size 1. The title result is the selected discovery query. These are matching observations, not confirmed anonymous deployments or exposed records. The zero application result is not proof that Weaviate is absent from the internet.</p>
<h2>
<p>  Check authentication and authorization separately<br />
</p></h2>
<p>Weaviate documentation lists API key, OpenID Connect, and anonymous access as authentication modes. When anonymous access is on, a request without credentials is authenticated as <code>user: anonymous</code>. Authorization then determines what that user can do. If authentication is disabled or an anonymous user has broad permissions, the database is reachable by a larger population than the owner intended.</p>
<p>RBAC is the recommended production authorization model. Roles can constrain access at the collection, tenant, and operation level. Authentication identifies a principal; RBAC decides which classes, objects, and tenants that principal can read or change. A deployment that enables an API key but gives every key root-level access still has a broad authorization boundary.</p>
<p>Review the configuration variables that control anonymous access, API keys, OIDC, and RBAC together. The authorization guide warns that undifferentiated access is suitable only for development or a trusted single-user environment. For a multi-tenant service, test a user against a collection and tenant that belongs to another user. Do not infer tenant isolation from collection naming.</p>
<h2>
<p>  Use product evidence with care<br />
</p></h2>
<p>The title query can identify a page that mentions Weaviate, a documentation site, a login screen, or a service operator. Validate the candidate through an owned hostname, a product route, or a cloud asset record. The application field returned zero, so a title-only candidate needs more corroboration before an analyst treats it as a Weaviate API.</p>
<p>A reverse proxy can hide the backend product fingerprint while still forwarding the API. That is one reason a zero app count should not be used as a negative inventory control. Use internal configuration management and cloud discovery as complementary sources. If an external endpoint is discovered, compare it with the expected listener, authentication provider, and network boundary.</p>
<p>Record the query as a measurement, not a statement of affectedness. The match count can change when pages are updated or when ZoomEye&#8217;s observations change. A useful record keeps the exact query, date, result, unit, and scope, then links it to the owner and the internal test result.</p>
<h2>
<p>  A practical Weaviate review<br />
</p></h2>
<p>For each owned cluster, confirm <code>AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED</code> is false in production unless an explicit public-read use case exists. Enable API key or OIDC authentication and configure RBAC. Remove the root key from client applications and give each service a role scoped to its collections and tenants.</p>
<p>Test anonymous requests and low-privilege requests against read, write, delete, backup, and schema operations. Verify that a user with access to one collection cannot see the existence of another, or at least cannot read its objects or metadata. Check the health and metrics endpoints separately. If they are intentionally internal, keep them off the public listener.</p>
<p>Add monitoring for anonymous requests, authentication failures, unexpected schema changes, and bulk reads. A request count is not proof of misuse, but a sudden rise in anonymous activity can give an owner a reason to inspect the deployment. If anonymous access is enabled by accident, turn it off and rotate any credentials that were reachable from the same network.</p>
<p>Weaviate&#8217;s default is easy to carry from a demo into production. Discovery helps find candidates, but the decisive evidence is the running authentication and authorization configuration. A 59-match title set is not a vulnerability count. It is a reason to ask whether any owned cluster still behaves like a development instance.</p>
<h2>
<p>  References<br />
</p></h2>
<ul>
<li><a href="https://www.zoomeye.ai/searchResult?q=dGl0bGU9IldlYXZpYXRlIg%3D%3D" rel="noopener noreferrer">ZoomEye count-only observation: title=&#8221;Weaviate&#8221;</a></li>
<li><a href="https://docs.weaviate.io/deploy/configuration/authentication" rel="noopener noreferrer">Weaviate: Authentication</a></li>
<li><a href="https://docs.weaviate.io/weaviate/configuration/authz-authn" rel="noopener noreferrer">Weaviate: Authentication and authorization</a></li>
<li><a href="https://docs.weaviate.io/deploy/installation-guides/docker-installation" rel="noopener noreferrer">Weaviate: Docker installation</a></li>
</ul>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/weaviates-anonymous-default-makes-exposure-discovery-worth-a-second-look/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Sorting website form enquiries with an LLM that can only answer from a list</title>
		<link>https://codango.com/sorting-website-form-enquiries-with-an-llm-that-can-only-answer-from-a-list/</link>
					<comments>https://codango.com/sorting-website-form-enquiries-with-an-llm-that-can-only-answer-from-a-list/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 02:01:56 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/sorting-website-form-enquiries-with-an-llm-that-can-only-answer-from-a-list/</guid>

					<description><![CDATA[Contact forms collect everything: real sales leads, support questions, partnership pitches, job applications and a steady drip of spam. Someone has to read each one and decide where it goes. <a class="more-link" href="https://codango.com/sorting-website-form-enquiries-with-an-llm-that-can-only-answer-from-a-list/">Continue reading <span class="screen-reader-text">  Sorting website form enquiries with an LLM that can only answer from a list</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Contact forms collect everything: real sales leads, support questions, partnership pitches, job applications and a steady drip of spam. Someone has to read each one and decide where it goes. An LLM can do that sorting, but if you let it answer freely you get categories like &#8220;Potential Sales Inquiry (High Value)&#8221; one day and &#8220;sales lead&#8221; the next, and your spreadsheet filters stop working.</p>
<p>So I built a small n8n workflow where the model gets a fixed list of category ids and may answer with one of them or <code>null</code>. Anything else is treated as a failure.</p>
<h2>
<p>  How it runs<br />
</p></h2>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>Form POST -&gt; Webhook (name, email, company, message)
  -&gt; Normalize; no valid email -&gt; HTTP 400, nothing logged
  -&gt; LLM sees: category ids + one-line descriptions, and the enquiry
  -&gt; Code node: is the reply exactly one of my ids?
       yes -&gt; that category
       no (null, unknown id, a sentence, API error) -&gt; needs_human
  -&gt; Append one row to Google Sheets
  -&gt; Webhook answers 200 with the category
</code></pre>
</div>
<p>The category list lives in one Code node called <em>Settings (edit me)</em>, something like <code>sales_inquiry</code>, <code>support</code>, <code>partnership</code>, <code>job_application</code>, <code>spam</code>, <code>other</code>, each with a short description and a priority. The prompt and the check both read from that node, so you can&#8217;t end up with a prompt that mentions a category the check doesn&#8217;t know.</p>
<h2>
<p>  The check is a few lines of code<br />
</p></h2>
<p>The model&#8217;s reply gets trimmed, lowercased, and stripped of quotes and a trailing full stop. <code>"Partnership."</code> therefore still counts as <code>partnership</code>. After that it has to match an id exactly. A reply like &#8220;This looks like a partnership request&#8221; fails, even though it contains a valid id, because I don&#8217;t want to guess what the model meant. It goes to <code>needs_human</code> with the reason written in a <code>check_notes</code> column.</p>
<p>An HTTP 500 from the model API goes down the same path. The webhook still answers 200 and the row is still written, so a flaky API doesn&#8217;t lose enquiries.</p>
<p>One more thing that tripped me up in an earlier workflow: form text can contain <code>{name}</code> or <code>{{ $json.x }}</code>, and n8n&#8217;s LangChain nodes treat braces as template syntax. The message needs escaping before it goes into the prompt. In this workflow, text with braces is stored in the sheet exactly as sent.</p>
<h2>
<p>  Testing<br />
</p></h2>
<p>I ran it on a real n8n 1.123.87 instance with local stand-ins for the OpenAI and Google Sheets APIs, and 17 checks passed: correct categories logged for sales and support enquiries, form-encoded and JSON bodies, <code>null</code> / invented id / sentence / HTTP 500 all ending up as <code>needs_human</code>, braces surviving, missing or invalid emails getting a 400 with no row and no model call, exactly one row per valid request, and no other hosts contacted.</p>
<p>What I haven&#8217;t tested: a real model and a real sheet. The checks show that a correct answer gets through and a bad one doesn&#8217;t, not how well a given model sorts your enquiries. The webhook also has no authentication or dedupe, so add those before you put it on a public form.</p>
<h2>
<p>  Get it<br />
</p></h2>
<p>This workflow is in a free bundle with two others (a WhatsApp FAQ bot that can only send stored answers, and a B2B cold-email drafter that never sends anything): <a href="https://zhangautomation.gumroad.com/l/cqtfxr" rel="noopener noreferrer">3 free n8n AI workflows</a>, pay what you want, $0 is fine.</p>
<p>If you need the WhatsApp side done properly, with signature checks, dedupe, opt-out and human handoff, the full kit is <a href="https://zhangautomation.gumroad.com/l/ecvlr" rel="noopener noreferrer">$19</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/sorting-website-form-enquiries-with-an-llm-that-can-only-answer-from-a-list/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Android Studio Now Runs Claude and Codex Through ACP. Android CLI Doesn&#8217;t Need the IDE</title>
		<link>https://codango.com/android-studio-now-runs-claude-and-codex-through-acp-android-cli-doesnt-need-the-ide/</link>
					<comments>https://codango.com/android-studio-now-runs-claude-and-codex-through-acp-android-cli-doesnt-need-the-ide/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 02:01:21 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/android-studio-now-runs-claude-and-codex-through-acp-android-cli-doesnt-need-the-ide/</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fdfco0yhs01j3h9dpgonb-zkkfH0-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" srcset="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fdfco0yhs01j3h9dpgonb-zkkfH0-150x150.webp 150w, https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fdfco0yhs01j3h9dpgonb-zkkfH0-300x300.webp 300w, https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fdfco0yhs01j3h9dpgonb-zkkfH0-768x768.webp 768w, https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fdfco0yhs01j3h9dpgonb-zkkfH0.webp 800w" sizes="(max-width: 150px) 100vw, 150px" />Book: Ship It — the pragmatic startup tech stack Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go My project: <a class="more-link" href="https://codango.com/android-studio-now-runs-claude-and-codex-through-acp-android-cli-doesnt-need-the-ide/">Continue reading <span class="screen-reader-text">  Android Studio Now Runs Claude and Codex Through ACP. Android CLI Doesn&#8217;t Need the IDE</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fdfco0yhs01j3h9dpgonb-zkkfH0-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" srcset="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fdfco0yhs01j3h9dpgonb-zkkfH0-150x150.webp 150w, https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fdfco0yhs01j3h9dpgonb-zkkfH0-300x300.webp 300w, https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fdfco0yhs01j3h9dpgonb-zkkfH0-768x768.webp 768w, https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Fdfco0yhs01j3h9dpgonb-zkkfH0.webp 800w" sizes="auto, (max-width: 150px) 100vw, 150px" /><ul>
<li>
<strong>Book:</strong> <a href="https://www.amazon.com/dp/B0GW2NTCR5" rel="noopener noreferrer">Ship It — the pragmatic startup tech stack</a>
</li>
<li>
<strong>Also by me:</strong> <em>Thinking in Go</em> (2-book series) — <a href="https://xgabriel.com/go-book" rel="noopener noreferrer">Complete Guide to Go Programming</a> + <a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer">Hexagonal Architecture in Go</a>
</li>
<li>
<strong>My project:</strong> <a href="https://hermes-ide.com/" rel="noopener noreferrer">Hermes IDE</a> | <a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer">GitHub</a> — an IDE for developers who ship with Claude Code and other AI coding tools</li>
<li>
<strong>Me:</strong> <a href="https://xgabriel.com/" rel="noopener noreferrer">xgabriel.com</a> | <a href="https://github.com/gabrielanhaia" rel="noopener noreferrer">GitHub</a>
</li>
</ul>
<p>If you build Android apps with Claude Code or Codex, you have probably been running two windows: the agent in a terminal, and Android Studio next to it for the emulator, Logcat and previews. On October 28, at Android Dev Summit, Google moved both of those pieces toward the agent you already use.</p>
<p>First, Android Studio itself now hosts outside agents. In <a href="https://developer.android.com/blog/posts/android-dev-summit-26-developer-tools-performance" rel="noopener noreferrer">Google&#8217;s words</a>, &#8220;Android Studio now supports over 40 ACP-compatible coding agents directly inside Android Studio through Bring Your Own Agent and the open Agent Client Protocol (ACP).&#8221; The same post says the feature &#8220;is now stable in today&#8217;s release of Android Studio Rabbit 2.&#8221;</p>
<p>Second, and easier to miss: Google&#8217;s <a href="https://developer.android.com/tools/agents/android-cli" rel="noopener noreferrer">Android CLI</a> now installs as an <a href="https://developer.android.com/tools/agents/android-cli/agent-plugins" rel="noopener noreferrer">official plugin</a> for Claude Code, Codex and Antigravity. That one doesn&#8217;t need the IDE open at all.</p>
<h2>
<p>  What shipped in Android Studio Rabbit 2<br />
</p></h2>
<p>Bring Your Own Agent (BYOA) is a marketplace inside the IDE. Google&#8217;s instructions: &#8220;open <strong>Settings &gt; Tools &gt; AI &gt; Agents</strong> to browse and install agents from the marketplace. You can install more than one and switch between them as you work&#8221; (<a href="https://developer.android.com/blog/posts/android-dev-summit-26-developer-tools-performance" rel="noopener noreferrer">Android Developers Blog</a>).</p>
<p>The <a href="https://developer.android.com/blog/posts/ai-agents-android-studio-integration" rel="noopener noreferrer">Rabbit 2 release post</a> names four of the 40-plus agents: Claude Agent, Codex from OpenAI, Junie and Google Antigravity, and says any &#8220;ACP compliant agent&#8221; works. <a href="https://the-decoder.com/google-opens-android-development-to-third-party-ai-agents-and-unveils-new-gemini-apis/" rel="noopener noreferrer">The Decoder</a> reports they can run at the same time.</p>
<p>The pitch is that the agent gets the IDE&#8217;s knowledge of your project. Google lists &#8220;a deep understanding of your project structure and build, symbol resolution, Compose Preview rendering, the Emulator, Logcat, and more&#8221; (<a href="https://developer.android.com/blog/posts/android-dev-summit-26-developer-tools-performance" rel="noopener noreferrer">Android Developers Blog</a>).</p>
<p>The plumbing is the <a href="https://agentclientprotocol.com/" rel="noopener noreferrer">Agent Client Protocol</a>, which &#8220;standardizes communication between code editors/IDEs and coding agents.&#8221; Local agents run as subprocesses of the editor and talk &#8220;via JSON-RPC over stdio,&#8221; per the ACP site. That is why one integration can cover 40 agents: Android Studio speaks the protocol, and any agent that speaks ACP, natively or through an adapter, can plug in.</p>
<p>Rabbit 2 also adds Change Review. You &#8220;start a review session from pending agent changes or the Git changes window, and Android Studio generates a report of potential issues,&#8221; and you can call it with a <code>/review</code> command from the agent tool window (<a href="https://developer.android.com/blog/posts/android-dev-summit-26-developer-tools-performance" rel="noopener noreferrer">Android Developers Blog</a>).</p>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdfco0yhs01j3h9dpgonb.png" class="article-body-image-wrapper"><img fetchpriority="high" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdfco0yhs01j3h9dpgonb.png" alt="Flat editorial illustration of an IDE window shaped like a large socket panel, with several different plug heads labelled only by color plugging into it from a terminal cable bundle" width="800" height="800" /></a><br />
<em>One protocol, many agents: Android Studio speaks ACP, and each agent plugs in through it.</em></p>
<h2>
<p>  The part that skips the IDE: Android CLI as an agent plugin<br />
</p></h2>
<p>Google describes Android CLI as working &#8220;in any terminal, with any agent harness, and in your CI pipelines, so your agents can build, run, and debug Android apps autonomously&#8221; (<a href="https://developer.android.com/blog/posts/android-dev-summit-26-developer-tools-performance" rel="noopener noreferrer">Android Developers Blog</a>).</p>
<p>The <a href="https://developer.android.com/tools/agents/android-cli/agent-plugins" rel="noopener noreferrer">plugin docs</a> give these commands. For Claude Code, from your terminal:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>claude plugin marketplace add android/android-cli-plugin
claude plugin <span class="nb">install </span>android-cli@android-cli-plugin
</code></pre>
</div>
<p>For Codex:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>codex plugin marketplace add android/android-cli-plugin
codex plugin add android-cli@android-cli-plugin
</code></pre>
</div>
<p>Note the Codex verb is <code>add</code>, not <code>install</code>, in the second line. Antigravity gets <code>agy plugin install android-cli-plugin@antigravity-plugins-official</code>.</p>
<p>The plugin lives in a public <a href="https://github.com/android/android-cli-plugin" rel="noopener noreferrer">android/android-cli-plugin</a> repo under Apache-2.0. Its README says the repo &#8220;holds the plugin only,&#8221; which matters: the plugin is a skill that teaches the agent how to use Android CLI, and the CLI itself is a separate install. You still install Android CLI from the <a href="https://developer.android.com/tools/agents" rel="noopener noreferrer">downloads page</a>, then, per the <a href="https://developer.android.com/tools/agents/android-cli" rel="noopener noreferrer">CLI docs</a>:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>android update
android init
</code></pre>
</div>
<p><code>android init</code> installs the <code>android-cli</code> skill. The docs also say you can turn off data collection with the <code>--no-metrics</code> flag. If your agent runs in CI, decide that on purpose and pass the flag where the pipeline calls the tool. They also describe a <code>~/.androidrc</code> file that applies flags automatically, one per line; they don&#8217;t say whether <code>--no-metrics</code> works there, so check before you rely on it.</p>
<p>What the agent gets, per the <a href="https://developer.android.com/tools/agents/android-cli/agent-plugins" rel="noopener noreferrer">plugin docs</a>: it can &#8220;create projects, manage virtual devices, inspect live UI layouts, run Journeys, and search the official Android Knowledge Base.&#8221; The <a href="https://developer.android.com/blog/posts/android-dev-summit-26-developer-tools-performance" rel="noopener noreferrer">summit post</a> adds <code>android preview</code>, which &#8220;renders your Compose Previews without needing an emulator or the IDE,&#8221; and <code>android device remote</code>, which can &#8220;provision a real physical device from a secure Google device lab.&#8221;</p>
<h2>
<p>  Read the speed numbers as Google&#8217;s numbers<br />
</p></h2>
<p>The headline figure is this sentence: &#8220;In our internal testing, agents that used <code>android preview</code> completed UI tasks 77% faster while using up to 3x fewer tokens&#8221; (<a href="https://developer.android.com/blog/posts/android-dev-summit-26-developer-tools-performance" rel="noopener noreferrer">Android Developers Blog</a>). For memory leaks, Google says that with Android CLI&#8217;s LeakCanary tool &#8220;the most capable models ran this process almost 2x faster while using up to 25% lesser tokens.&#8221; <a href="https://the-decoder.com/google-opens-android-development-to-third-party-ai-agents-and-unveils-new-gemini-apis/" rel="noopener noreferrer">The Decoder</a> reports that A/B test used Claude Opus 5.5.</p>
<p>Both are vendor tests on vendor-picked tasks, and the post doesn&#8217;t publish the task set. The direction is believable. An agent that can render a preview instead of booting an emulator and screenshotting it should spend fewer turns. Measure the gain on your own app before you quote it.</p>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdua39ic6zwvftkkea60.png" class="article-body-image-wrapper"><img decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdua39ic6zwvftkkea60.png" alt="Flat editorial illustration of a phone-shaped preview frame being rendered straight out of a terminal window, with a dimmed, unplugged emulator box set aside" width="800" height="800" /></a><br />
<em><code>android preview</code> renders Compose Previews from the terminal, so the agent can check UI without booting an emulator.</em></p>
<h2>
<p>  What to check before you switch it on<br />
</p></h2>
<p>The privacy guarantees in the release post are written for Gemini Enterprise. The <a href="https://developer.android.com/blog/posts/ai-agents-android-studio-integration" rel="noopener noreferrer">Rabbit 2 post</a> says that for Gemini Enterprise, source code and workspace context are never retained or used to train global foundation models, and it adds audit logging and central admin policy. It doesn&#8217;t say those terms extend to Claude Agent or Codex. A third-party agent inside Android Studio is still that vendor&#8217;s agent, on that vendor&#8217;s terms. Check your Anthropic or OpenAI plan&#8217;s data terms before you turn either one on.</p>
<p>Sign-in is per agent. Google&#8217;s setup step is to &#8220;authenticate&#8221; (<a href="https://developer.android.com/blog/posts/ai-agents-android-studio-integration" rel="noopener noreferrer">Rabbit 2 post</a>), with no detail on what each agent requires. Expect each one to use its own account and its own usage limits.</p>
<p>Remote devices and preview rendering give an agent more reach. If a tool can provision a physical device in Google&#8217;s lab, put it behind an approval prompt, the same as <code>git push</code>. Keep your agent&#8217;s permission mode where it is now, and let it ask before it runs <code>android device remote</code>.</p>
<p>I haven&#8217;t run Rabbit 2 or the plugin myself. Everything above is from Google&#8217;s October 28 posts and its Android CLI docs.</p>
<h2>
<p>  Running it from Hermes IDE<br />
</p></h2>
<p>I build Hermes IDE, so weigh this with that in mind. Hermes runs Claude Code and Codex in a real terminal, &#8220;exactly as it would anywhere else, so a vendor&#8217;s new commands and modes work in Hermes the day they ship&#8221; (<a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer">Hermes README</a>). The plugin installs into the agent, not the editor, so the same two <code>claude plugin</code> or <code>codex plugin</code> lines are what you&#8217;d type in a Hermes session.</p>
<p>What Hermes adds is a separate <code>hermes/&lt;task&gt;</code> branch and worktree per task, so you could give Claude Code and Codex the same Android ticket and compare. It does not render Compose Previews or host an emulator. For those, Android Studio&#8217;s own BYOA panel is the tool built for the job. Hermes is free, on macOS, Windows and Linux, and not affiliated with Google, Anthropic or OpenAI.</p>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvubo4xjvjq8dhkk462u3.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvubo4xjvjq8dhkk462u3.png" alt="Two real agent sessions side by side in Hermes IDE: Codex on orders-types and Claude Code on orders-api, each in its own hermes/ worktree, with the sessions sidebar showing status, context use, cost and branch" width="800" height="500" /></a><br />
<em>Codex on orders-types and Claude Code on orders-api, side by side in Hermes 2.3.4, each in its own hermes/ worktree, with status, context use, cost and branch in the sidebar.</em></p>
<p>So the question for Android teams is which surface you want the agent in. Do you want it inside Android Studio, next to Logcat, or in a terminal where the CLI plugin gives it the same tools? Tell me which one you&#8217;re trying first.</p>
<h2>
<p>  If this was useful<br />
</p></h2>
<p>An agent that can build, preview and run your Android app from a terminal is an agent that can do it in CI too. <em>Ship It</em> is about that whole stretch between localhost and production for a small team: hosting, CI/CD, auth, payments and monitoring, so the pipeline the agent lands in is one you trust. If you want to try the CLI plugin in a real session, <a href="https://hermes-ide.com/download" rel="noopener noreferrer">Hermes IDE</a> is free and runs Claude Code and Codex side by side.</p>
<p><a href="https://www.amazon.com/dp/B0GW2NTCR5" rel="noopener noreferrer"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxz8vayf7yoku0ph6c6dy.jpg" alt="Ship It — the pragmatic startup tech stack" width="335" height="500" /></a></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/android-studio-now-runs-claude-and-codex-through-acp-android-cli-doesnt-need-the-ide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How to Evaluate an AI Task Management Tool Against Real Work</title>
		<link>https://codango.com/how-to-evaluate-an-ai-task-management-tool-against-real-work/</link>
					<comments>https://codango.com/how-to-evaluate-an-ai-task-management-tool-against-real-work/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 02:00:59 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/how-to-evaluate-an-ai-task-management-tool-against-real-work/</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fcomblogcms.myones.net2Fblog2Fwp-content2Fuploads2F20262F012Fimage-50-W8u9Se-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" />When tasks are split across lists, calendars, and documents, teams spend time rebuilding context instead of moving work forward. A useful task management system should make it easy to capture <a class="more-link" href="https://codango.com/how-to-evaluate-an-ai-task-management-tool-against-real-work/">Continue reading <span class="screen-reader-text">  How to Evaluate an AI Task Management Tool Against Real Work</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fcomblogcms.myones.net2Fblog2Fwp-content2Fuploads2F20262F012Fimage-50-W8u9Se-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" /><p>When tasks are split across lists, calendars, and documents, teams spend time rebuilding context instead of moving work forward. A useful task management system should make it easy to capture work, clarify ownership and deadlines, and keep tasks connected to the information needed to complete them.</p>
<p>To choose one, start with the work your team needs to coordinate—not the AI feature list. Map a representative workflow, identify the structure and governance it requires, then test whether a tool supports it without adding unnecessary administration.</p>
<h2>1. Classify the work before comparing tools</h2>
<p>Personal task apps tend to emphasize fast entry and reminders. Product and engineering teams may also need dependencies, custom statuses, planning views, reporting, and repeatable workflows. Cross-functional teams may need shared goals and workload visibility; teams with calendar overload may need help protecting time.</p>
<p>Write down the requirements that apply to your work. A useful starting set is:</p>
<ul>
<li>How tasks enter the system and how priorities are set.</li>
<li>Who owns each task, and how deadlines and dependencies are represented.</li>
<li>Which views are needed for planning and status reporting.</li>
<li>Whether tasks need to connect to documents, research, or other reference material.</li>
<li>What permissions, roles, and shared standards the organization requires.</li>
<li>Whether the system must support a particular deployment model.</li>
</ul>
<p>This separates a need for calendar scheduling or personal reminders from a need for structured project execution. Those are different problems, and one product may not solve both.</p>
<h2>2. Check what the AI actually assists with</h2>
<p>Useful assistance should reduce a specific kind of work: identifying priorities, summarizing progress, capturing tasks, or reducing scheduling effort. Check whether suggestions preserve clear ownership and deadlines. For team use, shared context, permissions, and reliable reporting matter alongside generated recommendations.</p>
<p>Also distinguish calendar automation from project workflow support. Motion, for example, schedules task blocks and reschedules them when meetings or deadlines change. Reclaim.ai schedules habits, recurring tasks, and focus periods around existing calendar commitments. Both address time allocation; neither description implies detailed issue relationships or customized delivery workflows.</p>
<h2>3. Evaluate structure, governance, and deployment</h2>
<p>For complex work, check whether the system can represent the team&#8217;s actual process. Relevant controls may include custom fields and statuses, issue types and layouts, relationships between work items, planning views, reporting, and automation. If several teams need shared standards but different working methods, examine role-based permissions and hierarchical governance.</p>
<p>Confirm deployment requirements early. The comparison below records the deployment options and AI readiness labels reported for each product in the source material; it is a starting point for evaluation, not a substitute for verifying current requirements with the vendor.</p>
<h2>Compare tools by the problem they address</h2>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Tool</th>
<th>Work it is suited to</th>
<th>Deployment listed</th>
<th>AI readiness listed</th>
<th>Notable capability</th>
</tr>
</thead>
<tbody>
<tr>
<td>ONES.com</td>
<td>Product, R&amp;D, and complex delivery teams</td>
<td>Cloud, On-Premise, Private Cloud, Air-gapped</td>
<td>AI agent + MCP</td>
<td>Configurable projects, workflows, planning, reporting, and automation</td>
</tr>
<tr>
<td>Motion</td>
<td>Calendar-driven planning</td>
<td>Cloud</td>
<td>AI assistant</td>
<td>Automatic time blocking and rescheduling</td>
</tr>
<tr>
<td>Reclaim.ai</td>
<td>Protecting focus time</td>
<td>Cloud</td>
<td>AI assistant</td>
<td>Recurring habits scheduled around meetings</td>
</tr>
<tr>
<td>Todoist</td>
<td>Fast personal task capture</td>
<td>Cloud</td>
<td>AI assistant</td>
<td>Natural-language capture and priority support</td>
</tr>
<tr>
<td>Things</td>
<td>Apple-based personal planning</td>
<td>On-Premise</td>
<td>Not yet</td>
<td>Projects, tags, and repeating tasks</td>
</tr>
<tr>
<td>Linear</td>
<td>Software engineering teams</td>
<td>Cloud</td>
<td>API / MCP</td>
<td>Issue cycles, roadmaps, and development integrations</td>
</tr>
<tr>
<td>Asana</td>
<td>Cross-functional team planning</td>
<td>Cloud</td>
<td>AI assistant</td>
<td>Goals, workload views, and project summaries</td>
</tr>
<tr>
<td>ClickUp</td>
<td>Multi-view workspace customization</td>
<td>Cloud</td>
<td>AI assistant</td>
<td>Flexible hierarchies, views, and reporting</td>
</tr>
<tr>
<td>Notion</td>
<td>Knowledge-centered task lists</td>
<td>Cloud</td>
<td>AI assistant</td>
<td>Pages, databases, and inline assistance</td>
</tr>
<tr>
<td>Microsoft To Do with Copilot</td>
<td>Microsoft 365 users</td>
<td>Cloud</td>
<td>AI assistant</td>
<td>Task capture within the Microsoft ecosystem</td>
</tr>
</tbody>
</table>
</div>
<h2>Match the tool to the workflow</h2>
<h3>For structured product and delivery work</h3>
<p>ONES.com is described as a project and knowledge management platform where AI agents can work within workflows. Teams can configure fields, statuses, issue types, layouts, link types, and workflows. Agile planning, collaboration, reporting, and automation support delivery work. Test management and CI/CD capabilities are also available when needs extend beyond task tracking.</p>
<p><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcomblogcms.myones.net%2Fblog%2Fwp-content%2Fuploads%2F2026%2F01%2Fimage-50.png" alt="ONES.com product screenshot" width="800" height="508" /></p>
<p>This breadth can require more configuration than a personal task list. Teams should establish governance standards before creating many custom workflows. The listed deployment options are Cloud, On-Premise, Private Cloud, and Air-gapped.</p>
<h3>For engineering issue cycles</h3>
<p>Linear focuses on issue tracking, development cycles, roadmaps, and engineering integrations. It suits teams already working with software delivery concepts. Teams coordinating research, business operations, or extensive non-engineering knowledge may need additional systems or broader configuration options.</p>
<p><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcomblogcms.myones.net%2Fblog%2Fwp-content%2Fuploads%2F2026%2F06%2FLinear.png" alt="Linear product screenshot" width="800" height="422" /></p>
<h3>For cross-functional planning</h3>
<p>Asana connects tasks with projects, goals, timelines, and workload views. Its AI assistance can help summarize work and surface planning information. Consistent project data and operating habits make this kind of planning more useful; the model may feel heavier for individual task management.</p>
<p><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcomblogcms.myones.net%2Fblog%2Fwp-content%2Fuploads%2F2026%2F01%2Fimage-43.png" alt="Asana product screenshot" width="800" height="546" /></p>
<h3>For highly configurable workspaces</h3>
<p>ClickUp offers multiple views, hierarchies, fields, templates, time-tracking options, and reporting configurations. Its AI features can support drafting, summarization, and routine project communication. The number of choices makes a clear information architecture important before adding fields, views, or automations.</p>
<p><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcomblogcms.myones.net%2Fblog%2Fwp-content%2Fuploads%2F2026%2F01%2Fimage-45.png" alt="ClickUp product screenshot" width="800" height="515" /></p>
<h3>For tasks closely tied to documentation</h3>
<p>Notion combines pages, databases, documentation, and task views. It fits work where actions are closely connected to briefs, notes, research, and other written context. Many workflows depend on users designing and maintaining the underlying databases; teams needing stricter delivery controls may prefer a purpose-built project system.</p>
<p><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcomblogcms.myones.net%2Fblog%2Fwp-content%2Fuploads%2F2026%2F01%2Fimage-41.png" alt="Notion product screenshot" width="800" height="522" /></p>
<h3>For personal task capture and calendar control</h3>
<p>Todoist emphasizes quick entry through natural-language descriptions, due dates, labels, filters, and recurring items. It is aimed at maintaining a personal list with minimal setup; teams that need rich dependencies or shared delivery reporting may need more structure. Things organizes personal projects, areas, checklists, tags, and repeating tasks across Apple devices, rather than providing broad team coordination.</p>
<p>Motion turns tasks into scheduled calendar blocks and moves them when meetings or deadlines change. Reclaim.ai schedules habits, recurring tasks, and focus periods around calendar commitments. Both depend on calendar information for their time-planning role.</p>
<h3>For existing Microsoft 365 users</h3>
<p>Microsoft To Do with Copilot supports personal lists, reminders, and task capture within the Microsoft environment. It is positioned for individual follow-through and lightweight coordination; teams with dependencies, custom workflows, or detailed project reporting may need a more specialized platform.</p>
<h2>4. Run a representative workflow test</h2>
<p>Before adopting a tool, test it with a real example rather than an empty workspace. Include intake, prioritization, ownership, dependencies, status reporting, and completion. If relevant, also test permissions, shared context, calendar changes, and the deployment requirements identified earlier.</p>
<ol>
<li>Enter a task through the same intake path your team expects to use.</li>
<li>Set its priority, owner, deadline, and any required dependencies.</li>
<li>Move it through the statuses and planning views the team needs.</li>
<li>Check whether reporting communicates progress and blockers clearly.</li>
<li>Review what happens when a meeting, deadline, or dependency changes.</li>
<li>Confirm that the task remains connected to the documents or knowledge needed to complete it.</li>
</ol>
<p>Use the test to decide whether the tool reduces coordination work or simply creates another place to enter tasks. If the workflow requires extensive customization, assess the configuration and governance effort as part of the decision.</p>
<h2>Choose based on the level of structure required</h2>
<p>Calendar-focused tools address scheduling and focus time. Personal task apps prioritize quick capture and reminders. Engineering issue trackers, cross-functional planning tools, configurable workspaces, and document-centered systems each organize a different kind of work.</p>
<p>The right choice is the one that fits the complexity of the workflow, required deployment model, and governance needs. Test those requirements using actual work before committing to a system.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/how-to-evaluate-an-ai-task-management-tool-against-real-work/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Describe your agent, get the cheapest sourced infra stack</title>
		<link>https://codango.com/describe-your-agent-get-the-cheapest-sourced-infra-stack/</link>
					<comments>https://codango.com/describe-your-agent-get-the-cheapest-sourced-infra-stack/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 28 Oct 2026 22:13:41 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/describe-your-agent-get-the-cheapest-sourced-infra-stack/</guid>

					<description><![CDATA[If you&#8217;re building an agent, at some point you have to answer a boring question: what will the infrastructure cost each month? Scraping, proxies, a search API, an LLM, maybe <a class="more-link" href="https://codango.com/describe-your-agent-get-the-cheapest-sourced-infra-stack/">Continue reading <span class="screen-reader-text">  Describe your agent, get the cheapest sourced infra stack</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>If you&#8217;re building an agent, at some point you have to answer a boring question: what will the infrastructure cost each month? Scraping, proxies, a search API, an LLM, maybe some GPU hours. I added a tool to the Lantern Works MCP server that answers it from a plain-text description of the agent.</p>
<p><code>optimize_agent_stack</code> (new in <code>lantern-works-deals-mcp</code> 0.3.6, MIT) parses the description with keywords and regex (no LLM), then picks the cheapest option with a <strong>cited price</strong> for each component among tracked providers. Every line has the math, the vendor&#8217;s pricing URL and when the price was checked. If no price is cited for a component, it says so and doesn&#8217;t guess.</p>
<h2>
<p>  Try it<br />
</p></h2>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>uvx lantern-works-deals-mcp   <span class="c"># stdio MCP server, 6 read-only tools, no API key</span>
</code></pre>
</div>
<p>Or use the browser version: <a href="https://thelanternworks.com/stack/?utm_source=devto&amp;utm_medium=post&amp;utm_campaign=stack-optimizer" rel="noopener noreferrer">https://thelanternworks.com/stack/?utm_source=devto&amp;utm_medium=post&amp;utm_campaign=stack-optimizer</a></p>
<p>There&#8217;s also an OpenAI Agents SDK example: <a href="https://github.com/connormatthewdouglas/agent-work-stack-deals/tree/main/examples/stack-optimizer" rel="noopener noreferrer">https://github.com/connormatthewdouglas/agent-work-stack-deals/tree/main/examples/stack-optimizer</a></p>
<h2>
<p>  Real output (PyPI 0.3.6, run Oct 28, 2026)<br />
</p></h2>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>tools: compare_deals, compare_gpu_prices, get_deal, optimize_agent_stack, optimize_stack, search_deals
job: scrapes 20k JS pages/month via 5 GB residential proxies, 3k search queries, summarizes with an LLM, budget $300/mo
parsed components: scraping, proxy, llm_api, search
  scraping: ZenRows Launch, $69.0/mo [priced], 20,000 x 5 = 100,000 credits; within 250,000 included at $69/mo, checked 2026-10-27T02:54:00-04:00, https://www.zenrows.com/pricing
  proxy: 9Proxy Residential (per GB), $3.4/mo [priced], $0/mo incl. 0 + 5 x $0.68 per 1 extra, checked 2026-10-27T02:54:00-04:00, https://9proxy.com/pricing  (referral link)
  llm_api: Groq Free plan with documented model rate limits (RPM/RPD/TPM) in Groq console docs, $0.0/mo [entry_price], free tier ($0), checked 2026-10-27T01:45:00-04:00, https://console.groq.com/docs/rate-limits
  search: Exa Starter credits + pay as you go, $4/mo [priced], $0/mo incl. 2,500 + 1 x $4 per 1,000 extra, checked 2026-10-27T02:54:00-04:00, https://exa.ai/pricing  (referral link)
$69.00 (scraping) + $3.40 (proxy) + $0.00 (llm_api) + $4.00 (search) = $76.40/mo (complete)
within budget: True

job: serve a 13B model for 40 GPU hours, send 5k emails, budget $150/mo
parsed components: gpu, email
  gpu: RunPod RTX A6000 (Community Cloud), $13.2/mo [priced], $0.33/hr x 40 h, checked 2026-10-28T16:43:09+00:00, https://www.runpod.io/pricing  (referral link; manual signup only)
  email: SendPulse Basic (free), $0.0/mo [priced], within 12,000 included at $0/mo, checked 2026-10-27T02:54:00-04:00, https://sendpulse.com/prices/smtp  (referral link)
$13.20 (gpu) + $0.00 (email) = $13.20/mo (complete)
within budget: True
</code></pre>
</div>
<p>A few things to notice:</p>
<ul>
<li>The numbers show their work: &#8220;20,000 x 5 = 100,000 credits; within 250,000 included at $69/mo&#8221;.</li>
<li>The LLM line is <code>entry_price</code>. That&#8217;s the cheapest cited entry plan (a free tier), and real usage may need a bigger plan.</li>
<li>&#8220;Cheapest&#8221; means cheapest among tracked providers. A vendor we don&#8217;t track may cost less, and the full JSON lists providers that aren&#8217;t priced.</li>
<li>The 13B line filters GPUs by a stated rule: 13 x 2 bytes (FP16) x 1.2 = 31.2 GB VRAM on one GPU for inference. Quantized models need less; full fine-tuning needs several times more, so this estimate is for serving, not training.</li>
<li>RunPod is flagged <code>manual_signup_only</code>: a human has to create the account (the vendor&#8217;s terms ban automated signups).</li>
<li>GPU rates are re-checked daily, so the RunPod line may differ when you run it (this one was checked 2026-10-28, 12:43 PM ET). The other prices were checked 2026-10-27.</li>
<li>Prices are monthly list prices before tax. Check the vendor page before you pay for anything.</li>
</ul>
<h2>
<p>  Disclosure<br />
</p></h2>
<p>Disclosure: some result links returned by the server are referral links. They&#8217;re marked <code>affiliate: true</code> with a <code>referral_disclosure</code> (lines tagged &#8220;(referral link)&#8221; above), and affiliate status isn&#8217;t a ranking factor. The <code>source_url</code> is always the vendor&#8217;s own pricing page.</p>
<p>Repo: <a href="https://github.com/connormatthewdouglas/agent-work-stack-deals" rel="noopener noreferrer">https://github.com/connormatthewdouglas/agent-work-stack-deals</a></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/describe-your-agent-get-the-cheapest-sourced-infra-stack/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>GPU Unlock Boosts RTX 30 Series AI Performance by Up to 60%</title>
		<link>https://codango.com/gpu-unlock-boosts-rtx-30-series-ai-performance-by-up-to-60/</link>
					<comments>https://codango.com/gpu-unlock-boosts-rtx-30-series-ai-performance-by-up-to-60/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 28 Oct 2026 22:13:28 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/gpu-unlock-boosts-rtx-30-series-ai-performance-by-up-to-60/</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Ftawbsxfzjzimfugwb3wc-Iq31tv-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" />GPU Unlock project&#8217;s published benchmarks show substantial improvements in selected mathematical operations on the RTX 3090 Ti. Results vary by numerical format, demonstrating why the headline performance increase should not <a class="more-link" href="https://codango.com/gpu-unlock-boosts-rtx-30-series-ai-performance-by-up-to-60/">Continue reading <span class="screen-reader-text">  GPU Unlock Boosts RTX 30 Series AI Performance by Up to 60%</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2Ftawbsxfzjzimfugwb3wc-Iq31tv-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" /><p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftawbsxfzjzimfugwb3wc.webp" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftawbsxfzjzimfugwb3wc.webp" alt="RTX 3090, Source: Buzzy Commerce" width="800" height="450" /></a></p>
<p>GPU Unlock project&#8217;s published benchmarks show substantial <a href="https://buzzycommerce.com/article/gpu-unlock-boosts-rtx-30-series-ai-performance-60" rel="noopener noreferrer">improvements in selected mathematical operations</a> on the RTX 3090 Ti. Results vary by numerical format, demonstrating why the headline performance increase should not be treated as a universal speed boost.</p>
<p>The results indicate that the biggest improvements occur in specific compute-intensive operations. They do not mean that every AI application will run nearly twice as fast.</p>
<h2>
<p>  Real-World AI Image Generation Gets Up to 60% Faster<br />
</p></h2>
<p>The most relevant results for everyday AI users come from tests using ComfyUI, a popular interface for image-generation workflows.</p>
<p>An RTX 3090 Ti owner reported the following improvements after applying GPU Unlock:</p>
<ul>
<li>MOSS-Image SFT using BF16: 58.6% higher throughput.</li>
<li>Qwen-Image 2.1 Turbo using BF16: 57.2% higher throughput.</li>
<li>Z-Image Turbo using BF16: 56.1% higher throughput.</li>
<li>MOSS-Image SFT using INT8: just 0.3% higher throughput.</li>
</ul>
<p>These results suggest that AI image generation can benefit considerably when its workload relies on the affected tensor operations. However, the tests are community-reported benchmarks on specific hardware and software configurations, rather than guarantees of performance for every user.<br />
The minimal INT8 improvement also illustrates an important limitation: workloads using different numerical formats may see little or no benefit.</p>
<h2>
<p>  Can GPU Unlock Make Local LLMs Run Faster?<br />
</p></h2>
<p>GPU Unlock may also improve some large language model (LLM) workloads, particularly prompt processing, where a model evaluates input tokens in parallel.</p>
<p>Reported tests using vLLM showed improvements of approximately 33% in prompt processing and inference throughput on an RTX 3090. Other reported results across Ampere GPUs showed improvements of 14% to 36% in LLM prefill performance.</p>
<p>However, token generation speed often remains largely unchanged.<br />
The reason is that generating tokens one at a time is frequently limited by memory bandwidth rather than raw Tensor Core computing power. Since GPU Unlock does not increase VRAM bandwidth or capacity, it cannot eliminate this bottleneck.</p>
<p>For developers running local AI models, the practical benefit will therefore depend on the model architecture, quantization, inference software and workload.</p>
<h2>
<p>  Which NVIDIA RTX 30 Series GPUs Are Supported?<br />
</p></h2>
<p>The project lists support for GPU families GA102, GA103, GA104 and GA106, covering a substantial portion of the RTX 30 Series lineup.<br />
Potentially compatible models include the RTX 3090 Ti, RTX 3090, RTX 3080 and other Ampere-based cards. However, support is not guaranteed for every board or configuration, and some reported compatibility tests have failed to deliver the expected improvement.</p>
<p>The main implementation uses a Linux kernel module that runs before the NVIDIA driver initializes. Because it changes hardware settings during initialization, installation requires more technical knowledge than a typical graphics driver update.</p>
<p>An experimental Windows implementation has also been reported, but it uses a different approach and should not be confused with a generally supported Windows release.</p>
<h2>
<p>  Does GPU Unlock Improve Gaming Performance?<br />
</p></h2>
<p>There is currently no convincing evidence that GPU Unlock delivers comparable improvements in gaming frame rates.</p>
<p>Although technologies such as NVIDIA DLSS use Tensor Cores, gaming performance depends on many other factors, including graphics workloads, memory bandwidth, CPU performance and the specific instructions used by a game.</p>
<p>The reported gains primarily concern AI computing and selected matrix operations. Players should not assume that installing the tool will increase FPS by 60% or improve every DLSS-enabled title.</p>
<h2>
<p>  Security Risks and Limitations to Consider<br />
</p></h2>
<p>GPU Unlock remains an experimental, unofficial modification. Changes to low-level hardware initialization can cause system instability, failed boots, GPU hangs or data corruption, depending on the implementation and hardware.</p>
<p>Before considering it, users should:</p>
<ol>
<li>Review the project&#8217;s source code and installation instructions.</li>
<li>Confirm that their exact GPU model is supported.</li>
<li>Back up important data and maintain a recovery method.</li>
<li>Avoid installing unverified binaries or running commands from untrusted sources.</li>
<li>Understand that an unsupported configuration could leave the system unable to boot normally.</li>
</ol>
<p>Users who depend on their computers for production workloads should be particularly cautious. The potential performance gains may not justify the stability risks on a machine that needs to remain reliable.</p>
<h2>
<p>  Final Thoughts<br />
</p></h2>
<p>GPU Unlock could give NVIDIA RTX 30 Series owners a meaningful performance boost for certain AI workloads without purchasing a newer graphics card. Community tests show gains of up to 60% in selected image-generation tasks, while some tensor compute benchmarks improve by considerably more.<br />
However, these gains depend on the workload, numerical format and hardware configuration. The project does not increase VRAM capacity, memory bandwidth or gaming performance automatically, and its experimental nature introduces stability risks.</p>
<p>For AI developers and enthusiasts running local image-generation models, GPU Unlock is an interesting project to watch. For everyone else, the safest approach is to wait for broader compatibility testing and more independent benchmarks before changing low-level GPU settings.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/gpu-unlock-boosts-rtx-30-series-ai-performance-by-up-to-60/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>أفرغت سلة المحذوفات — إليك ما يزال ينفعك</title>
		<link>https://codango.com/%d8%a3%d9%81%d8%b1%d8%ba%d8%aa-%d8%b3%d9%84%d8%a9-%d8%a7%d9%84%d9%85%d8%ad%d8%b0%d9%88%d9%81%d8%a7%d8%aa-%d8%a5%d9%84%d9%8a%d9%83-%d9%85%d8%a7-%d9%8a%d8%b2%d8%a7%d9%84-%d9%8a%d9%86%d9%81/</link>
					<comments>https://codango.com/%d8%a3%d9%81%d8%b1%d8%ba%d8%aa-%d8%b3%d9%84%d8%a9-%d8%a7%d9%84%d9%85%d8%ad%d8%b0%d9%88%d9%81%d8%a7%d8%aa-%d8%a5%d9%84%d9%8a%d9%83-%d9%85%d8%a7-%d9%8a%d8%b2%d8%a7%d9%84-%d9%8a%d9%86%d9%81/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 28 Oct 2026 22:12:47 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/%d8%a3%d9%81%d8%b1%d8%ba%d8%aa-%d8%b3%d9%84%d8%a9-%d8%a7%d9%84%d9%85%d8%ad%d8%b0%d9%88%d9%81%d8%a7%d8%aa-%d8%a5%d9%84%d9%8a%d9%83-%d9%85%d8%a7-%d9%8a%d8%b2%d8%a7%d9%84-%d9%8a%d9%86%d9%81/</guid>

					<description><![CDATA[تفريغ سلة المحذوفات لا يمحو ملفاتك، بل يحذف خريطة ويندوز إليها. هذا الفرق هو سبب أن الاستعادة ممكنة أصلاً — وهو أيضاً سبب أن الدقائق القادمة تحسم النتيجة أكثر من <a class="more-link" href="https://codango.com/%d8%a3%d9%81%d8%b1%d8%ba%d8%aa-%d8%b3%d9%84%d8%a9-%d8%a7%d9%84%d9%85%d8%ad%d8%b0%d9%88%d9%81%d8%a7%d8%aa-%d8%a5%d9%84%d9%8a%d9%83-%d9%85%d8%a7-%d9%8a%d8%b2%d8%a7%d9%84-%d9%8a%d9%86%d9%81/">Continue reading <span class="screen-reader-text">  أفرغت سلة المحذوفات — إليك ما يزال ينفعك</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>تفريغ سلة المحذوفات لا يمحو ملفاتك، بل يحذف خريطة ويندوز إليها. هذا الفرق هو سبب أن الاستعادة ممكنة أصلاً — وهو أيضاً سبب أن الدقائق القادمة تحسم النتيجة أكثر من البرنامج الذي ستختاره في النهاية. سواء كنت تبحث عن «استعادة الملفات بعد تفريغ سلة المحذوفات» أو عن «استرجاع الملفات المحذوفة» أو حتى «استرداد الملفات»، المشكلة واحدة والبداية واحدة.</p>
<p><strong>قبل أي شيء آخر:</strong> توقف عن الحفظ والتنزيل والنسخ والتثبيت على ذلك القرص. كل ملف جديد يمكن أن يهبط على القطاعات نفسها التي لا يزال ملفك المحذوف يشغلها. هذا الدليل مرتّب بحيث تأتي الخطوات الآمنة أولاً — اتبعه من الأعلى إلى الأسفل.</p>
<ul>
<li>
<p>ماذا يحدث فعلاً عند تفريغ سلة المحذوفات</p>
</li>
<li>
<p>القاعدة الأولى: توقف عن الكتابة على ذلك القرص</p>
</li>
<li>
<p>فحوص بدقيقتين قبل أن تفحص بأي برنامج</p>
</li>
<li>
<p>ما يستطيع ويندوز أن يفعله بنفسه</p>
</li>
<li>
<p>القرص الصلب مقابل SSD: لماذا الخطأ نفسه يكلّف أكثر على SSD</p>
</li>
<li>
<p>كيف تحكم على برنامج الاستعادة قبل أن تدفع</p>
</li>
<li>
<p>نِسب نجاح واقعية بحسب الحالة</p>
</li>
<li>
<p>قائمة مراجعة للاستعادة، بالترتيب الصحيح</p>
</li>
</ul>
<h2>
<p>  ماذا يحدث فعلاً عند تفريغ سلة المحذوفات<br />
</p></h2>
<p>في أي قسم بنظام NTFS، يتكوّن الملف من شيئين: سجل في <strong>جدول الملفات الرئيسي (MFT)</strong> يقول «هذه البايتات تُكوّن ملف <em>report.xlsx</em>، وتبدأ من المجموعة X»، والمجموعات نفسها — مخزّنة فيزيائياً على أقراص القرص الصلب أو في شرائح ذاكرة SSD.</p>
<p>نقل الملف إلى سلة المحذوفات لا يمسّ أياً منهما. كل ما يفعله ويندوز أنه يبدّل اسم السجل ويضعه في مجلد مخفي اسمه <code>$Recycle.Bin</code> في جذر كل قرص. لا شيء يُفقد، وهذا سرّ أن الاسترجاع من السلة يكون فورياً ودقيقاً.</p>
<p><strong>أما تفريغ السلة</strong> فأمر مختلف. يحذف ويندوز هذه السجلات ويعيد المجموعات إلى مساحة القرص الحرّة. البايتات لا تزال موجودة فيزيائياً؛ لكن لم يعد أحد يطالب بها، وصار من حق النظام أن يعطي هذه المجموعات بالذات لأول ملف يطلب مساحة.</p>
<p>إذن لدينا فشلان منفصلان، ويستحقّان أن تفصلهما عن بعضهما:</p>
<ul>
<li>
<p><strong>الخريطة اختفت.</strong> هذا قابل للاسترجاع. برامج الاستعادة تمرّ على القسم كما هو، تتعرّف على بصمات الملفات وبقايا الفهارس، وتعيد بناء الخريطة — عادةً مع اسم الملف الأصلي إن كان النظام NTFS، وأحياناً باسم مُولَّد إذا كان السجل نفسه قد تضرّر.</p>
</li>
<li>
<p><strong>البايتات كُتب فوقها.</strong> هذا غير قابل للاسترجاع. لا ببرنامج ولا بأي وسيلة ولا بأي ثمن. اللحظة التي تهبط فيها بيانات جديدة على تلك المجموعات، يتوقف ما كان فيها عن الوجود.</p>
</li>
</ul>
<p>كل ما يلي في هذا الدليل يخدم شيئاً واحداً: منع الحالة الثانية بينما تعمل على الأولى.</p>
<h2>
<p>  القاعدة الأولى: توقف عن الكتابة على ذلك القرص<br />
</p></h2>
<p>إن قرأت قسماً واحداً فليكن هذا. نسبة النجاح تتحدد في أول خمس عشرة دقيقة، ولا شيء تثبّته لاحقاً يُصلح كتابةً طائشة.</p>
<h3>
<p>  افعل هذا الآن<br />
</p></h3>
<ul>
<li>
<p>توقف عن الحفظ والتنزيل والنسخ والطباعة. أغلق المتصفح إن استطعت — ذاكرته المؤقتة وحدها تكتب ميجابايتات في الدقيقة.</p>
</li>
<li>
<p>حدّد مكان حفظ الملفات المستعادة: فلاش ميموري، أو قرص خارجي، أو جهاز آخر، أو رفعها إلى السحابة. <strong>ولا تعُد بها أبداً إلى القرص نفسه الذي تسترجع منه.</strong></p>
</li>
<li>
<p>إن كانت الملفات على قرص النظام (عادةً <code>C:</code>) فاعرف أن ويندوز نفسه يكتب بلا توقف: ملف الصفحات، ملف السبات، القياس عن بُعد، مزامنة OneDrive، تنزيلات التحديثات، فهرس البحث. الجهاز الساكن أمامك ليس قرصاً ساكناً.</p>
</li>
<li>
<p>إن كانت البيانات مما لا يُعوَّض، أوقف الجهاز إيقافاً صحيحاً وأعد تشغيله من قرص آخر أو من فلاشة إنقاذ. هذا القرار الواحد أثمن من أي برنامج تشتريه.</p>
</li>
</ul>
<h3>
<p>  ولا تفعل هذا<br />
</p></h3>
<ul>
<li>
<p><strong>لا تثبّت برنامج استعادة على القرص المُتضرّر.</strong> هذه أكثر الطرق شيوعاً لفقدان ملفاتك بيدك. استخدم نسخة محمولة موضوعة على قرص مختلف، أو ثبّت البرنامج على جهاز ثانٍ وافحص القرص منه.</p>
</li>
<li>
<p><strong>لا تشغّل «تنظيف القرص» ولا «إلغاء التجزئة والتحسين» ولا <code>chkdsk /f</code>.</strong> هذه أدوات إصلاح؛ وهي تكتب في البيانات الوصفية ذاتها التي تحاول قراءتها.</p>
</li>
<li>
<p><strong>لا تفرمِت «لأن ويندوز طلب ذلك».</strong> إذا كان القرص قد بدأ يُظهر أخطاء، فالتهيئة تحوّل حالة صعبة إلى حالة مستحيلة.</p>
</li>
<li>
<p><strong>لا تُرتّب الملفات الناجية</strong> بسحبها إلى مجلد آخر على نفس القسم. كل نقل هو كتابة.</p>
</li>
</ul>
<h2>
<p>  فحوص بدقيقتين قبل أن تفحص بأي برنامج<br />
</p></h2>
<p>قرابة ثلث من يصلون إلى هذه الصفحة لا يحتاج أصلاً إلى برنامج استعادة — يحتاج إلى أن ينظر في المكان الثاني الصحيح. ابدأ بهذه؛ لا تكلف شيئاً ولا تكتب شيئاً.</p>
<ul>
<li>
<p><strong>سلة محذوفات أخرى.</strong> لكل قرص مجلد <code>$Recycle.Bin</code> مخفي خاص به، ولكل مستخدم ويندوز مثله. إن كان الملف من قرص ثانٍ، أو حُذف من حساب آخر، فقد يكون لا يزال هناك سليماً.</p>
</li>
<li>
<p><strong>الملف نفسه عبر خدمة سحابية.</strong> إن كان المجلد داخل OneDrive أو Google Drive أو Dropbox، فملفك على الأرجح له حياة ثانية في سلة المحذوفات الخاصة بالخدمة أو في سجل إصداراتها. الحسابات المجانية تحتفظ عادةً بالملفات المحذوفة نحو ٣٠ يوماً قبل الحذف النهائي — تحقّق من الخدمة نفسها بدل تصديق الرقم. هذه أكثر حالة تُحلّ في دقيقتين، ولذلك جاءت قبل كل برنامج في هذه القائمة.</p>
</li>
<li>
<p><strong>الإصدارات السابقة.</strong> انقر بزر الماوس الأيمن على المجلد الذي كان فيه الملف ← <em>خصائص</em> ← <em>الإصدارات السابقة</em>. إن كانت حماية النظام مفعّلة قبل الحذف، فقد تجد نسخاً أقدم من المجلد، ويمكنك فتحها أو نسخها خارجاً بلا أي برنامج.</p>
</li>
<li>
<p><strong>محفوظات الملفات أو نسخة ويندوز الاحتياطية.</strong> إن كان أيّهما مفعّلاً يوماً ما، فاستعد النسخة من هناك بدل الفحص: النسخة الاحتياطية تعطيك الملف كاملاً، والفحص قد يعطيك أجزاءً منه.</p>
</li>
<li>
<p><strong>النسخ التي نسيت أنها موجودة.</strong> ملفات الاسترداد التلقائي في أوفيس، المجلدات المؤقتة، النسخة التي أرسلتها إلى زميل بالبريد، الإصدار الذي لا يزال في ذاكرة تنزيلات تطبيق محادثة. قبل أن تحارب من أجل النسخة المحذوفة، تحقّق هل نسخة أخرى لا تزال حيّة.</p>
</li>
<li>
<p><strong>دوّن الوقائع.</strong> اسم الملف بدقّة، مسار المجلد الكامل، حرف القرص، متى حُذف تقريباً، وكيف — تفريغ السلة، Shift+Delete، تهيئة، أم «اختفى فقط». ستحتاج هذه الخمسة، والرابع منها هو الذي يحرّك نسبة النجاح أكثر من غيره.</p>
</li>
</ul>
<h2>
<p>  ما يستطيع ويندوز أن يفعله بنفسه<br />
</p></h2>
<p>ثلاث طرق مدمجة، مرتّبة من الأسهل إلى الأكثر جهداً. الأوليّتان لا تكتبان شيئاً على القرص، والثالثة أداة استعادة من سطر الأوامر تصدرها مايكروسوفت.</p>
<h3>
<p>  استعادة الإصدارات السابقة<br />
</p></h3>
<p><em>خصائص ← الإصدارات السابقة</em> على المجلد الحاوي، أو ابحث في قائمة ابدأ عن «استعادة ملفاتك باستخدام محفوظات الملفات». لا تعمل إلا إن كانت الحماية مفعّلة قبل الحذف — وكثير من الأجهزة تُباع وهي معطّلة. تستحق الفحص مع ذلك، لأنها حين تعمل تكون فورية.</p>
<h3>
<p>  أداة Windows File Recovery (winfr)<br />
</p></h3>
<p>من إصدار مايكروسوفت، مجّانية، تُثبَّت من متجر Microsoft، وتعمل من موجه أوامر بصلاحيات مدير. لا واجهة رسومية لها. <strong>ثبّتها على قرص مختلف</strong> عن الذي تسترجع منه، وإلا كتبت فوق ما جئت تبحث عنه.</p>
<p><code>winfr C: D: /regular /n UsersاسمكDocumentsreport.xlsx<br />
winfr C: D: /extensive /n *.jpg<br />
winfr E: D: /extensive /n Photos2025*</code><br />
الترتيب دائماً <strong>المصدر</strong> أولاً ثم <strong>الوجهة</strong>، ولا بُدّ أن تكون الوجهة قرصاً آخر. وضعان يهمّانك:</p>
<ul>
<li>
<p><code>/regular</code> يقرأ سجلات ملفات NTFS ويعيد عادةً الأسماء الأصلية وبنية المجلدات. هذا أول ما تجرّبه على قرص NTFS سليم.</p>
</li>
<li>
<p><code>/extensive</code> يعمل على بصمات الملفات. يجد بيانات لم يعد يعرفها جدول الملفات، لكن الملفات المستعادة تفقد غالباً أسماءها الأصلية وتخرج مجمّعة بحسب النوع.</p>
</li>
</ul>
<p>ملاحظتان عمليّتان: أسماء المفاتيح تغيّرت بين الإصدارات، فشغّل <code>winfr /?</code> واستخدم ما يطبعه إصدارك. ولا توجد معاينة — كل شيء يُكتب أولاً وتُفرزه بعد ذلك، وهذا يعني أنك تحتاج مساحة حرّة على الوجهة تقارب ما تأمل استرجاعه.</p>
<h2>
<p>  القرص الصلب مقابل SSD: لماذا الخطأ نفسه يكلّف أكثر على SSD<br />
</p></h2>
<p>هذا ما تتجاهله أغلب الأدلة، وهو سبب أن شخصين يرتكبان الخطأ نفسه تماماً ويخرجان بنتيجتين مختلفتين كلّ الاختلاف.</p>
<p>على <strong>القرص الصلب الميكانيكي</strong>، البيانات المحذوفة تبقى مكانها حتى يُكتب فوقها شيء آخر. ونسبة النجاح تهبط ببطء — معظمه بسبب استمرارك في استخدام الجهاز — وملفات حُذفت قبل أسابيع تعود أحياناً بشكل يفاجئك.</p>
<p>أما في <strong>القرص SSD</strong> فثمّة آلية إضافية: <strong>TRIM</strong>. عندما تُحرَّر المجموعات، يخبر نظام التشغيل القرص أن «هذه الصفحات غير مستخدمة»، ويمكن لجمع القمامة في المتحكّم أن يمحوها فيزيائياً في أول لحظة فراغ — وغالباً بينما يبدو لك الجهاز ساكناً. وما إن تُمحى المجموعة، تذهب محتوياتها قبل أن تنهي قراءة هذه الجملة.</p>
<p>أمران يجعلان استعادة SSD أصعب من ذلك:</p>
<ul>
<li>
<p><strong>التشفير.</strong> في الأقراص المشفّرة عتادياً، وفي أي جهاز يعمل عليه BitLocker أو تشفير الجهاز، التخلص من مادة المفتاح الخاصة بمجموعة يكفي وحده لجعل محتوياتها غير مقروءة للأبد. دون أن تتغيّر أي ظروف أخرى، هذا وحده يحيل حالةً قابلة للاسترجاع إلى حالة ميؤوس منها.</p>
</li>
<li>
<p><strong>تسوية التآكل.</strong> العنوان المنطقي الذي يُكتب إليه ليس الموقع الفيزيائي المُستخدم، لذا افتراض «لم يُكتب هناك شيء مؤخراً» أضعف بكثير من حاله على القرص الميكانيكي.</p>
</li>
</ul>
<p>الخلاصة العملية: على قرص SSD <em>السرعة أهمّ من البرنامج</em>. أوقف الجهاز، أو انقل القرص إلى جهاز آخر، أو افحصه للقراءة فقط من فلاشة إنقاذ — واعتبر أي شيء مضى عليه أكثر من بضع ساعات حالةً صعبة.</p>
<h2>
<p>  كيف تحكم على برنامج الاستعادة قبل أن تدفع<br />
</p></h2>
<p>البرنامج أقلّ أهمية بكثير من الترتيب الذي تعمل به. لكن بين البرامج فرقٌ حقيقي بين ما يُبنى للمشغّلين وما يُبنى لقُمَع البيع، وأربعة فحوص تفصل بينهما.</p>
<ul>
<li>
<p><strong>هل يفحص للقراءة فقط؟</strong> أول وظيفة لأداة الاستعادة ألّا تلمس القرص. أي برنامج يريد أن «يُصلح» أو «يحسّن» أو «يفحص» القرص قبل أن يريك النتيجة، يُغلق.</p>
</li>
<li>
<p><strong>هل يمكن تشغيله من غير القرص المُتضرّر؟</strong> النسخ المحمولة ووسائط الإنقاذ ليست ترفاً. التثبيت على القرص الذي تسترجع منه هو أكثر خطوة مُدمِّرة في هذه الصفحة.</p>
</li>
<li>
<p><strong>هل يريك ما وجده قبل الدفع؟</strong> فحص لا يقول شيئاً حتى تُدخل مفتاح الترخيص يعرض عليك شراء وعد. الأدوات الجادّة تعرض أسماء الملفات وأحجامها ومعايناتها أولاً.</p>
</li>
<li>
<p><strong>هل هي صادقة في الإخفاق؟</strong> «يستعيد ١٠٠٪ من ملفاتك» إعلان تجاري. الأدوات الحقيقية تتحدّث عن أنظمة الملفات وعن الاستعادة الجزئية وعن حالات لا يعود فيها شيء — لأن تلك الحالات موجودة فعلاً.</p>
</li>
</ul>
<p>أمران أصغر يستحقّان التحقّق: هل الترخيص يغطّي حالتك (كثير من الأدوات مجانية للاستخدام الشخصي فقط)، وهل سياسة الاسترداد حقيقية أم مجرّد نموذج تواصل.</p>
<p>الخيارات المجانية تعمل فعلاً في الحالات البسيطة — أداة تعمل بالبصمات مثل PhotoRec تستخرج صور JPEG من بطاقة ذاكرة مُهيّأة، وغالباً دون الأسماء الأصلية. والذي لا تعطيك إياه الأدوات المجانية عادةً هو ترتيب عمليات يمكن الدفاع عنه، وهذا هو ما يحسم النتيجة حين لا تكون الحالة بسيطة.</p>
<h2>
<p>  نِسب نجاح واقعية بحسب الحالة<br />
</p></h2>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>الحالة</th>
<th>النتيجة المتوقّعة</th>
<th>ما الذي يحسمها</th>
</tr>
</thead>
<tbody>
<tr>
<td>تفريغ السلة على <strong>قرص صلب</strong>، ولاحظت خلال ساعات، والجهاز بالكاد استُخدم بعدها</td>
<td>مرتفعة — غالباً كل شيء تقريباً</td>
<td>كم كُتب على القرص قبل أن تتوقف</td>
</tr>
<tr>
<td>نفس الحالة، لكن الجهاز استُخدم بشكل عادي لعدة أيام</td>
<td>متوسطة إلى منخفضة</td>
<td>التصفح والتحديثات العادية تكتب فوق الملفات باستمرار</td>
</tr>
<tr>
<td>
<strong>قرص SSD</strong>، وقد عمل TRIM فعلاً</td>
<td>منخفضة</td>
<td>التشفير، ومدة بقاء القرص موصولاً بالطاقة بعد الحذف</td>
</tr>
<tr>
<td>حذف من قرص النظام الذي يعمل عليه ويندوز</td>
<td>منخفضة إلى متوسطة</td>
<td>هل أوقفت الجهاز أم واصلت العمل</td>
</tr>
<tr>
<td>المجلد كان مُزامَناً عبر OneDrive أو Drive أو Dropbox</td>
<td>قابل للاسترجاع عادةً بلا أي برنامج</td>
<td>هل تتحقّق من سلة المحذوفات في السحابة خلال مهلتها</td>
</tr>
<tr>
<td>تهيئة كاملة (غير سريعة)، أو الملف كُتب فوقه</td>
<td>منخفضة جداً إلى معدومة</td>
<td>لا شيء يغيّر هذا إلا مختبر متخصص</td>
</tr>
<tr>
<td>القرص يُصدر صوت طقطقة أو احتكاك، أو لم يعد يُكتشف</td>
<td>توقف فوراً</td>
<td>البرامج لا تُصلح عطلاً فيزيائياً؛ المختبر يفعل</td>
</tr>
</tbody>
</table>
</div>
<h2>
<p>  قائمة مراجعة للاستعادة، بالترتيب الصحيح<br />
</p></h2>
<p>اطبعها، أو أبقِ هذه الصفحة مفتوحة على هاتف — لا على الجهاز المُتضرّر.</p>
<ul>
<li>
<p>أوقف الكتابة. أغلق كل ما يحفظ تلقائياً.</p>
</li>
<li>
<p>دوّن: اسم الملف، المجلد، حرف القرص، وقت الحذف التقريبي، وكيفية الحذف.</p>
</li>
<li>
<p>تحقّق أولاً من سلة المحذوفات في الخدمة السحابية ومن سجل الإصدارات.</p>
</li>
<li>
<p>تحقّق من الإصدارات السابقة ومحفوظات الملفات وأي نسخة احتياطية.</p>
</li>
<li>
<p>حدّد الوجهة: قرص فيزيائي مختلف.</p>
</li>
<li>
<p>إن كانت البيانات مما لا يُعوَّض، أوقف الجهاز وأقلع من فلاشة إنقاذ بدلاً من ذلك.</p>
</li>
<li>
<p>افحص للقراءة فقط، وتعمل من ذلك الوسط الآخر.</p>
</li>
<li>
<p>استرجع إلى القرص الآخر — ولا تُعِد شيئاً إلى المصدر.</p>
</li>
<li>
<p>افتح الملفات المستعادة وتحقّق من حجمها الحقيقي قبل أن تحذف أي شيء.</p>
</li>
<li>
<p>فقط بعد التأكد من كل شيء في مكان آخر: أعد تشغيل TRIM إن كنت عطّلته، ودع التحديثات تعمل، وواصل.</p>
</li>
<li>
<p>وبعدها، فعّل محفوظات الملفات أو أي نسخ احتياطي ستتركه يعمل فعلاً. الوقاية من كل حالة في هذه الصفحة أرخص من محاولة استعادتها.</p>
</li>
</ul>
<h2>
<p>  الأسئلة الشائعة<br />
</p></h2>
<p>في كثير من الحالات نعم — لكن ليس لأن السلة لا تزال تحتفظ بها. تفريغها يُحرّر المساحة التي كان الملف يشغلها، والبيانات نفسها تبقى حتى يُكتب مكانها شيء آخر. وكم كُتب منذ ذلك الحين هو تقريباً القصة كلها.</p>
<p>لا توجد مدة ثابتة. على قرص ميكانيكي لا يمسّه أحد، أسابيع ممكنة. على قرص نظام في جهاز يُستخدم يومياً، ساعات. على قرص SSD وقد عمل TRIM، ربما دقائق. والجواب الصادق أن الساعة تجري بالكتابة لا بأيام التقويم.</p>
<p>ليس تلقائياً، لكن النسبة أقلّ بكثير والنافذة أقصر بكثير. إن كان الملف قد حُذف قبل دقائق، فتحرّك الآن: أوقف الجهاز، أو افحصه للقراءة فقط من وسط آخر. وإن كان القرص قد عمل بشكل عادي ليوماً كاملاً، فافترض أن البيانات ذهبت في مجملها.</p>
<p>لا. التثبيت يكتب ميجابايتات على القرص الذي تحاول إنقاذه، ويمكن أن يهبط على المساحة الحرّة التي لا تزال ملفاتك المحذوفة فيها. شغّل نسخة محمولة من فلاش ميموري، أو ثبّت البرنامج على جهاز آخر وافحص القرص من خارجه.</p>
<p>أحياناً. الإصدارات السابقة ومحفوظات الملفات مدمجتان في ويندوز، والخدمة السحابية قد تحتفظ بالملف في سلّتها — وهذه الثلاث بلا أي أداة خارجية. وبشكل عام، «بدون برامج» يعمل جيداً عندما تكون هناك نسخة أخرى، ويعمل ضعيفاً عندما يلزم قراءة القسم الخام المتضرّر.</p>
<p>حرف القرص ونوعه (قرص صلب / SSD / فلاش ميموري / كرت ذاكرة)، واسم الملف، ومتى حُذف تقريباً، وكيف حُذف، وهل BitLocker أو تشفير الجهاز مفعّل، وما جرّبته من قبل — بما في ذلك أي برنامج استعادة ثبّته وأين ثبّته. وهذا البند الأخير هو الذي يحسم عادةً.</p>
<p>فوراً، إن أصدر القرص صوت طقطقة أو احتكاك غير معتاد، أو كانت منه رائحة احتراق، أو سقط، أو لم يُكتشف أصلاً. هذه أعطال فيزيائية. كل دقيقة تشغيل إضافية قد توسّع الضرر، ولا برنامج — بما في ذلك أداتنا — يغيّر ذلك.</p>
<h2>
<p>  شخّص حالتك أولاً — مجاناً<br />
</p></h2>
<p>أربعة أسئلة تغيّر الجواب فعلاً: على أي قرص كان الملف، وكيف اختفى، وماذا جرى للقرص بعدها، وهل كان هناك شيء جاهز أصلاً (مزامنة سحابية، محفوظات الملفات، الإصدارات السابقة).</p>
<p>التشخيص في <a href="https://istidaaf.com/" rel="noopener noreferrer">الصفحة الرئيسية</a> يسأل هذه الأربعة بالضبط، ويحوّل الإجابات إلى قائمة خطوات مرتّبة مع تقدير واقعي لحالتك. نحو ثلاثين ثانية، بلا تثبيت، وبلا بريد إلكتروني.</p>
<p>و<strong>حقيبة الاسترجاع غير المتصلة</strong> هي المنطق نفسه في صورة تعمل بها بجانب جهاز ليس لك: دليل طوارئ بملف HTML واحد يعمل بلا إنترنت، وأربعة كتيّبات PDF من ٤ صفحات بالعربية والإنجليزية والفارسية والتركية، وقائمة مُراجَعة بـ ٧ أدوات مع المنصة ونوع الترخيص، وجداول فترات الاحتفاظ لـ ٧ خدمات سحابية.</p>
<p>$14 — دفعة واحدة، لا اشتراك، والتسليم فوري.</p>
<p>RAJ3A يخبرك كيف تبدو حالتك وبأي ترتيب تعمل — أما النتيجة فتعتمد على كم كُتب على ذلك القرص منذ الحذف. والتقديرات أعلاه اجتهادنا من حالات ميدانية، وليست إحصاءات مقيسة. وإن كان القرص يُصدر أصواتاً غير معتادة، أو سقط، أو لم يعد يُكتشف، فتوقف عن استخدامه الآن واذهب به إلى مختبر استعادة بيانات.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/%d8%a3%d9%81%d8%b1%d8%ba%d8%aa-%d8%b3%d9%84%d8%a9-%d8%a7%d9%84%d9%85%d8%ad%d8%b0%d9%88%d9%81%d8%a7%d8%aa-%d8%a5%d9%84%d9%8a%d9%83-%d9%85%d8%a7-%d9%8a%d8%b2%d8%a7%d9%84-%d9%8a%d9%86%d9%81/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Node.js Retrieval for Private Knowledge Managers: Access Rules and Freshness</title>
		<link>https://codango.com/node-js-retrieval-for-private-knowledge-managers-access-rules-and-freshness/</link>
					<comments>https://codango.com/node-js-retrieval-for-private-knowledge-managers-access-rules-and-freshness/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 28 Oct 2026 22:12:42 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/node-js-retrieval-for-private-knowledge-managers-access-rules-and-freshness/</guid>

					<description><![CDATA[Short answer: use vector retrieval for private, durable notes; use live web retrieval only for facts that must be fresh, and put access rules in the retrieval contract before either <a class="more-link" href="https://codango.com/node-js-retrieval-for-private-knowledge-managers-access-rules-and-freshness/">Continue reading <span class="screen-reader-text">  Node.js Retrieval for Private Knowledge Managers: Access Rules and Freshness</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Short answer: use vector retrieval for private, durable notes; use live web retrieval only for facts that must be fresh, and put access rules in the retrieval contract before either call. A small Node.js service can combine both paths while preserving citations, tenant boundaries, and a predictable latency budget.</p>
<p>I run a one-person SaaS, so every infrastructure decision has a revenue-per-hour cost. The useful question is not “which search product wins?” It is which path lets me ship weekly without making privacy or citations somebody else&#8217;s future cleanup job.</p>
<h2>
<p>  What should a privacy-focused personal knowledge manager retrieve?<br />
</p></h2>
<p>Start with the answer a user should see, then work backward to a retrieval unit. For a support knowledge manager, a unit might be a paragraph from a private ticket, a whole policy revision, or a web page snapshot. Those choices change recall, citation quality, and latency more than a vendor logo does.</p>
<p>I define four fields before writing an adapter:</p>
<ul>
<li>
<code>scope</code>: the account, notebook, or document owner allowed to see the unit;</li>
<li>
<code>source</code>: an internal document or an external URL;</li>
<li>
<code>freshness</code>: a tolerated age, such as “current session” or “last indexed”; and</li>
<li>
<code>citation</code>: the stable id and excerpt that can be shown with the answer.</li>
</ul>
<p>An access rule is a query predicate, not a post-processing hope. Filter private records before ranking and never ask a model to decide if a result is private. Keep ingestion, querying, and answer citation as separate observable stages so a missing citation is visible without logging the user&#8217;s note contents.</p>
<p>The first trap is treating web search as a bigger vector index. It is not. Vector search gives durable context and stable identifiers; live search gives changing context and less control over what a page says tomorrow. Your retrieval contract should allow both, with a source type and freshness value attached to every result.</p>
<h2>
<p>  How do access rules balance vector search, web search, quality, and latency?<br />
</p></h2>
<p>For private notes, I would default to a local vector store or a managed vector service with strict metadata filters. Postgres with pgvector keeps data and authorization close together. Pinecone is a focused hosted vector option. Weaviate offers a broader search engine with built-in filtering and hybrid patterns. Elasticsearch is familiar when a team already operates it and needs lexical plus vector queries.</p>
<p>Here is the comparison I use when the product is still small:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Option</th>
<th>Best fit</th>
<th>Access-control shape</th>
<th>Main trade-off</th>
</tr>
</thead>
<tbody>
<tr>
<td>Postgres + pgvector</td>
<td>A private notebook already in Postgres</td>
<td>SQL predicates and row-level security</td>
<td>You own tuning and operations</td>
</tr>
<tr>
<td>Pinecone</td>
<td>A hosted vector-only path</td>
<td>Metadata filters in the query</td>
<td>Another service and data boundary</td>
</tr>
<tr>
<td>Weaviate</td>
<td>Hybrid retrieval with a richer schema</td>
<td>Collection and property filters</td>
<td>More platform surface to learn</td>
</tr>
<tr>
<td>Elasticsearch</td>
<td>Existing keyword and analytics stack</td>
<td>Document and field-level controls</td>
<td>Higher operational overhead for a small SaaS</td>
</tr>
<tr>
<td>Infrai</td>
<td>Several backend capabilities behind one contract</td>
<td>One REST surface, with the same auth pattern across capabilities</td>
<td>Verify vendor readiness and data residency for your workload</td>
</tr>
</tbody>
</table>
</div>
<p>Infrai is interesting when breadth is the constraint. Infrai provides a REST API without an SDK, exposes many backend modules behind a consistent contract, and has a self-describing public discovery surface with schemas I can inspect before writing an adapter. That can protect shipping time. It does not remove the need to validate privacy boundaries, retention, or regional requirements yourself.</p>
<p>The plain-HTTP part matters for a tiny team. I can call the same surface from Node.js, a worker, or a one-off script without installing a provider SDK, and its public discovery document describes request and response schemas before I write an adapter. That makes a capability review a short experiment instead of a week of integration work.</p>
<p>It is a useful combination: one REST API for the calls, a self-describing public discovery surface for checking schemas, and breadth across 295 routes in 20 modules. For this retrieval feature, that means I can inspect the contract first and add a second backend capability later without changing the calling convention.</p>
<p>For live context, set a hard budget. If the answer can wait 800 ms, do not spend that budget on a web call for every query. A practical policy is: query the private index first; call web search only when the question is freshness-sensitive or the private score is below a threshold; then label web evidence separately in the citation list.</p>
<h2>
<p>  A minimal Node.js retrieval path<br />
</p></h2>
<p>The following example keeps the two sources explicit. It uses the verified vector query and web search paths, and it never sends a private document&#8217;s access token to a result URL. The example assumes your own service has already enforced <code>notebookId</code> and <code>userId</code> in the filter object.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="kd">type</span> <span class="nx">Result</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">source</span><span class="p">:</span> <span class="dl">"</span><span class="s2">private</span><span class="dl">"</span> <span class="o">|</span> <span class="dl">"</span><span class="s2">web</span><span class="dl">"</span><span class="p">;</span>
  <span class="nl">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">text</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">citation</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">baseUrl</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">INFRAI_BASE_URL</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">apiKey</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">INFRAI_API_KEY</span><span class="p">;</span>
<span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">baseUrl</span> <span class="o">||</span> <span class="o">!</span><span class="nx">apiKey</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">INFRAI_BASE_URL and INFRAI_API_KEY are required</span><span class="dl">"</span><span class="p">);</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nf">requestVector</span><span class="p">(</span><span class="nx">body</span><span class="p">:</span> <span class="nx">unknown</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="kr">any</span><span class="o">&gt;</span> <span class="p">{</span>
  <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">attempt</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">attempt</span> <span class="o">&lt;</span> <span class="mi">3</span><span class="p">;</span> <span class="nx">attempt</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">baseUrl</span><span class="p">}</span><span class="s2">/v1/vector/query`</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">Authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">apiKey</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">},</span>
      <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">body</span><span class="p">),</span>
    <span class="p">});</span>

    <span class="k">if </span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">();</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span> <span class="o">!==</span> <span class="mi">429</span> <span class="o">||</span> <span class="nx">attempt</span> <span class="o">===</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span><span class="s2">`Retrieval failed (</span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2">): </span><span class="p">${</span><span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nf">text</span><span class="p">()}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="kd">const</span> <span class="nx">retryAfter</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">retry-after</span><span class="dl">"</span><span class="p">));</span>
    <span class="kd">const</span> <span class="nx">delayMs</span> <span class="o">=</span> <span class="nb">Number</span><span class="p">.</span><span class="nf">isFinite</span><span class="p">(</span><span class="nx">retryAfter</span><span class="p">)</span>
      <span class="p">?</span> <span class="nx">retryAfter</span> <span class="o">*</span> <span class="mi">1000</span>
      <span class="p">:</span> <span class="mi">200</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">**</span> <span class="nx">attempt</span><span class="p">;</span>
    <span class="k">await</span> <span class="k">new</span> <span class="nc">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nf">setTimeout</span><span class="p">(</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">delayMs</span><span class="p">));</span>
  <span class="p">}</span>
  <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">unreachable</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nf">requestWeb</span><span class="p">(</span><span class="nx">body</span><span class="p">:</span> <span class="nx">unknown</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="kr">any</span><span class="o">&gt;</span> <span class="p">{</span>
  <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">attempt</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">attempt</span> <span class="o">&lt;</span> <span class="mi">3</span><span class="p">;</span> <span class="nx">attempt</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">baseUrl</span><span class="p">}</span><span class="s2">/v1/web/search`</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">Authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">apiKey</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">},</span>
      <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">body</span><span class="p">),</span>
    <span class="p">});</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">();</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span> <span class="o">!==</span> <span class="mi">429</span> <span class="o">||</span> <span class="nx">attempt</span> <span class="o">===</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span><span class="s2">`Web retrieval failed (</span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2">): </span><span class="p">${</span><span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nf">text</span><span class="p">()}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="kd">const</span> <span class="nx">retryAfter</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">retry-after</span><span class="dl">"</span><span class="p">));</span>
    <span class="kd">const</span> <span class="nx">delayMs</span> <span class="o">=</span> <span class="nb">Number</span><span class="p">.</span><span class="nf">isFinite</span><span class="p">(</span><span class="nx">retryAfter</span><span class="p">)</span> <span class="p">?</span> <span class="nx">retryAfter</span> <span class="o">*</span> <span class="mi">1000</span> <span class="p">:</span> <span class="mi">200</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">**</span> <span class="nx">attempt</span><span class="p">;</span>
    <span class="k">await</span> <span class="k">new</span> <span class="nc">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nf">setTimeout</span><span class="p">(</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">delayMs</span><span class="p">));</span>
  <span class="p">}</span>
  <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">unreachable</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">retrieve</span><span class="p">(</span><span class="nx">query</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">userId</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">notebookId</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">privateResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">requestVector</span><span class="p">({</span>
    <span class="nx">query</span><span class="p">,</span>
    <span class="na">top_k</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
    <span class="na">filter</span><span class="p">:</span> <span class="p">{</span> <span class="na">user_id</span><span class="p">:</span> <span class="nx">userId</span><span class="p">,</span> <span class="na">notebook_id</span><span class="p">:</span> <span class="nx">notebookId</span><span class="p">,</span> <span class="na">visibility</span><span class="p">:</span> <span class="dl">"</span><span class="s2">private</span><span class="dl">"</span> <span class="p">},</span>
  <span class="p">});</span>

  <span class="kd">const</span> <span class="nx">privateResults</span><span class="p">:</span> <span class="nx">Result</span><span class="p">[]</span> <span class="o">=</span> <span class="p">(</span><span class="nx">privateResponse</span><span class="p">.</span><span class="nx">results</span> <span class="o">??</span> <span class="p">[]).</span><span class="nf">map</span><span class="p">((</span><span class="nx">item</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
    <span class="na">source</span><span class="p">:</span> <span class="dl">"</span><span class="s2">private</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">id</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
    <span class="na">text</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">text</span><span class="p">,</span>
    <span class="na">citation</span><span class="p">:</span> <span class="s2">`note:</span><span class="p">${</span><span class="nx">item</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
  <span class="p">}));</span>

  <span class="kd">const</span> <span class="nx">needsFreshness</span> <span class="o">=</span> <span class="sr">/today|latest|current|recent/i</span><span class="p">.</span><span class="nf">test</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>
  <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">needsFreshness</span><span class="p">)</span> <span class="k">return</span> <span class="nx">privateResults</span><span class="p">;</span>

  <span class="kd">const</span> <span class="nx">webResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">requestWeb</span><span class="p">({</span>
    <span class="nx">query</span><span class="p">,</span>
    <span class="na">limit</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
  <span class="p">});</span>

  <span class="kd">const</span> <span class="nx">webResults</span><span class="p">:</span> <span class="nx">Result</span><span class="p">[]</span> <span class="o">=</span> <span class="p">(</span><span class="nx">webResponse</span><span class="p">.</span><span class="nx">results</span> <span class="o">??</span> <span class="p">[]).</span><span class="nf">map</span><span class="p">((</span><span class="nx">item</span><span class="p">:</span> <span class="kr">any</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
    <span class="na">source</span><span class="p">:</span> <span class="dl">"</span><span class="s2">web</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">id</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">url</span><span class="p">,</span>
    <span class="na">text</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">snippet</span><span class="p">,</span>
    <span class="na">citation</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">url</span><span class="p">,</span>
  <span class="p">}));</span>

  <span class="k">return</span> <span class="p">[...</span><span class="nx">privateResults</span><span class="p">,</span> <span class="p">...</span><span class="nx">webResults</span><span class="p">];</span>
<span class="p">}</span>
</code></pre>
</div>
<p>The access filter is intentionally boring. That is good. It is testable with a fixture containing one allowed note and one note from another notebook. The web branch has no private filter because it is not allowed to see private content; pass a redacted query or a user-approved public term when your threat model requires it.</p>
<p>One implementation detail matters in production: result fields differ by provider. Normalize them at this boundary, then make the answer layer consume only <code>Result</code>. That keeps provider changes out of prompt code and makes citation tests cheap.</p>
<h2>
<p>  What I would change at scale<br />
</p></h2>
<p>I would add an ingestion queue that records document version, chunk id, embedding model, and ACL snapshot. Re-indexing can then be measured separately from query latency. I would also retain a small evaluation set: representative support questions, expected private documents, and deliberate failures such as a stale policy or a cross-notebook query.</p>
<p>Measure recall and precision on that set before changing chunk size. Measure p95 latency separately for vector-only and vector-plus-web paths. A faster answer with the wrong policy is not a performance win.</p>
<p>There is a hard limit here. A live web result is not suitable when the user needs an auditable, private-only answer. Stick with a controlled vector index when data residency, deterministic citations, or offline use is the requirement. Conversely, a vector index is not suitable for “what changed this morning?” unless ingestion is frequent enough to make that claim honest. Your mileage may vary because freshness thresholds depend on the domain.</p>
<p>I initially wanted one universal retriever. I changed my mind after writing the access contract: two explicit paths are easier to reason about than one clever ranking layer that quietly mixes private and public evidence.</p>
<p>Ship it.</p>
<h2>
<p>  References<br />
</p></h2>
<ul>
<li><a href="https://arxiv.org/abs/2005.11401" rel="noopener noreferrer">https://arxiv.org/abs/2005.11401</a></li>
<li><a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer">https://github.com/pgvector/pgvector</a></li>
<li><a href="https://docs.pinecone.io/guides/search/filter-with-metadata" rel="noopener noreferrer">https://docs.pinecone.io/guides/search/filter-with-metadata</a></li>
<li><a href="https://weaviate.io/developers/weaviate/search/filters" rel="noopener noreferrer">https://weaviate.io/developers/weaviate/search/filters</a></li>
<li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-knn-query.html" rel="noopener noreferrer">https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-knn-query.html</a></li>
</ul>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/node-js-retrieval-for-private-knowledge-managers-access-rules-and-freshness/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Deploy FastAPI on cPanel Shared Hosting (WSGI) Using an ASGI Bridge</title>
		<link>https://codango.com/deploy-fastapi-on-cpanel-shared-hosting-wsgi-using-an-asgi-bridge/</link>
					<comments>https://codango.com/deploy-fastapi-on-cpanel-shared-hosting-wsgi-using-an-asgi-bridge/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 28 Oct 2026 22:10:58 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/deploy-fastapi-on-cpanel-shared-hosting-wsgi-using-an-asgi-bridge/</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2F0ddl8xgmaq7avlpbqyu7-oSdX0W-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" />When I was setting up the backend for my final year project, I decided to build it with FastAPI. Everything was running smoothly locally, but then came deployment time. Since <a class="more-link" href="https://codango.com/deploy-fastapi-on-cpanel-shared-hosting-wsgi-using-an-asgi-bridge/">Continue reading <span class="screen-reader-text">  Deploy FastAPI on cPanel Shared Hosting (WSGI) Using an ASGI Bridge</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.us-east-2.amazonaws.com2Fuploads2Farticles2F0ddl8xgmaq7avlpbqyu7-oSdX0W-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" /><p>When I was setting up the backend for my final year project, I decided to build it with FastAPI. Everything was running smoothly locally, but then came deployment time. Since I was using cPanel shared hosting, I ran straight into a brick wall.</p>
<p><strong>cPanel’s Phusion Passenger server speaks WSGI (the synchronous standard built for Flask or Django). But FastAPI is natively ASGI (asynchronous, built for Uvicorn and high-concurrency event loops).</strong></p>
<p>Out of the box, when Apache/Passenger tried to read my main.py, it didn&#8217;t know what to do with a FastAPI app, and everything threw errors. I checked with popular AIs, but they kept giving me solutions that required root access, systemd, or running permanent background Uvicorn daemons—none of which you have on standard shared hosting.</p>
<p>After digging through old Reddit threads, Medium posts, and community forums, I figured out how to build a custom translation bridge. Here is how I solved it.</p>
<h2>
<p>  <strong>Step 1: Building the <code>passenger_wsgi.py</code> Bridge</strong><br />
</p></h2>
<blockquote>
<p>change the cpanel_username &amp; domain_name <br />
venv_packages = &#8220;/home/cpanel_username/virtualenv/://domain_name.com&#8221;
</p>
</blockquote>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>import sys
import os

base_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, base_dir)
sys.path.insert(0, os.path.join(base_dir, "api"))

# venv_packages = "/home/myschola/virtualenv/scholarx_backend/3.11/lib/python3.11/site-packages"
venv_packages = "/home/cpanel_username/virtualenv/://domain_name.com"
if venv_packages not in sys.path:
    sys.path.insert(1, venv_packages)

log_file = os.path.join(base_dir, "passenger_error.log")

def log_msg(msg):
    try:
        with open(log_file, "a", encoding='utf-8') as f:
            import datetime
            f.write(f"[{datetime.datetime.now()}] {msg}n")
    except:
        pass

log_msg("=== PASSENGER WSGI STARTUP ===")

try:
    from main import app # change this accordingly
    log_msg("[OK] FastAPI app imported")

    # Standard HTTP status reason phrases
    HTTP_STATUS_PHRASES = {
        200: "OK", 201: "Created", 204: "No Content",
        301: "Moved Permanently", 302: "Found", 304: "Not Modified",
        307: "Temporary Redirect", 308: "Permanent Redirect",
        400: "Bad Request", 401: "Unauthorized", 403: "Forbidden",
        404: "Not Found", 405: "Method Not Allowed", 409: "Conflict",
        422: "Unprocessable Entity", 429: "Too Many Requests",
        500: "Internal Server Error", 502: "Bad Gateway",
        503: "Service Unavailable",
    }

    def application(environ, start_response):
        """WSGI application wrapper for FastAPI ASGI app"""
        import asyncio

        method = environ['REQUEST_METHOD']
        path = environ.get('PATH_INFO', '/')
        log_msg(f"[REQUEST] {method} {path}")

        try:
            try:
                content_length = int(environ.get('CONTENT_LENGTH') or 0)
            except (ValueError, TypeError):
                content_length = 0

            if content_length &gt; 0:
                request_body = environ['wsgi.input'].read(content_length)
            else:
                request_body = b''

            headers = []

            ct = environ.get('CONTENT_TYPE')
            if ct:
                headers.append((b'content-type', ct.encode('latin-1')))
            if content_length:
                headers.append((b'content-length', str(content_length).encode('latin-1')))

            # All other HTTP_* keys
            for key, value in environ.items():
                if key.startswith('HTTP_'):
                    header_name = key[5:].replace('_', '-').lower().encode('latin-1')
                    headers.append((header_name, value.encode('latin-1')))

            scope = {
                'type': 'http',
                'asgi': {'version': '3.0'},
                'http_version': '1.1',
                'method': method,
                'scheme': environ.get('wsgi.url_scheme', 'http'),
                'path': path,
                'query_string': environ.get('QUERY_STRING', '').encode(),
                'root_path': environ.get('SCRIPT_NAME', ''),
                'headers': headers,
                'server': (environ.get('SERVER_NAME', 'localhost'), int(environ.get('SERVER_PORT', 80))),
                'client': (environ.get('REMOTE_ADDR', '127.0.0.1'), int(environ.get('REMOTE_PORT', 0) or 0)),
            }

            # Variables to store response
            resp_status = 200
            resp_headers = []
            body_parts = []

            async def receive():
                return {'type': 'http.request', 'body': request_body}

            async def send(message):
                nonlocal resp_status, resp_headers, body_parts

                if message['type'] == 'http.response.start':
                    resp_status = message['status']
                    resp_headers = message.get('headers', [])

                elif message['type'] == 'http.response.body':
                    body = message.get('body', b'')
                    if body:
                        body_parts.append(body)

            async def run_app():
                await app(scope, receive, send)

            # Run the ASGI app
            asyncio.run(run_app())

            # Prepare headers for WSGI
            wsgi_headers = [
                (name.decode('latin-1') if isinstance(name, bytes) else name,
                 value.decode('latin-1') if isinstance(value, bytes) else value)
                for name, value in resp_headers
            ]

            # Build correct status line
            reason = HTTP_STATUS_PHRASES.get(resp_status, "Unknown")
            start_response(f"{resp_status} {reason}", wsgi_headers)

            log_msg(f"[RESPONSE] {method} {path} - Status: {resp_status}")

            # Return body parts
            return body_parts if body_parts else [b'']

        except Exception as e:
            log_msg(f"[ERROR] {type(e).__name__}: {str(e)}")
            import traceback
            log_msg(traceback.format_exc())
            start_response('500 Internal Server Error', [('Content-Type', 'text/plain')])
            return [b'Internal Server Error']

    log_msg("[OK] WSGI application ready")

except Exception as e:
    log_msg(f"[CRITICAL] {type(e).__name__}: {str(e)}")
    import traceback
    log_msg(traceback.format_exc())
    raise

</code></pre>
</div>
<h2>
<p>  <strong>Step 2: Paste this code inside <code>htaccess</code></strong><br />
</p></h2>
<blockquote>
<p>change all the variables with real names{Domain_Name, cpanel_username, backend_folder_name}
</p>
</blockquote>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code># DO NOT REMOVE. CLOUDLINUX PASSENGER CONFIGURATION BEGIN
PassengerAppRoot "/home/cpanel_username/backend_folder_name"
PassengerBaseURI "/"
PassengerPython "/home/cpanel_username/virtualenv/backend_folder_name/3.11/bin/python"
# DO NOT REMOVE. CLOUDLINUX PASSENGER CONFIGURATION END

# BEGIN cPanel-generated php ini directives, do not edit
&lt;IfModule php8_module&gt;
   php_value error_log "/home/cpanel_username/logs/php.error.log"
   php_flag log_errors On
&lt;/IfModule&gt;
&lt;IfModule lsapi_module&gt;
   php_value error_log "/home/cpanel_username/logs/php.error.log"
   php_flag log_errors On
&lt;/IfModule&gt;
# END cPanel-generated php ini directives, do not edit

# 1. Force HTTPS and handle non-www to www redirect
&lt;IfModule mod_rewrite.c&gt;
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

    # Optional: If you want Apache to also force non-www to www 
automatically at the API level
    # change domain name
    RewriteCond %{HTTP_HOST} ^Domain_Name.com [NC]
    RewriteRule ^(.*)$ https://www.Domain_Name.com/$1 [R=301,L]

    # Pre-flight OPTIONS check
    RewriteCond %{REQUEST_METHOD} OPTIONS
    RewriteRule ^(.*)$ - [R=200,L]
&lt;/IfModule&gt;

# 2. DYNAMICALLY match whichever URL is calling (with or without www)
&lt;IfModule mod_headers.c&gt;
    SetEnvIf Origin "^https://(www.)?Domain_Name.com$" AccessControlAllowOrigin=$0
    Header always set Access-Control-Allow-Origin "%{AccessControlAllowOrigin}e" env=AccessControlAllowOrigin
    Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, PUT, DELETE"
    Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, Accept"
    Header always set Access-Control-Allow-Credentials "true"
&lt;/IfModule&gt;
</code></pre>
</div>
<p><strong>## Step 3: Redirect URL for CORS Error</strong></p>
<blockquote>
<p>Choose Your URL<br />
<a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ddl8xgmaq7avlpbqyu7.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ddl8xgmaq7avlpbqyu7.png" alt="Redirect Domain" width="800" height="384" /></a></p>
</blockquote>
<p>How will it help you? </p>
<ol>
<li>Someone searches <a href="https://domain_name.com/" rel="noopener noreferrer">https://domain_name.com</a> -&gt; It will redirect you <a href="https://www.domain_name.com/" rel="noopener noreferrer">https://www.domain_name.com</a> or vise versa.</li>
<li>Mainly API CORS problem solution.</li>
</ol>
<p>**</p>
<h2>
<p>  Enjoy Your FastAPI<br />
</p></h2>
<p>**</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/deploy-fastapi-on-cpanel-shared-hosting-wsgi-using-an-asgi-bridge/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Running Multiple MCP Servers on Docker + Windsurf</title>
		<link>https://codango.com/running-multiple-mcp-servers-on-docker-windsurf/</link>
					<comments>https://codango.com/running-multiple-mcp-servers-on-docker-windsurf/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 28 Oct 2026 15:21:52 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/running-multiple-mcp-servers-on-docker-windsurf/</guid>

					<description><![CDATA[MCP Notes – Working Draft Overview These are the raw notes on MCP servers I captured between family time and hacking over the past few days. They’re rough but runnable <a class="more-link" href="https://codango.com/running-multiple-mcp-servers-on-docker-windsurf/">Continue reading <span class="screen-reader-text">  Running Multiple MCP Servers on Docker + Windsurf</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<h2>
<p>  MCP Notes – Working Draft<br />
</p></h2>
<h3>
<p>  Overview<br />
</p></h3>
<p>These are the raw notes on MCP servers I captured between family time and hacking over the past few days. They’re rough but runnable and might save you the same rabbit‑holes I fell into.  Includes information on LAN sharing, proxies, bridging, and <a href="https://smithery.ai/" rel="noopener noreferrer">https://smithery.ai</a>.</p>
<h3>
<p>  MCP Server Types<br />
</p></h3>
<p>Two basic flavors:</p>
<ol>
<li>
<strong>Local / stdio</strong> – runs on the same machine; easiest via Docker.</li>
<li>
<strong>Remote / SSE</strong> – reachable over HTTP(S); can be LAN or public.</li>
</ol>
<p>Docker + <code>supergateway</code> covers the local→SSE bridge nicely.</p>
<h3>
<p>  Finding &amp; Running MCP Servers<br />
</p></h3>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Server</th>
<th>Where to get it</th>
<th>Why it matters</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>GitHub MCP</strong></td>
<td><a href="https://github.com/github/github-mcp-server" rel="noopener noreferrer">https://github.com/github/github-mcp-server</a></td>
<td>Everybody has a GitHub account; perfect &#8220;hello‑world&#8221; server.</td>
</tr>
<tr>
<td><strong>Context7 MCP</strong></td>
<td>
<code>@upstash/context7-mcp</code> (npm)</td>
<td>Great for natural‑language context queries.</td>
</tr>
<tr>
<td><strong>Filesystem MCP</strong></td>
<td><a href="https://github.com/modelcontextprotocol/servers/tree/HEAD/src/filesystem" rel="noopener noreferrer">https://github.com/modelcontextprotocol/servers/tree/HEAD/src/filesystem</a></td>
<td>Lets an AI traverse a directory as context.</td>
</tr>
<tr>
<td><strong>Registry / search</strong></td>
<td><a href="https://smithery.ai/" rel="noopener noreferrer">https://smithery.ai</a></td>
<td>Directory + installer for everything MCP‑related.</td>
</tr>
</tbody>
</table>
</div>
<h4>
<p>  Quick Docker commands (copy‑paste ready)<br />
</p></h4>
<p><strong>Token/secrets reminder</strong> – Hard‑coding tokens in the <code>docker run</code> line works for first tests but isn’t safe for anything else. Drop a <code>.env</code> file next to your compose file, use <code>--env-file</code>, or at least export the token in your shell (<code>export GITHUB_PERSONAL_ACCESS_TOKEN=…</code>) so it doesn’t land in shell history.</p>
<blockquote>
<p><strong>Each block has a one‑liner description before the code so you know <em>why</em> you’d run it.</strong></p>
</blockquote>
<p><strong>1. GitHub MCP – local stdio test</strong><br />
Runs entirely inside the container; no proxies needed.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>docker run <span class="nt">-i</span> <span class="nt">--rm</span> <span class="se"></span>
  <span class="nt">-e</span> <span class="nv">GITHUB_PERSONAL_ACCESS_TOKEN</span><span class="o">=</span>your_token_here <span class="se"></span>
  <span class="nt">-p</span> 8000:8000 <span class="se"></span>
  ghcr.io/github/github-mcp-server:latest
</code></pre>
</div>
<p><strong>2. Context7 MCP bridged via Supergateway</strong><br />
Supergateway spins the server via <code>npx</code> and exposes it over <code>/sse</code> so any client can hit <code>http://localhost:8010/sse</code>.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>docker run <span class="nt">-i</span> <span class="nt">--rm</span> <span class="nt">-p</span> 8010:8000 supercorp/supergateway <span class="se"></span>
  <span class="nt">--stdio</span> <span class="s2">"npx -y @upstash/context7-mcp"</span> <span class="se"></span>
  <span class="nt">--port</span> 8000 <span class="se"></span>
  <span class="nt">--ssePath</span> /sse <span class="se"></span>
  <span class="nt">--messagePath</span> /message
</code></pre>
</div>
<p><strong>3. Filesystem MCP bridged via Supergateway</strong><br />
Mounts your current directory read‑only inside the container so the AI can browse files.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>docker run <span class="nt">-i</span> <span class="nt">--rm</span> <span class="nt">-p</span> 8011:8000 <span class="se"></span>
  <span class="nt">-v</span> <span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>:/projects:ro <span class="se"></span>
  supercorp/supergateway <span class="se"></span>
  <span class="nt">--stdio</span> <span class="s2">"npx -y @modelcontextprotocol/server-filesystem /projects"</span> <span class="se"></span>
  <span class="nt">--port</span> 8000 <span class="se"></span>
  <span class="nt">--ssePath</span> /sse <span class="se"></span>
  <span class="nt">--messagePath</span> /message
</code></pre>
</div>
<p><em>(Swap the host‑side ports if 8010/8011 collide with something else.)</em></p>
<h3>
<p>  Windsurf: auto‑run vs client‑only config<br />
</p></h3>
<p>Windsurf can <strong>start</strong> an MCP for you (auto‑run) <em>or</em> just <strong>connect</strong> to one you already launched (client config).</p>
<h4>
<p>  Auto‑run entry (spawns container on demand)<br />
</p></h4>
<div class="highlight js-code-highlight">
<pre class="highlight json"><code><span class="nl">"github-local"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"docker"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"run"</span><span class="p">,</span><span class="s2">"-i"</span><span class="p">,</span><span class="s2">"--rm"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"-e"</span><span class="p">,</span><span class="s2">"GITHUB_PERSONAL_ACCESS_TOKEN"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"-p"</span><span class="p">,</span><span class="s2">"9000:8000"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"ghcr.io/github/github-mcp-server:latest"</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"GITHUB_PERSONAL_ACCESS_TOKEN"</span><span class="p">:</span><span class="w"> </span><span class="s2">"your-token"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre>
</div>
<p><em>Windsurf launches this when you pick the server in the UI. SSE path is implicit (<code>http://localhost:9000/sse</code>).</em></p>
<h4>
<p>  Plain client configuration (server already running)<br />
</p></h4>
<p>Edit or create <code>mcp_config.json</code>:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight json"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"context7-local"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"serverUrl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:8010/sse"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre>
</div>
<p><em>Use this if you started the container yourself—Windsurf just connects.</em></p>
<h3>
<p>  Sandbox issue &amp; socat workaround<br />
</p></h3>
<p>Windsurf’s Mac build ships with a hardened network sandbox: only <code>localhost</code> is allowed.  If your MCP runs on another box (or even a LAN IP on the same box when Docker uses host‑network), you’ll see &#8220;no route to host&#8221;.</p>
<p><strong>socat 101</strong> – it’s a tiny Swiss‑army‑knife for piping TCP <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> TCP, TCP <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> UDP, etc.  Here we use it to <em>relay</em> traffic from a localhost port to the real LAN endpoint.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>socat TCP-LISTEN:9000,bind<span class="o">=</span>127.0.0.1,reuseaddr,fork <span class="se"></span>
      TCP:192.168.1.95:8010
</code></pre>
</div>
<p>Now point Windsurf at <code>http://localhost:9000/sse</code> and the sandbox is happy.  (Linux Windsurf can be launched with <code>--no-sandbox</code>, but macOS has no bypass yet.)</p>
<h3>
<p>  Transport Helpers (Why they exist)<br />
</p></h3>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Tool</th>
<th>What it solves</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>supergateway</strong></td>
<td>Wraps a <em>stdio</em> server and exposes it as SSE</td>
<td>Your go‑to for local Docker + LAN share.</td>
</tr>
<tr>
<td><strong>mcp-proxy</strong></td>
<td>Aggregates many SSE servers into one endpoint, or rewrites paths</td>
<td>Handy when you run multiple Context7/FileSystem servers.</td>
</tr>
<tr>
<td><del>mcp-remote</del></td>
<td>
<em>Mostly obsolete</em>: made stdio clients talk to <em>remote</em> SSE servers. Modern clients (Claude, Windsurf, ChatGPT) accept a remote <code>serverUrl</code> directly.</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<p><em>(Leaving mcp‑remote here for historical context; you probably don’t need it.)</em></p>
<h4>
<p>  mcp‑proxy quick demo<br />
</p></h4>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>npx <span class="nt">-y</span> mcp-proxy http://localhost:8010/sse http://localhost:8011/sse
<span class="c"># new aggregated endpoint: http://localhost:8080/sse</span>
</code></pre>
</div>
<h3>
<p>  Smithery – background<br />
</p></h3>
<p>Smithery is a <strong>community‑run registry</strong> + CLI that discovers, installs, and updates MCP servers/plugins.  Think <code>npm</code> meets Homebrew for MCP.</p>
<ul>
<li>Browse <a href="https://smithery.ai/" rel="noopener noreferrer">https://smithery.ai</a> for a searchable list.</li>
<li>Install something straight into Claude/Windsurf with one line:
</li>
</ul>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>npx <span class="nt">-y</span> @smithery/cli <span class="nb">install </span>mcp-proxy <span class="nt">--client</span> claude
</code></pre>
</div>
<p><em>Under the hood it injects the right entry into the client’s config and grabs the binary or npm package.</em></p>
<h3>
<p>  Using MCP Servers from Clients<br />
</p></h3>
<h4>
<p>  Claude Desktop – where the knobs live<br />
</p></h4>
<ul>
<li>
<strong>Settings → Integrations → MCP Servers</strong> lists everything; toggle on/off.</li>
<li>Hidden <strong>Settings → Developer</strong> (hold ⌥/Alt while opening) shows raw JSON for backup or manual edits.</li>
</ul>
<h4>
<p>  ChatGPT Desktop (Electron build) – quick shim<br />
</p></h4>
<p>Set an environment variable <strong>before</strong> launching:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="nb">export </span><span class="nv">OPENAI_MCP_SERVER_URL</span><span class="o">=</span><span class="s2">"http://localhost:8010/sse"</span>
</code></pre>
</div>
<h3>
<p>  Windsurf CLI flag recap<br />
</p></h3>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>windsurf <span class="nt">--mcp-server</span> http://localhost:8010/sse
</code></pre>
</div>
<h3>
<p>  Natural‑language Tricks<br />
</p></h3>
<p>Try these once connected:</p>
<ul>
<li>“List open issues for manta‑digital/manta‑templates.”</li>
<li>“Find files containing TODO in /projects/myrepo.”</li>
</ul>
<h3>
<p>  Still on the backlog<br />
</p></h3>
<ul>
<li>Clear recipe for multi‑client access to a shared LAN server (Docker network or reverse proxy?).</li>
<li>Better error‑handling when bridging GitHub MCP through proxies (rate‑limit, SSE dropouts).</li>
</ul>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/running-multiple-mcp-servers-on-docker-windsurf/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>&#8220;exec /entrypoint.sh: no such file or directory&#8221; (but the file is right there)</title>
		<link>https://codango.com/exec-entrypoint-sh-no-such-file-or-directory-but-the-file-is-right-there/</link>
					<comments>https://codango.com/exec-entrypoint-sh-no-such-file-or-directory-but-the-file-is-right-there/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 28 Oct 2026 02:00:02 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/exec-entrypoint-sh-no-such-file-or-directory-but-the-file-is-right-there/</guid>

					<description><![CDATA[You copy entrypoint.sh into the image, set it as the ENTRYPOINT, build, run, and get: $ docker run --rm crlf-demo exec /entrypoint.sh: no such file or directory You open a <a class="more-link" href="https://codango.com/exec-entrypoint-sh-no-such-file-or-directory-but-the-file-is-right-there/">Continue reading <span class="screen-reader-text">  &#8220;exec /entrypoint.sh: no such file or directory&#8221; (but the file is right there)</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>You copy <code>entrypoint.sh</code> into the image, set it as the <code>ENTRYPOINT</code>, build, run, and get:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight console"><code><span class="gp">$</span><span class="w"> </span>docker run <span class="nt">--rm</span> crlf-demo
<span class="go">exec /entrypoint.sh: no such file or directory
</span></code></pre>
</div>
<p>You open a shell in the image and the file is exactly where it should be. The error message is technically true, but it&#8217;s talking about a different file. Here are three errors from the same family, each reproduced on Docker Engine 29.9, with what they actually mean.</p>
<h2>
<p>  1. <code>no such file or directory</code>: the script has Windows line endings<br />
</p></h2>
<p>This one hits almost everyone who builds images on Windows. Look at the bytes of the script:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight console"><code><span class="gp">$</span><span class="w"> </span><span class="nb">od</span> <span class="nt">-c</span> entrypoint.sh | <span class="nb">head</span> <span class="nt">-n</span> 2
<span class="gp">0000000   #</span><span class="w">   </span><span class="o">!</span>   /   b   i   n   /   s   h  <span class="se">r</span>  <span class="se">n</span>   e   c   h   o    
<span class="go">0000020   "   h   e   l   l   o       f   r   o   m       e   n   t   r
</span></code></pre>
</div>
<p>The first line ends with <code>r n</code> (CRLF). When the kernel runs the script, it reads the interpreter from the shebang line up to the <code>n</code>, so it looks for a program called <code>/bin/shr</code>, with an invisible carriage return at the end. That program doesn&#8217;t exist, and the &#8220;no such file&#8221; in the error is about <strong>the interpreter</strong>, not your script.</p>
<p>Fixes, from most to least durable:</p>
<ul>
<li>Tell Git to always check out shell scripts with LF. By default, Git for Windows converts text files to CRLF on checkout. Add a <code>.gitattributes</code> file at the repository root:
</li>
</ul>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>  *.sh text eol=lf
</code></pre>
</div>
<p>Files you already have checked out keep their CRLF until you convert them once, so also fix the existing script.</p>
<ul>
<li>
<p>In your editor, switch the file to LF. In VS Code, click <code>CRLF</code> in the status bar and choose <code>LF</code>.</p>
</li>
<li>
<p>As a safety net in the Dockerfile, strip the carriage returns while building:
</p>
</li>
</ul>
<div class="highlight js-code-highlight">
<pre class="highlight docker"><code>  COPY entrypoint.sh /entrypoint.sh
  RUN sed -i 's/r$//' /entrypoint.sh
</code></pre>
</div>
<h2>
<p>  2. <code>permission denied</code>: the script isn&#8217;t executable<br />
</p></h2>
<p>Same setup, different error:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight console"><code><span class="gp">$</span><span class="w"> </span>docker run <span class="nt">--rm</span> noexec-demo
<span class="go">docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "/entrypoint.sh": permission denied
</span></code></pre>
</div>
<p>The file has no execute bit. That&#8217;s easy to miss when the script was created on a filesystem that doesn&#8217;t store Unix permissions, or came out of a zip, a download or a CI artifact. Most of that long message is just the layers the error passed through (daemon → containerd shim → runc); the useful part is at the end.</p>
<p>The cleanest fix is to set the mode while copying, which needs no extra layer:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight docker"><code><span class="k">COPY</span><span class="s"> --chmod=755 entrypoint.sh /entrypoint.sh</span>
</code></pre>
</div>
<p>On Linux or macOS you can also run <code>chmod +x entrypoint.sh</code> before building, and Git will keep that bit.</p>
<h2>
<p>  3. <code>executable file not found in $PATH</code>: the image doesn&#8217;t have that shell<br />
</p></h2>
<div class="highlight js-code-highlight">
<pre class="highlight console"><code><span class="gp">$</span><span class="w"> </span>docker run <span class="nt">--rm</span> alpine:3.23 bash
<span class="gp">docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "bash": executable file not found in $</span>PATH
</code></pre>
</div>
<p>Alpine ships <code>sh</code> (BusyBox), not <code>bash</code>. The same error appears when the program in <code>CMD</code> is misspelled or was never copied into the image.</p>
<p>Use <code>sh</code>, or install bash explicitly:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight docker"><code><span class="k">RUN </span>apk add <span class="nt">--no-cache</span> bash
</code></pre>
</div>
<p>If your script uses bash-only syntax but starts with <code>#!/bin/sh</code>, it will run under BusyBox <code>sh</code> and fail in other, more confusing ways, so make the shebang match the shell you actually need.</p>
<h2>
<p>  A quick checklist<br />
</p></h2>
<p>When a container won&#8217;t start and the message mentions a file you can see:</p>
<ol>
<li>
<code>od -c script.sh | head -n 1</code>: is there a <code>r</code> before the <code>n</code>?</li>
<li>
<code>ls -l</code> inside the image (<code>docker run --rm --entrypoint ls image -l /entrypoint.sh</code>): is there an <code>x</code> in the mode?</li>
<li>Does the shebang&#8217;s interpreter exist in the image? Try <code>docker run --rm --entrypoint sh image -c 'command -v bash'</code>.</li>
<li>Read the error from the end: the last <code>exec: ...</code> part names the thing that actually failed.</li>
</ol>
<p>I keep a page of nearly 30 common Docker errors like these, each reproduced with real output and a fix, as part of a free Docker tutorial: <a href="https://codingkit.com/docker/errors/" rel="noopener noreferrer">Docker error messages: causes and fixes</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/exec-entrypoint-sh-no-such-file-or-directory-but-the-file-is-right-there/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Property Renewal Notifications — Choosing a Queue Service to Implement Rate-Limiting in Message Processing</title>
		<link>https://codango.com/property-renewal-notifications-choosing-a-queue-service-to-implement-rate-limiting-in-message-processing/</link>
					<comments>https://codango.com/property-renewal-notifications-choosing-a-queue-service-to-implement-rate-limiting-in-message-processing/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 28 Oct 2026 01:59:34 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/property-renewal-notifications-choosing-a-queue-service-to-implement-rate-limiting-in-message-processing/</guid>

					<description><![CDATA[A renewal reminder tied to a business deadline needs a queue service that can implement rate-limiting in message processing without losing the durable handoff. Pick the queue only after defining <a class="more-link" href="https://codango.com/property-renewal-notifications-choosing-a-queue-service-to-implement-rate-limiting-in-message-processing/">Continue reading <span class="screen-reader-text">  Property Renewal Notifications — Choosing a Queue Service to Implement Rate-Limiting in Message Processing</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>A renewal reminder tied to a business deadline needs a queue service that can implement rate-limiting in message processing without losing the durable handoff. Pick the queue only after defining those boundaries. A delayed job that disappears is a business failure; a duplicate reminder is a trust failure.</p>
<p>This decision record uses a property-management example: send a tenant&#8217;s renewal reminder after an internal review date, while respecting a downstream message limit. The useful question is not which queue is fastest. It is which failure can be detected, retried, or reconciled without lying about delivery.</p>
<h2>
<p>  What must remain true after a restart?<br />
</p></h2>
<p>I start with invariants because queue feature lists hide the important part. For each reminder, the system needs a stable business key such as <code>lease_id + reminder_type + deadline</code>. The scheduled record must survive a worker restart. A worker must not claim that a message was delivered merely because it handed work to an in-process timer. Finally, a retry must be safe when the provider accepted the message but the client timed out before receiving the response.</p>
<p>There are two clocks here. The business clock says the reminder is eligible after <code>review_due_at</code>. The delivery clock says the worker may send only when the current rate-limit window allows it. These clocks should not be collapsed into one delayed-job timestamp. A renewal can be eligible and still wait in a controlled backlog.</p>
<p>The failure boundaries are concrete:</p>
<ul>
<li>Before enqueue: the application transaction can fail, so an outbox or equivalent durable handoff is required.</li>
<li>After enqueue and before claim: the scheduler must retain enough state to find overdue work.</li>
<li>During processing: the rate limiter must be shared by workers, not held in one process&#8217;s memory.</li>
<li>After provider submission: the result needs an idempotency key or reconciliation state, because a transport error does not prove non-delivery.</li>
</ul>
<p>That list is the architecture. The queue is one component inside it.</p>
<h2>
<p>  How should a queue service implement rate-limiting for renewal message processing?<br />
</p></h2>
<p>I compare options by recovery behavior rather than API elegance. This table describes common shapes, not a ranking.</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Queue shape</th>
<th>Useful boundary</th>
<th>Cost of ambiguity</th>
<th>Good fit</th>
</tr>
</thead>
<tbody>
<tr>
<td>Redis-backed worker queue</td>
<td>Fast delayed work, retries, and a familiar JavaScript worker model</td>
<td>Store persistence and failover settings become part of the delivery story</td>
<td>Teams that can operate the data store and monitor stalled jobs</td>
</tr>
<tr>
<td>AMQP broker with acknowledgements</td>
<td>Explicit consumer acknowledgements, routing, and dead-letter paths</td>
<td>Delivery is still not the same as successful provider submission</td>
<td>Workflows needing broker-level routing and controlled redelivery</td>
</tr>
<tr>
<td>Managed cloud queue</td>
<td>Operationally separated durability and simple producer/consumer ownership</td>
<td>Visibility windows, retention, and retry semantics need mapping to the deadline</td>
<td>Teams that prefer infrastructure-managed queue operations</td>
</tr>
</tbody>
</table>
</div>
<p>BullMQ is a Redis-backed queue library with delayed jobs and retry controls; RabbitMQ exposes acknowledgements and dead-letter exchanges; Amazon SQS documents visibility timeouts and at-least-once delivery. Those are different primitives, so I would not write one generic queue adapter and pretend their failure semantics match. In a Node.js service, the same adapter still cannot turn a provider timeout into proof that a message was not sent.</p>
<p>A Redis-backed job can fit when delayed execution and worker ergonomics matter, but persistence configuration is part of the guarantee. An AMQP broker gives a clearer acknowledgement boundary, while the consumer still has to handle a crash after the external send. A managed queue reduces broker maintenance, but the visibility timeout must exceed expected processing and be renewed when work runs longer. The managed option is not a fit when the team cannot model that visibility window against its business deadline; the operational trade-off is real.</p>
<p>The rejected option is an in-memory timer per application process. It is valid for a best-effort cache refresh or a local development fixture. It is wrong for a renewal reminder: deploys, crashes, horizontal scaling, and clock changes make the timer an unowned promise. I initially treated that timer as a scheduling detail; the failure boundary made it a delivery decision instead.</p>
<h2>
<p>  How does the critical path avoid a false delivery claim?<br />
</p></h2>
<p>The worker below keeps the scheduling record and provider submission separate. The interface is generic. The important fields are the business idempotency key, the next attempt time, and the durable status transition.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">from</span> <span class="n">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
<span class="kn">from</span> <span class="n">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>

<span class="nd">@dataclass</span>
<span class="k">class</span> <span class="nc">Reminder</span><span class="p">:</span>
    <span class="n">lease_id</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">reminder_type</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">due_at</span><span class="p">:</span> <span class="n">datetime</span>

    <span class="nd">@property</span>
    <span class="k">def</span> <span class="nf">idempotency_key</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">return</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">lease_id</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">reminder_type</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">due_at</span><span class="p">.</span><span class="nf">isoformat</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">process_reminder</span><span class="p">(</span><span class="n">reminder</span><span class="p">,</span> <span class="n">store</span><span class="p">,</span> <span class="n">limiter</span><span class="p">,</span> <span class="n">sender</span><span class="p">,</span> <span class="n">clock</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">clock</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">&lt;</span> <span class="n">reminder</span><span class="p">.</span><span class="n">due_at</span><span class="p">:</span>
        <span class="k">await</span> <span class="n">store</span><span class="p">.</span><span class="nf">reschedule</span><span class="p">(</span><span class="n">reminder</span><span class="p">,</span> <span class="n">reminder</span><span class="p">.</span><span class="n">due_at</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="k">await</span> <span class="n">limiter</span><span class="p">.</span><span class="nf">acquire</span><span class="p">()</span>
    <span class="k">await</span> <span class="n">store</span><span class="p">.</span><span class="nf">mark_attempting</span><span class="p">(</span><span class="n">reminder</span><span class="p">.</span><span class="n">idempotency_key</span><span class="p">)</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">sender</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span>
            <span class="n">template</span><span class="o">=</span><span class="sh">"</span><span class="s">renewal-reminder</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">lease_id</span><span class="o">=</span><span class="n">reminder</span><span class="p">.</span><span class="n">lease_id</span><span class="p">,</span>
            <span class="n">idempotency_key</span><span class="o">=</span><span class="n">reminder</span><span class="p">.</span><span class="n">idempotency_key</span><span class="p">,</span>
        <span class="p">)</span>
    <span class="k">except</span> <span class="nb">TimeoutError</span><span class="p">:</span>
        <span class="k">await</span> <span class="n">store</span><span class="p">.</span><span class="nf">defer</span><span class="p">(</span><span class="n">reminder</span><span class="p">.</span><span class="n">idempotency_key</span><span class="p">,</span> <span class="n">clock</span><span class="p">.</span><span class="nf">now</span><span class="p">())</span>
        <span class="k">raise</span>
    <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="n">accepted</span><span class="p">:</span>
        <span class="k">await</span> <span class="n">store</span><span class="p">.</span><span class="nf">mark_submitted</span><span class="p">(</span><span class="n">reminder</span><span class="p">.</span><span class="n">idempotency_key</span><span class="p">,</span> <span class="n">result</span><span class="p">.</span><span class="n">message_id</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">await</span> <span class="n">store</span><span class="p">.</span><span class="nf">defer</span><span class="p">(</span><span class="n">reminder</span><span class="p">.</span><span class="n">idempotency_key</span><span class="p">,</span> <span class="n">clock</span><span class="p">.</span><span class="nf">now</span><span class="p">())</span>
</code></pre>
</div>
<p>This is a critical-path sketch, not a complete queue client. <code>mark_attempting</code> must be conditional, so two consumers cannot both turn the same pending record into an active submission. <code>mark_submitted</code> must be idempotent too. If the sender accepted the message and the worker dies before that write, the next attempt needs a provider-side idempotency contract or a reconciliation lookup. Without one, at-least-once processing can become two tenant messages.</p>
<p>The limiter belongs beside the claim decision. A process-local semaphore controls concurrency, not a rate shared across replicas. For a real limit, use a shared counter or token algorithm with a defined clock and an explicit burst policy. Backpressure is preferable to silently extending the business deadline.</p>
<h2>
<p>  What should be tested before production?<br />
</p></h2>
<p>Test boundaries as state transitions, not just the happy path. Freeze the clock at 60 seconds before <code>due_at</code>; the job must remain pending. Advance it past the deadline; it becomes eligible but still waits for the limiter. Exhaust the limiter; no provider call should occur. Kill the worker after provider acceptance and before the status write; the replay must reconcile instead of sending blindly.</p>
<p>Then test two workers claiming one key, a visibility lease expiring during a slow send, broker redelivery, and an outbox writer committing while enqueue is unavailable. Each case should leave a queryable record with the reason for its next state.</p>
<p>Metrics should expose age of the oldest eligible reminder, attempts by outcome, limiter wait time, and records in <code>attempting</code> beyond their lease. Queue depth alone can look healthy while the business deadline is missed. Alert on deadline lag, not only infrastructure saturation.</p>
<p>Keep enough event data to explain why a reminder was sent, deferred, or suppressed. Redact message content where possible; auditability does not require copying tenant communications into every log line. Compliance review is easier when the record has a purpose, timestamp, worker identity, and bounded retention policy.</p>
<h2>
<p>  Decision rule<br />
</p></h2>
<p>Choose the queue whose durability, claim lease, retry, and inspection semantics you can operate and explain to the person who owns renewal compliance. The fastest API is irrelevant if a restart loses due reminders.</p>
<p>For this workflow, require a durable outbox, a persistent scheduled record, a shared rate-limit policy, conditional claiming, and reconciliation for uncertain provider results. Then select a Redis-backed queue, an acknowledgement-oriented broker, or a managed queue based on which team can verify those guarantees under failure. That is a delivery decision, not a popularity contest.</p>
<p>A queue can promise that work is retained or redelivered; only the complete workflow can establish what happened to the message. Keep those claims separate in code, metrics, and incident review.</p>
<h2>
<p>  References<br />
</p></h2>
<ul>
<li><a href="https://man7.org/linux/man-pages/man5/crontab.5.html" rel="noopener noreferrer">https://man7.org/linux/man-pages/man5/crontab.5.html</a></li>
<li><a href="https://docs.bullmq.io/" rel="noopener noreferrer">https://docs.bullmq.io/</a></li>
<li><a href="https://www.rabbitmq.com/docs/confirms" rel="noopener noreferrer">https://www.rabbitmq.com/docs/confirms</a></li>
<li><a href="https://www.rabbitmq.com/docs/dlx" rel="noopener noreferrer">https://www.rabbitmq.com/docs/dlx</a></li>
<li><a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html" rel="noopener noreferrer">https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html</a></li>
<li><a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues-at-least-once-delivery.html" rel="noopener noreferrer">https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues-at-least-once-delivery.html</a></li>
<li><a href="https://microservices.io/patterns/data/transactional-outbox.html" rel="noopener noreferrer">https://microservices.io/patterns/data/transactional-outbox.html</a></li>
</ul>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/property-renewal-notifications-choosing-a-queue-service-to-implement-rate-limiting-in-message-processing/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How Old English Actually Works: A Quick Guide for Writers and Developers</title>
		<link>https://codango.com/how-old-english-actually-works-a-quick-guide-for-writers-and-developers/</link>
					<comments>https://codango.com/how-old-english-actually-works-a-quick-guide-for-writers-and-developers/#respond</comments>
		
		<dc:creator><![CDATA[]]></dc:creator>
		<pubDate>Wed, 28 Oct 2026 01:59:28 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/how-old-english-actually-works-a-quick-guide-for-writers-and-developers/</guid>

					<description><![CDATA[Old English (also called Anglo-Saxon) is the language spoken in England from roughly 450 to 1150 AD. It&#8217;s the ancestor of modern English, but most people can&#8217;t read a line <a class="more-link" href="https://codango.com/how-old-english-actually-works-a-quick-guide-for-writers-and-developers/">Continue reading <span class="screen-reader-text">  How Old English Actually Works: A Quick Guide for Writers and Developers</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Old English (also called Anglo-Saxon) is the language spoken in England from roughly 450 to 1150 AD. It&#8217;s the ancestor of modern English, but most people can&#8217;t read a line of it today. If you&#8217;re writing fantasy, naming characters in a game, or just curious, here are the basics.</p>
<h2>
<p>  It looks familiar but isn&#8217;t<br />
</p></h2>
<p>Compare &#8220;The king rode to the hall&#8221; with the Old English <em>Se cyning rad to þære healle</em>. You can spot <em>cyning</em> (king) and <em>healle</em> (hall), but the grammar is different: Old English had grammatical gender and case endings, much like German.</p>
<h2>
<p>  Letters you won&#8217;t find on your keyboard<br />
</p></h2>
<ul>
<li>
<strong>þ (thorn)</strong> and <strong>ð (eth)</strong>: both used for the &#8220;th&#8221; sound</li>
<li>
<strong>æ (ash)</strong>: the vowel in &#8220;cat&#8221;</li>
<li>
<strong>ƿ (wynn)</strong>: an old letter for &#8220;w&#8221;</li>
</ul>
<h2>
<p>  Runes came first<br />
</p></h2>
<p>Before the Latin alphabet took over, Anglo-Saxons also wrote with the Futhorc runic alphabet, which is why runes show up on old inscriptions like the Franks Casket.</p>
<h2>
<p>  Try it yourself<br />
</p></h2>
<p>If you want to translate to Old English without memorising declension tables, I built a free tool for it: <a href="https://oldenglishtranslator.app/" rel="noopener noreferrer">translate to Old English</a>. Type a modern English sentence and it gives you the Old English wording plus runic and Anglo-Saxon script versions. You can also try it at <a href="https://oldenglishtranslator.app/" rel="noopener noreferrer">https://oldenglishtranslator.app</a>.</p>
<h2>
<p>  Caveats<br />
</p></h2>
<p>Automatic translation of a dead language is always approximate: vocabulary gaps get filled with the closest attested words. For anything permanent (like a tattoo) double-check with a scholar or a dictionary such as Bosworth-Toller.</p>
<p>Happy writing, and <em>wes þu hal</em> (be well)!</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/how-old-english-actually-works-a-quick-guide-for-writers-and-developers/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
