This commit is contained in:
eric
2026-02-04 06:20:15 +00:00
parent bd862cb238
commit 7de3b87680
38 changed files with 173 additions and 104 deletions

View File

@@ -4,4 +4,4 @@
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -13,4 +13,4 @@ My work focuses on Infrastructure Performance and Customer Engineering, specific
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -4,4 +4,4 @@
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -4,4 +4,4 @@
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -1,8 +1,8 @@
<!doctype html><html lang=en><head><title>Eric X. Liu's Personal Page</title><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=color-scheme content="light dark"><meta http-equiv=Content-Security-Policy content="upgrade-insecure-requests; block-all-mixed-content; default-src 'self'; child-src 'self'; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net/; form-action 'self'; frame-src 'self' https://www.youtube.com https://disqus.com; img-src 'self' https://referrer.disqus.com https://c.disquscdn.com https://*.disqus.com; object-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https://cdn.jsdelivr.net/; script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://cdn.jsdelivr.net/ https://pagead2.googlesyndication.com https://static.cloudflareinsights.com https://unpkg.com https://ericxliu-me.disqus.com https://disqus.com https://*.disqus.com https://*.disquscdn.com https://unpkg.com; connect-src 'self' https://www.google-analytics.com https://pagead2.googlesyndication.com https://cloudflareinsights.com ws://localhost:1313 ws://localhost:* wss://localhost:* https://links.services.disqus.com https://*.disqus.com;"><meta name=author content="Eric X. Liu"><meta name=description content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta name=keywords content="software engineer,performance engineering,Google engineer,tech blog,software development,performance optimization,Eric Liu,engineering blog,mountain biking,Jeep enthusiast,overlanding,camping,outdoor adventures"><meta name=twitter:card content="summary"><meta name=twitter:title content="Eric X. Liu's Personal Page"><meta name=twitter:description content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta property="og:url" content="https://ericxliu.me/"><meta property="og:site_name" content="Eric X. Liu's Personal Page"><meta property="og:title" content="Eric X. Liu's Personal Page"><meta property="og:description" content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta property="og:locale" content="en"><meta property="og:type" content="website"><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=canonical href=https://ericxliu.me/><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-regular-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=stylesheet href=/css/coder.min.4b392a85107b91dbdabc528edf014a6ab1a30cd44cafcd5325c8efe796794fca.css integrity="sha256-SzkqhRB7kdvavFKO3wFKarGjDNRMr81TJcjv55Z5T8o=" crossorigin=anonymous media=screen><link rel=stylesheet href=/css/coder-dark.min.a00e6364bacbc8266ad1cc81230774a1397198f8cfb7bcba29b7d6fcb54ce57f.css integrity="sha256-oA5jZLrLyCZq0cyBIwd0oTlxmPjPt7y6KbfW/LVM5X8=" crossorigin=anonymous media=screen><link rel=icon type=image/svg+xml href=/images/favicon.svg sizes=any><link rel=icon type=image/png href=/images/favicon-32x32.png sizes=32x32><link rel=icon type=image/png href=/images/favicon-16x16.png sizes=16x16><link rel=apple-touch-icon href=/images/apple-touch-icon.png><link rel=apple-touch-icon sizes=180x180 href=/images/apple-touch-icon.png><link rel=manifest href=/site.webmanifest><link rel=mask-icon href=/images/safari-pinned-tab.svg color=#5bbad5><link rel=alternate type=application/rss+xml href=/index.xml title="Eric X. Liu's Personal Page"><meta name=generator content="Hugo 0.154.5"><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3972604619956476" crossorigin=anonymous></script><script type=application/ld+json>{"@context":"http://schema.org","@type":"Person","name":"Eric X. Liu","url":"https:\/\/ericxliu.me\/","description":"Software \u0026 Performance Engineer at Google","sameAs":["https:\/\/www.linkedin.com\/in\/eric-x-liu-46648b93\/","https:\/\/git.ericxliu.me\/eric"]}</script></head><body class="preload-transitions colorscheme-auto"><div class=float-container><a id=dark-mode-toggle class=colorscheme-toggle><i class="fa-solid fa-adjust fa-fw" aria-hidden=true></i></a></div><main class=wrapper><nav class=navigation><section class=container><a class=navigation-title href=https://ericxliu.me/>Eric X. Liu's Personal Page
<!doctype html><html lang=en><head><title>Eric X. Liu's Personal Page</title><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=color-scheme content="light dark"><meta http-equiv=Content-Security-Policy content="upgrade-insecure-requests; block-all-mixed-content; default-src 'self'; child-src 'self'; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net/; form-action 'self'; frame-src 'self' https://www.youtube.com https://disqus.com; img-src 'self' https://referrer.disqus.com https://c.disquscdn.com https://*.disqus.com; object-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https://cdn.jsdelivr.net/; script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://cdn.jsdelivr.net/ https://pagead2.googlesyndication.com https://static.cloudflareinsights.com https://unpkg.com https://ericxliu-me.disqus.com https://disqus.com https://*.disqus.com https://*.disquscdn.com https://unpkg.com; connect-src 'self' https://www.google-analytics.com https://pagead2.googlesyndication.com https://cloudflareinsights.com ws://localhost:1313 ws://localhost:* wss://localhost:* https://links.services.disqus.com https://*.disqus.com;"><meta name=author content="Eric X. Liu"><meta name=description content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta name=keywords content="software engineer,performance engineering,Google engineer,tech blog,software development,performance optimization,Eric Liu,engineering blog,mountain biking,Jeep enthusiast,overlanding,camping,outdoor adventures"><meta name=twitter:card content="summary"><meta name=twitter:title content="Eric X. Liu's Personal Page"><meta name=twitter:description content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta property="og:url" content="https://ericxliu.me/"><meta property="og:site_name" content="Eric X. Liu's Personal Page"><meta property="og:title" content="Eric X. Liu's Personal Page"><meta property="og:description" content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta property="og:locale" content="en"><meta property="og:type" content="website"><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=canonical href=https://ericxliu.me/><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-regular-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=stylesheet href=/css/coder.min.4b392a85107b91dbdabc528edf014a6ab1a30cd44cafcd5325c8efe796794fca.css integrity="sha256-SzkqhRB7kdvavFKO3wFKarGjDNRMr81TJcjv55Z5T8o=" crossorigin=anonymous media=screen><link rel=stylesheet href=/css/coder-dark.min.a00e6364bacbc8266ad1cc81230774a1397198f8cfb7bcba29b7d6fcb54ce57f.css integrity="sha256-oA5jZLrLyCZq0cyBIwd0oTlxmPjPt7y6KbfW/LVM5X8=" crossorigin=anonymous media=screen><link rel=icon type=image/svg+xml href=/images/favicon.svg sizes=any><link rel=icon type=image/png href=/images/favicon-32x32.png sizes=32x32><link rel=icon type=image/png href=/images/favicon-16x16.png sizes=16x16><link rel=apple-touch-icon href=/images/apple-touch-icon.png><link rel=apple-touch-icon sizes=180x180 href=/images/apple-touch-icon.png><link rel=manifest href=/site.webmanifest><link rel=mask-icon href=/images/safari-pinned-tab.svg color=#5bbad5><link rel=alternate type=application/rss+xml href=/index.xml title="Eric X. Liu's Personal Page"><meta name=generator content="Hugo 0.155.2"><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3972604619956476" crossorigin=anonymous></script><script type=application/ld+json>{"@context":"http://schema.org","@type":"Person","name":"Eric X. Liu","url":"https:\/\/ericxliu.me\/","description":"Software \u0026 Performance Engineer at Google","sameAs":["https:\/\/www.linkedin.com\/in\/eric-x-liu-46648b93\/","https:\/\/git.ericxliu.me\/eric"]}</script></head><body class="preload-transitions colorscheme-auto"><div class=float-container><a id=dark-mode-toggle class=colorscheme-toggle><i class="fa-solid fa-adjust fa-fw" aria-hidden=true></i></a></div><main class=wrapper><nav class=navigation><section class=container><a class=navigation-title href=https://ericxliu.me/>Eric X. Liu's Personal Page
</a><input type=checkbox id=menu-toggle>
<label class="menu-button float-right" for=menu-toggle><i class="fa-solid fa-bars fa-fw" aria-hidden=true></i></label><ul class=navigation-list><li class=navigation-item><a class=navigation-link href=/posts/>Posts</a></li><li class=navigation-item><a class=navigation-link href=https://chat.ericxliu.me>Chat</a></li><li class=navigation-item><a class=navigation-link href=https://git.ericxliu.me/user/oauth2/Authenitk>Git</a></li><li class=navigation-item><a class=navigation-link href=https://coder.ericxliu.me/api/v2/users/oidc/callback>Coder</a></li><li class=navigation-item><a class=navigation-link href=/about/>About</a></li><li class=navigation-item><a class=navigation-link href=/>|</a></li><li class=navigation-item><a class=navigation-link href=https://sso.ericxliu.me>Sign in</a></li></ul></section></nav><div class=content><section class="container centered"><div class=about><div class=avatar><img src=/images/gravatar.png alt=avatar width=200 height=200></div><h1>Eric X. Liu</h1><h2 id=typeit-info></h2><script src=https://unpkg.com/typeit@8.7.1/dist/index.umd.js></script><script>document.addEventListener("DOMContentLoaded",function(){new TypeIt("#typeit-info",{strings:["Software & Performance Engineer @Google","DIY Overlander & Rock Crawler","Tech Enthusiast"],speed:50,loop:!0,breakLines:!1,nextStringDelay:2e3,deleteSpeed:50,startDelay:500,lifeLike:!0}).go()})</script><ul><li><a href=https://git.ericxliu.me/eric aria-label=Git><i class="fa-brands fa-git fa-2x" aria-hidden=true></i></a></li><li><a href=https://www.linkedin.com/in/eric-x-liu-46648b93/ aria-label=linkedin><i class="fa-brands fa-linkedin fa-2x" aria-hidden=true></i></a></li><li><style>#span-17968cae.cloaked-e-mail{display:none}</style> <span class=cloaked-e-mail data-user=cire data-domain=em.uilxcire data-display="PGkgY2xhc3M9ImZhIGZhLWVudmVsb3BlIGZhLTJ4IiBhcmlhLWhpZGRlbj0idHJ1ZSI+PC9pPg==" id=span-17968cae></span>
<script id=script-17968cae>var span,scriptTag=document.getElementById("script-17968cae"),link=document.createElement("a"),address="cire".split("").reverse().join("")+"@"+"em.uilxcire".split("").reverse().join("");link.href="mailto:"+address,span=document.getElementById("span-17968cae"),link.innerHTML=atob(span.getAttribute("data-display")),scriptTag.parentElement.insertBefore(link,scriptTag.previousElementSibling),scriptTag.parentElement.removeChild(scriptTag.previousElementSibling)</script></li><li><a href=https://ericxliu.me/index.xml aria-label=RSS rel=alternate type=application/rss+xml><i class="fa-solid fa-rss fa-2x" aria-hidden=true></i></a></li></ul></div></section></div><footer class=footer><section class=container>©
<label class="menu-button float-right" for=menu-toggle><i class="fa-solid fa-bars fa-fw" aria-hidden=true></i></label><ul class=navigation-list><li class=navigation-item><a class=navigation-link href=/posts/>Posts</a></li><li class=navigation-item><a class=navigation-link href=https://chat.ericxliu.me>Chat</a></li><li class=navigation-item><a class=navigation-link href=https://git.ericxliu.me/user/oauth2/Authenitk>Git</a></li><li class=navigation-item><a class=navigation-link href=https://coder.ericxliu.me/api/v2/users/oidc/callback>Coder</a></li><li class=navigation-item><a class=navigation-link href=/about/>About</a></li><li class=navigation-item><a class=navigation-link href=/>|</a></li><li class=navigation-item><a class=navigation-link href=https://sso.ericxliu.me>Sign in</a></li></ul></section></nav><div class=content><section class="container centered"><div class=about><div class=avatar><img src=/images/gravatar.png alt=avatar width=200 height=200></div><h1>Eric X. Liu</h1><h2 id=typeit-info></h2><script src=https://unpkg.com/typeit@8.7.1/dist/index.umd.js></script><script>document.addEventListener("DOMContentLoaded",function(){new TypeIt("#typeit-info",{strings:["Software & Performance Engineer @Google","DIY Overlander & Rock Crawler","Tech Enthusiast"],speed:50,loop:!0,breakLines:!1,nextStringDelay:2e3,deleteSpeed:50,startDelay:500,lifeLike:!0}).go()})</script><ul><li><a href=https://git.ericxliu.me/eric aria-label=Git><i class="fa-brands fa-git fa-2x" aria-hidden=true></i></a></li><li><a href=https://www.linkedin.com/in/eric-x-liu-46648b93/ aria-label=linkedin><i class="fa-brands fa-linkedin fa-2x" aria-hidden=true></i></a></li><li><style>#span-867c354d.cloaked-e-mail{display:none}</style> <span class=cloaked-e-mail data-user=cire data-domain=em.uilxcire data-display="PGkgY2xhc3M9ImZhIGZhLWVudmVsb3BlIGZhLTJ4IiBhcmlhLWhpZGRlbj0idHJ1ZSI+PC9pPg==" id=span-867c354d></span>
<script id=script-867c354d>var span,scriptTag=document.getElementById("script-867c354d"),link=document.createElement("a"),address="cire".split("").reverse().join("")+"@"+"em.uilxcire".split("").reverse().join("");link.href="mailto:"+address,span=document.getElementById("span-867c354d"),link.innerHTML=atob(span.getAttribute("data-display")),scriptTag.parentElement.insertBefore(link,scriptTag.previousElementSibling),scriptTag.parentElement.removeChild(scriptTag.previousElementSibling)</script></li><li><a href=https://ericxliu.me/index.xml aria-label=RSS rel=alternate type=application/rss+xml><i class="fa-solid fa-rss fa-2x" aria-hidden=true></i></a></li></ul></div></section></div><footer class=footer><section class=container>©
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Eric X. Liu's Personal Page</title><link>https://ericxliu.me/</link><description>Recent content on Eric X. Liu's Personal Page</description><generator>Hugo</generator><language>en</language><lastBuildDate>Thu, 22 Jan 2026 06:48:07 +0000</lastBuildDate><atom:link href="https://ericxliu.me/index.xml" rel="self" type="application/rss+xml"/><item><title>Hacking a Chinese Car Stereo to fulfill my Knight Rider dreams</title><link>https://ericxliu.me/posts/vibe-coding-from-the-jeep/</link><pubDate>Wed, 21 Jan 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/vibe-coding-from-the-jeep/</guid><description>&lt;p&gt;&amp;ldquo;Vibe coding&amp;rdquo; has become my latest obsession. It&amp;rsquo;s that flow state where the tools disappear, and you&amp;rsquo;re just manipulating logic at the speed of thought. Usually, this happens in a high-end IDE like Antigravity. But lately, I&amp;rsquo;ve been trying to answer a childhood dream.&lt;/p&gt;
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Eric X. Liu's Personal Page</title><link>https://ericxliu.me/</link><description>Recent content on Eric X. Liu's Personal Page</description><generator>Hugo</generator><language>en</language><lastBuildDate>Wed, 04 Feb 2026 06:18:45 +0000</lastBuildDate><atom:link href="https://ericxliu.me/index.xml" rel="self" type="application/rss+xml"/><item><title>Deployment Lessons and My Take on Self-Hosting OpenClaw</title><link>https://ericxliu.me/posts/blog-draft/</link><pubDate>Tue, 03 Feb 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/blog-draft/</guid><description>&lt;p&gt;Deploying autonomous agents like OpenClaw on a self-hosted Kubernetes cluster offers significantly more control and integration potential than cloud-hosted alternatives. However, moving from a standard SaaS model to running your own intelligence infrastructure introduces several deployment challenges.&lt;/p&gt;
&lt;p&gt;Here are the practical lessons learned, organized by the layers of the agentic stack: Environment, Runtime, and Capabilities.&lt;/p&gt;
&lt;h2 id="layer-1-the-environment--breaking-the-sandbox"&gt;
Layer 1: The Environment Breaking the Sandbox
&lt;a class="heading-link" href="#layer-1-the-environment--breaking-the-sandbox"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;To move beyond being a chatbot, an agent needs to be able to affect its world. Deep integration starts with networking.&lt;/p&gt;</description></item><item><title>Hacking a Chinese Car Stereo to fulfill my Knight Rider dreams</title><link>https://ericxliu.me/posts/vibe-coding-from-the-jeep/</link><pubDate>Wed, 21 Jan 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/vibe-coding-from-the-jeep/</guid><description>&lt;p&gt;&amp;ldquo;Vibe coding&amp;rdquo; has become my latest obsession. It&amp;rsquo;s that flow state where the tools disappear, and you&amp;rsquo;re just manipulating logic at the speed of thought. Usually, this happens in a high-end IDE like Antigravity. But lately, I&amp;rsquo;ve been trying to answer a childhood dream.&lt;/p&gt;
&lt;p&gt;Growing up in China before the internet age, my window to the outside world was CCTV-6. Along with &lt;em&gt;Baywatch&lt;/em&gt;, one of the first American TV shows I ever watched was &lt;em&gt;Knight Rider&lt;/em&gt;. I don&amp;rsquo;t remember the exact plot lines, but the core concept stuck with me forever: KITT. A car that could talk, think, and do things for you.&lt;/p&gt;</description></item><item><title>How I Built a Blog Agent that Writes About Itself</title><link>https://ericxliu.me/posts/reverse-engineering-antigravity-ide/</link><pubDate>Fri, 16 Jan 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/reverse-engineering-antigravity-ide/</guid><description>&lt;p&gt;I&amp;rsquo;ve been spending a lot of time &amp;ldquo;vibe coding&amp;rdquo; in the Antigravity IDE lately. It&amp;rsquo;s an incredible flow state—intense, iterative, and fast. But it has a major flaw: the context is ephemeral. Once the session is over, that rich history of decisions, wrong turns, and &amp;ldquo;aha!&amp;rdquo; moments is locked away in an opaque, internal format.&lt;/p&gt;
&lt;p&gt;I wanted to capture that value. I wanted a system that could take my chaotic coding sessions and distill them into structured, technical blog posts (like the one you&amp;rsquo;re reading right now).&lt;/p&gt;</description></item><item><title>Why I Downgraded Magisk to Root My Pixel 2 XL</title><link>https://ericxliu.me/posts/rooting-pixel-2-xl-for-reverse-engineering/</link><pubDate>Wed, 07 Jan 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/rooting-pixel-2-xl-for-reverse-engineering/</guid><description>&lt;p&gt;For the past few weeks, I&amp;rsquo;ve been stuck in a stalemate with my EcoFlow Bluetooth Protocol Reverse Engineering Project. I have the hci snoop logs, I have the decompiled APK, and I have a strong suspicion about where the authentication logic is hiding. But suspicion isn&amp;rsquo;t proof.&lt;/p&gt;
&lt;p&gt;Static analysis has its limits. I found the &amp;ldquo;smoking gun&amp;rdquo; function—a native method responsible for encrypting the login payload—but understanding &lt;em&gt;how&lt;/em&gt; it constructs that payload within a strict 13-byte limit purely from assembly (ARM64) was proving to be a headache.&lt;/p&gt;</description></item><item><title>Why Your "Resilient" Homelab is Slower Than a Raspberry Pi</title><link>https://ericxliu.me/posts/debugging-authentik-performance/</link><pubDate>Fri, 02 Jan 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/debugging-authentik-performance/</guid><description>&lt;p&gt;In the world of self-hosting, there are many metrics for success: 99.9% uptime, sub-second latency, or a perfect GitOps pipeline. But for those of us running &amp;ldquo;production&amp;rdquo; at home, there is only one metric that truly matters: &lt;strong&gt;The Wife Acceptance Factor (WAF)&lt;/strong&gt;.&lt;/p&gt;

View File

@@ -39,7 +39,7 @@ After running 66 inference tests across seven different language models ranging
<a class=heading-link href=#what-this-actually-means><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><h3 id=why-memory-bandwidth-dominates-in-single-stream-inference>Why Memory Bandwidth Dominates (in Single-Stream Inference)
<a class=heading-link href=#why-memory-bandwidth-dominates-in-single-stream-inference><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>The roofline numbers tell a clear story: operational intensity ranges from 0.91 to 3.23 FLOPs/byte across all tested models during single-token generation (batch size = 1). To actually saturate those 1024 CUDA cores and hit compute-bound operation, you&rsquo;d need an operational intensity around 147 FLOPs/byte at the device&rsquo;s 68 GB/s memory bandwidth.</p><p>In practice, for a model to actually become compute-bound on this device during single-stream inference, it would need an operational intensity exceeding:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-fallback data-lang=fallback><span style=display:flex><span>OI_threshold = Peak_Compute / Memory_Bandwidth
<span class=sr-only>Link to heading</span></a></h3><p>The roofline numbers tell a clear story: operational intensity ranges from 0.91 to 3.23 FLOPs/byte across all tested models during single-token generation (batch size = 1). To actually saturate those 1024 CUDA cores and hit compute-bound operation, you&rsquo;d need an operational intensity around 147 FLOPs/byte at the device&rsquo;s 68 GB/s memory bandwidth.</p><p>In practice, for a model to actually become compute-bound on this device during single-stream inference, it would need an operational intensity exceeding:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-fallback data-lang=fallback><span style=display:flex><span>OI_threshold = Peak_Compute / Memory_Bandwidth
</span></span><span style=display:flex><span> = (40 × 10^12 ops/s) / (68 × 10^9 bytes/s)
</span></span><span style=display:flex><span> = 588 FLOPs/byte
</span></span></code></pre></div><p>Single-stream autoregressive decoding falls 100-600× short of this threshold because each token generation requires loading the entire model from memory (matrix-vector multiplication) while performing only ~2 FLOPs per parameter. The compute units are idle most of the time, simply waiting for model weights and activations to arrive from memory.</p><p>Note: Production LLM serving with large batch sizes (32-256 requests) changes this dynamic dramatically—batching transforms matrix-vector operations into matrix-matrix multiplications, increasing operational intensity by 30-250× and making workloads compute-bound. However, edge devices serving single users cannot exploit this optimization.</p><p>The largest model tested—gemma3n:e2b at 3.5GB quantized (5.44B total parameters, 2B effective)—shows only 16.3% efficiency, similar to other quantized models. Despite being the largest model, Q4_K_M quantization keeps its memory footprint manageable, resulting in similar operational intensity (3.23 FLOPs/byte) to the other INT4-quantized models. Its MatFormer architecture with selective parameter activation (only 2B of 5.44B params active per token) actually helps reduce memory traffic, though this benefit is partially offset by the overhead of routing logic.</p><h3 id=what-this-means-for-edge-deployment>What This Means for Edge Deployment
@@ -62,4 +62,4 @@ After running 66 inference tests across seven different language models ranging
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -0,0 +1,50 @@
<!doctype html><html lang=en><head><title>Deployment Lessons and My Take on Self-Hosting OpenClaw · Eric X. Liu's Personal Page</title><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=color-scheme content="light dark"><meta http-equiv=Content-Security-Policy content="upgrade-insecure-requests; block-all-mixed-content; default-src 'self'; child-src 'self'; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net/; form-action 'self'; frame-src 'self' https://www.youtube.com https://disqus.com; img-src 'self' https://referrer.disqus.com https://c.disquscdn.com https://*.disqus.com; object-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https://cdn.jsdelivr.net/; script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://cdn.jsdelivr.net/ https://pagead2.googlesyndication.com https://static.cloudflareinsights.com https://unpkg.com https://ericxliu-me.disqus.com https://disqus.com https://*.disqus.com https://*.disquscdn.com https://unpkg.com; connect-src 'self' https://www.google-analytics.com https://pagead2.googlesyndication.com https://cloudflareinsights.com ws://localhost:1313 ws://localhost:* wss://localhost:* https://links.services.disqus.com https://*.disqus.com;"><meta name=author content="Eric X. Liu"><meta name=description content="Deploying autonomous agents like OpenClaw on a self-hosted Kubernetes cluster offers significantly more control and integration potential than cloud-hosted alternatives. However, moving from a standard SaaS model to running your own intelligence infrastructure introduces several deployment challenges.
Here are the practical lessons learned, organized by the layers of the agentic stack: Environment, Runtime, and Capabilities.
Layer 1: The Environment Breaking the Sandbox
Link to heading
To move beyond being a chatbot, an agent needs to be able to affect its world. Deep integration starts with networking."><meta name=keywords content="software engineer,performance engineering,Google engineer,tech blog,software development,performance optimization,Eric Liu,engineering blog,mountain biking,Jeep enthusiast,overlanding,camping,outdoor adventures"><meta name=twitter:card content="summary"><meta name=twitter:title content="Deployment Lessons and My Take on Self-Hosting OpenClaw"><meta name=twitter:description content="Deploying autonomous agents like OpenClaw on a self-hosted Kubernetes cluster offers significantly more control and integration potential than cloud-hosted alternatives. However, moving from a standard SaaS model to running your own intelligence infrastructure introduces several deployment challenges.
Here are the practical lessons learned, organized by the layers of the agentic stack: Environment, Runtime, and Capabilities.
Layer 1: The Environment Breaking the Sandbox Link to heading To move beyond being a chatbot, an agent needs to be able to affect its world. Deep integration starts with networking."><meta property="og:url" content="https://ericxliu.me/posts/blog-draft/"><meta property="og:site_name" content="Eric X. Liu's Personal Page"><meta property="og:title" content="Deployment Lessons and My Take on Self-Hosting OpenClaw"><meta property="og:description" content="Deploying autonomous agents like OpenClaw on a self-hosted Kubernetes cluster offers significantly more control and integration potential than cloud-hosted alternatives. However, moving from a standard SaaS model to running your own intelligence infrastructure introduces several deployment challenges.
Here are the practical lessons learned, organized by the layers of the agentic stack: Environment, Runtime, and Capabilities.
Layer 1: The Environment Breaking the Sandbox Link to heading To move beyond being a chatbot, an agent needs to be able to affect its world. Deep integration starts with networking."><meta property="og:locale" content="en"><meta property="og:type" content="article"><meta property="article:section" content="posts"><meta property="article:published_time" content="2026-02-03T00:00:00+00:00"><meta property="article:modified_time" content="2026-02-04T06:18:45+00:00"><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=canonical href=https://ericxliu.me/posts/blog-draft/><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-regular-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=stylesheet href=/css/coder.min.4b392a85107b91dbdabc528edf014a6ab1a30cd44cafcd5325c8efe796794fca.css integrity="sha256-SzkqhRB7kdvavFKO3wFKarGjDNRMr81TJcjv55Z5T8o=" crossorigin=anonymous media=screen><link rel=stylesheet href=/css/coder-dark.min.a00e6364bacbc8266ad1cc81230774a1397198f8cfb7bcba29b7d6fcb54ce57f.css integrity="sha256-oA5jZLrLyCZq0cyBIwd0oTlxmPjPt7y6KbfW/LVM5X8=" crossorigin=anonymous media=screen><link rel=icon type=image/svg+xml href=/images/favicon.svg sizes=any><link rel=icon type=image/png href=/images/favicon-32x32.png sizes=32x32><link rel=icon type=image/png href=/images/favicon-16x16.png sizes=16x16><link rel=apple-touch-icon href=/images/apple-touch-icon.png><link rel=apple-touch-icon sizes=180x180 href=/images/apple-touch-icon.png><link rel=manifest href=/site.webmanifest><link rel=mask-icon href=/images/safari-pinned-tab.svg color=#5bbad5><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3972604619956476" crossorigin=anonymous></script><script type=application/ld+json>{"@context":"http://schema.org","@type":"Person","name":"Eric X. Liu","url":"https:\/\/ericxliu.me\/","description":"Software \u0026 Performance Engineer at Google","sameAs":["https:\/\/www.linkedin.com\/in\/eric-x-liu-46648b93\/","https:\/\/git.ericxliu.me\/eric"]}</script><script type=application/ld+json>{"@context":"http://schema.org","@type":"BlogPosting","headline":"Deployment Lessons and My Take on Self-Hosting OpenClaw","genre":"Blog","wordcount":"844","url":"https:\/\/ericxliu.me\/posts\/blog-draft\/","datePublished":"2026-02-03T00:00:00\u002b00:00","dateModified":"2026-02-04T06:18:45\u002b00:00","description":"\u003cp\u003eDeploying autonomous agents like OpenClaw on a self-hosted Kubernetes cluster offers significantly more control and integration potential than cloud-hosted alternatives. However, moving from a standard SaaS model to running your own intelligence infrastructure introduces several deployment challenges.\u003c\/p\u003e\n\u003cp\u003eHere are the practical lessons learned, organized by the layers of the agentic stack: Environment, Runtime, and Capabilities.\u003c\/p\u003e\n\u003ch2 id=\u0022layer-1-the-environment--breaking-the-sandbox\u0022\u003e\n Layer 1: The Environment Breaking the Sandbox\n \u003ca class=\u0022heading-link\u0022 href=\u0022#layer-1-the-environment--breaking-the-sandbox\u0022\u003e\n \u003ci class=\u0022fa-solid fa-link\u0022 aria-hidden=\u0022true\u0022 title=\u0022Link to heading\u0022\u003e\u003c\/i\u003e\n \u003cspan class=\u0022sr-only\u0022\u003eLink to heading\u003c\/span\u003e\n \u003c\/a\u003e\n\u003c\/h2\u003e\n\u003cp\u003eTo move beyond being a chatbot, an agent needs to be able to affect its world. Deep integration starts with networking.\u003c\/p\u003e","author":{"@type":"Person","name":"Eric X. Liu"}}</script></head><body class="preload-transitions colorscheme-auto"><div class=float-container><a id=dark-mode-toggle class=colorscheme-toggle><i class="fa-solid fa-adjust fa-fw" aria-hidden=true></i></a></div><main class=wrapper><nav class=navigation><section class=container><a class=navigation-title href=https://ericxliu.me/>Eric X. Liu's Personal Page
</a><input type=checkbox id=menu-toggle>
<label class="menu-button float-right" for=menu-toggle><i class="fa-solid fa-bars fa-fw" aria-hidden=true></i></label><ul class=navigation-list><li class=navigation-item><a class=navigation-link href=/posts/>Posts</a></li><li class=navigation-item><a class=navigation-link href=https://chat.ericxliu.me>Chat</a></li><li class=navigation-item><a class=navigation-link href=https://git.ericxliu.me/user/oauth2/Authenitk>Git</a></li><li class=navigation-item><a class=navigation-link href=https://coder.ericxliu.me/api/v2/users/oidc/callback>Coder</a></li><li class=navigation-item><a class=navigation-link href=/about/>About</a></li><li class=navigation-item><a class=navigation-link href=/>|</a></li><li class=navigation-item><a class=navigation-link href=https://sso.ericxliu.me>Sign in</a></li></ul></section></nav><div class=content><section class="container post"><article><header><div class=post-title><h1 class=title><a class=title-link href=https://ericxliu.me/posts/blog-draft/>Deployment Lessons and My Take on Self-Hosting OpenClaw</a></h1></div><div class=post-meta><div class=date><span class=posted-on><i class="fa-solid fa-calendar" aria-hidden=true></i>
<time datetime=2026-02-03T00:00:00Z>February 3, 2026
</time></span><span class=reading-time><i class="fa-solid fa-clock" aria-hidden=true></i>
4-minute read</span></div></div></header><div class=post-content><p>Deploying autonomous agents like OpenClaw on a self-hosted Kubernetes cluster offers significantly more control and integration potential than cloud-hosted alternatives. However, moving from a standard SaaS model to running your own intelligence infrastructure introduces several deployment challenges.</p><p>Here are the practical lessons learned, organized by the layers of the agentic stack: Environment, Runtime, and Capabilities.</p><h2 id=layer-1-the-environment--breaking-the-sandbox>Layer 1: The Environment Breaking the Sandbox
<a class=heading-link href=#layer-1-the-environment--breaking-the-sandbox><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>To move beyond being a chatbot, an agent needs to be able to affect its world. Deep integration starts with networking.</p><p>Code execution agents often need to spin up temporary servers—for previews, React apps, or documentation sites. In a standard Kubernetes Pod, these dynamic ports (like 3000, 8080, etc.) are isolated inside the container network namespace.</p><p>To securely expose these arbitrary ports, I deployed a lightweight <strong>Nginx sidecar</strong> alongside the main OpenClaw agent. This avoids the complexity and latency of dynamically updating Ingress resources.</p><p>The Nginx configuration handling the routing logic:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-yaml data-lang=yaml><span style=display:flex><span><span style=color:#a5d6ff>server {</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#a5d6ff>listen 80;</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#a5d6ff>server_name ~^(?&lt;port&gt;\d+)\.agent\.mydomain\.com$;</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#a5d6ff>location / {</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#a5d6ff>proxy_pass http://localhost:$port;</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#a5d6ff>proxy_set_header Host $host;</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span>}<span style=color:#6e7681>
</span></span></span><span style=display:flex><span>}<span style=color:#6e7681>
</span></span></span></code></pre></div><p>This configuration uses a regex-based server block to capture the port from the subdomain (e.g., <code>3000.agent.mydomain.com</code>) and proxies traffic to that port on <code>localhost</code>. Since containers in the same Pod share a network namespace, <code>localhost</code> connectivity is seamless.</p><p>For this to work effectively, the agent must be aware of its environment. I updated OpenClaw&rsquo;s system prompts to understand this pattern: <em>&ldquo;If you start a server on port X, the external URL is <a href=https://X.agent.mydomain.com class=external-link target=_blank rel=noopener>https://X.agent.mydomain.com</a>&rdquo;</em>. This allows the agent to provide valid, clickable links for its generated applications.</p><h2 id=layer-2-the-runtime--agility-and-persistence>Layer 2: The Runtime Agility and Persistence
<a class=heading-link href=#layer-2-the-runtime--agility-and-persistence><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>Once the agent allows for external connectivity, the next challenge is agility. Self-hosting often requires customizations that haven&rsquo;t yet been merged upstream.</p><p>Self-hosting often requires customizations that haven&rsquo;t yet been merged upstream. For example, I needed a custom OAuth flow for Google&rsquo;s internal APIs.</p><p>Instead of maintaining a forked Docker image, I used a Kubernetes <code>ConfigMap</code> to inject the necessary TypeScript plugin at runtime. The file is mounted directly into the container at <code>/app/extensions/google-antigravity-auth/index.ts</code>.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-yaml data-lang=yaml><span style=display:flex><span><span style=color:#7ee787>kind</span>:<span style=color:#6e7681> </span><span style=color:#a5d6ff>ConfigMap</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#7ee787>metadata</span>:<span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#7ee787>name</span>:<span style=color:#6e7681> </span><span style=color:#a5d6ff>openclaw-patch-antigravity</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#7ee787>data</span>:<span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#7ee787>index.ts</span>:<span style=color:#6e7681> </span>|<span style=color:#a5d6ff>
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> import { createHash, randomBytes } from &#34;node:crypto&#34;;
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> // ... custom OAuth implementation ...
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> export default antigravityPlugin;</span><span style=color:#6e7681>
</span></span></span></code></pre></div><p>This approach allows for rapid iteration on patches without rebuilding container images for every change.</p><p>However, two operational realities became clear during this process:</p><ol><li><strong>Debugging is Standard</strong>: When the agent fails (e.g., your custom patch throws an error), it behaves like any other application. Standard debugging tools like <code>kubectl logs</code> and <code>strace</code> remain the most effective way to diagnose issues.</li><li><strong>Persistent Storage Matches Tooling</strong>: Just as code needs injection, tools need persistence. I had to explicitly mount a volume for Homebrew (<code>.linuxbrew</code>) so that tools installed by me or the agent didn&rsquo;t vanish on pod restart. Agents need long-term memory on their filesystem as much as in their context window.</li></ol><h2 id=layer-3-the-capabilities--skills-over-abstractions>Layer 3: The Capabilities Skills over Abstractions
<a class=heading-link href=#layer-3-the-capabilities--skills-over-abstractions><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>With the infrastructure (Layer 1) and runtime (Layer 2) established, we move to the application logic: how the agent actually <em>does</em> work.</p><p>While the industry chases complex abstractions like the Model Context Protocol (MCP), I found that simple, text-based &ldquo;Skills&rdquo; offer a superior workflow. I recently created a Gitea skill simply by exposing the <code>tea</code> CLI documentation to the agent.</p><p>This approach aligns with the UNIX philosophy: small, simple tools that do one thing well. MCP servers often clutter the context window and impose significant development overhead. A well-structured &ldquo;Skill&rdquo;—essentially a localized knowledge base for a CLI—is cleaner and faster to implement. I predict that these lightweight Skills will eventually replace heavy MCP integrations for the majority of use cases.</p><p>There is one current limitation: Gemini models lack specific post-training for these custom skills. The agent doesn&rsquo;t always intuitively know when to reach for a specific tool. Also, remember that granting the agent access to CLI tools like <code>kubectl</code> or <code>tea</code> (Gitea CLI) enables it to perform operations directly, transforming it from a text generator to a system operator. My agent can now open Pull Requests on my self-hosted Gitea instance, effectively becoming a contributor to its own config repo.</p><h2 id=the-payoff-why-this-complexity-matters>The Payoff: Why This Complexity Matters
<a class=heading-link href=#the-payoff-why-this-complexity-matters><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>Why go through this trouble of sidecars, config patches, and custom skills?</p><p>My previous AI workflows relied on standard chatbots via interfaces like Open-WebUI. The friction in that model is the &ldquo;all-or-nothing&rdquo; generation. LLMs are stochastic; regenerating an entire file to change three lines is inefficient and risky.</p><p>OpenClaw&rsquo;s or agentic tools (such as Cursor or Antigravity) killer feature is <strong>partial editing</strong>. The ability to iteratively improve a stable codebase or document without regenerating the entire file is the missing link for AI-assisted development. We need to treat code as a living document, not a chat response.</p><p>When combined with tools like Obsidian that I already use as my second brain for persistent knowledge management, this model provides both the long-term memory and the granular control necessary for complex projects.</p><h2 id=references>References
<a class=heading-link href=#references><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><ol><li><strong>OpenClaw Documentation</strong>: <a href=https://docs.openclaw.org class=external-link target=_blank rel=noopener>https://docs.openclaw.org</a></li><li><strong>Kubernetes Flux CD</strong>: <a href=https://fluxcd.io/ class=external-link target=_blank rel=noopener>https://fluxcd.io/</a></li><li><strong>Nginx Regex Server Names</strong>: <a href=https://nginx.org/en/docs/http/server_names.html class=external-link target=_blank rel=noopener>https://nginx.org/en/docs/http/server_names.html</a></li></ol></div><footer><div id=disqus_thread></div><script>window.disqus_config=function(){},function(){if(["localhost","127.0.0.1"].indexOf(window.location.hostname)!=-1){document.getElementById("disqus_thread").innerHTML="Disqus comments not available by default when the website is previewed locally.";return}var t=document,e=t.createElement("script");e.async=!0,e.src="//ericxliu-me.disqus.com/embed.js",e.setAttribute("data-timestamp",+new Date),(t.head||t.body).appendChild(e)}(),document.addEventListener("themeChanged",function(){document.readyState=="complete"&&DISQUS.reset({reload:!0,config:disqus_config})})</script></footer></article><link rel=stylesheet href=https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.css integrity=sha384-vKruj+a13U8yHIkAyGgK1J3ArTLzrFGBbBc0tDp4ad/EyewESeXE/Iv67Aj8gKZ0 crossorigin=anonymous><script defer src=https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.js integrity=sha384-PwRUT/YqbnEjkZO0zZxNqcxACrXe+j766U2amXcgMg5457rve2Y7I6ZJSm2A0mS4 crossorigin=anonymous></script><script defer src=https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/auto-render.min.js integrity=sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05 crossorigin=anonymous onload='renderMathInElement(document.body,{delimiters:[{left:"$$",right:"$$",display:!0},{left:"$",right:"$",display:!1},{left:"\\(",right:"\\)",display:!1},{left:"\\[",right:"\\]",display:!0}]})'></script></section></div><footer class=footer><section class=container>©
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -25,4 +25,4 @@ Understanding the Two Primary Maintenance Cycles Link to heading The Breville Ba
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -10,7 +10,7 @@ My detailed Grafana dashboards said everything was fine. But my wife said the SS
<a class=heading-link href=#the-environment><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>My homelab is designed for node-level resilience, which adds complexity to the storage layer. It is not running on a single server, but rather a 3-node <strong>Proxmox</strong> cluster where every component is redundant:</p><ul><li><strong>Orchestration</strong>: Kubernetes (k3s) managed via Flux CD.</li><li><strong>Storage</strong>: A <strong>Ceph</strong> cluster running on the Proxmox nodes, utilizing enterprise NVMe SSDs (<code>bluestore</code>) for OSDs.</li><li><strong>Database</strong>: Postgres managed by the Zalando Postgres Operator, with persistent volumes (PVCs) provisioned on Ceph RBD (block storage).</li><li><strong>Identity</strong>: Authentik for SSO.</li></ul><p>While the underlying disks are blazing fast NVMe drives, the architecture dictates that a write to a Ceph RBD volume is not complete until it is replicated over the network and acknowledged by multiple OSDs. This setup provides incredible resilience—I can pull the plug on a node and nothing stops—but it introduces unavoidable network latency for synchronous write operations. <strong>Keep this particular trade-off in mind; it plays a starring role in the investigation later.</strong></p><h2 id=the-symptom>The Symptom
<a class=heading-link href=#the-symptom><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>The issue was insidious because it was intermittent. Clicking &ldquo;Login&rdquo; would sometimes hang for 5-8 seconds, while other times it was instant. To an engineer, &ldquo;sometimes slow&rdquo; is the worst kind of bug because it defies easy reproduction.</p><p>The breakthrough came when I put aside the server-side Grafana dashboards and looked at the client side. By opening Chrome DevTools and monitoring the <strong>Network</strong> tab during a slow login attempt, I was able to capture the exact failing request.</p><p>I identified the culprit: the <code>/api/v3/core/applications/</code> endpoint. It wasn&rsquo;t a connection timeout or a DNS issue; the server was simply taking 5+ seconds to respond to this specific GET request.</p><p>Armed with this &ldquo;smoking gun,&rdquo; I copied the request as cURL (preserving the session cookies) and converted it into a Python benchmark script (<code>reproduce_latency.py</code>). This allowed me to reliably trigger the latency on demand, turning an intermittent &ldquo;heisenbug&rdquo; into a reproducible test case.</p><p>The results were validating and horrifying:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-text data-lang=text><span style=display:flex><span>Request 1: 2.1642s
<span class=sr-only>Link to heading</span></a></h2><p>The issue was insidious because it was intermittent. Clicking &ldquo;Login&rdquo; would sometimes hang for 5-8 seconds, while other times it was instant. To an engineer, &ldquo;sometimes slow&rdquo; is the worst kind of bug because it defies easy reproduction.</p><p>The breakthrough came when I put aside the server-side Grafana dashboards and looked at the client side. By opening Chrome DevTools and monitoring the <strong>Network</strong> tab during a slow login attempt, I was able to capture the exact failing request.</p><p>I identified the culprit: the <code>/api/v3/core/applications/</code> endpoint. It wasn&rsquo;t a connection timeout or a DNS issue; the server was simply taking 5+ seconds to respond to this specific GET request.</p><p>Armed with this &ldquo;smoking gun,&rdquo; I copied the request as cURL (preserving the session cookies) and converted it into a Python benchmark script (<code>reproduce_latency.py</code>). This allowed me to reliably trigger the latency on demand, turning an intermittent &ldquo;heisenbug&rdquo; into a reproducible test case.</p><p>The results were validating and horrifying:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-text data-lang=text><span style=display:flex><span>Request 1: 2.1642s
</span></span><span style=display:flex><span>Request 2: 8.4321s
</span></span><span style=display:flex><span>Request 3: 5.1234s
</span></span><span style=display:flex><span>...
@@ -19,7 +19,7 @@ My detailed Grafana dashboards said everything was fine. But my wife said the SS
<a class=heading-link href=#investigation--red-herrings><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><h3 id=attempt-1-the-connection-overhead-hypothesis>Attempt 1: The Connection Overhead Hypothesis
<a class=heading-link href=#attempt-1-the-connection-overhead-hypothesis><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p><strong>The Hypothesis</strong>: Authentik defaults to <code>CONN_MAX_AGE=0</code>, meaning it closes the database connection after every request. Since I enforce SSL for the database, I assumed the handshake overhead was killing performance.</p><p><strong>The Fix Attempt</strong>: I updated the Authentik configuration to enable persistent connections:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-yaml data-lang=yaml><span style=display:flex><span><span style=color:#7ee787>env</span>:<span style=color:#6e7681>
<span class=sr-only>Link to heading</span></a></h3><p><strong>The Hypothesis</strong>: Authentik defaults to <code>CONN_MAX_AGE=0</code>, meaning it closes the database connection after every request. Since I enforce SSL for the database, I assumed the handshake overhead was killing performance.</p><p><strong>The Fix Attempt</strong>: I updated the Authentik configuration to enable persistent connections:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-yaml data-lang=yaml><span style=display:flex><span><span style=color:#7ee787>env</span>:<span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span>- <span style=color:#7ee787>name</span>:<span style=color:#6e7681> </span><span style=color:#a5d6ff>AUTHENTIK_POSTGRESQL__CONN_MAX_AGE</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#7ee787>value</span>:<span style=color:#6e7681> </span><span style=color:#a5d6ff>&#34;600&#34;</span><span style=color:#6e7681>
</span></span></span></code></pre></div><p><strong>The Reality</strong>: The benchmark showed a slight improvement (~4.2s average), but the random 5-8s spikes remained. The 300ms connection setup was a factor, but not the root cause. As a side note, enabling this without configuring TCP Keepalives caused the Authentik worker to crash with <code>OperationalError('the connection is closed')</code> when firewalls silently dropped idle connections.</p><h3 id=attempt-2-cpu-starvation>Attempt 2: CPU Starvation
@@ -30,7 +30,7 @@ My detailed Grafana dashboards said everything was fine. But my wife said the SS
<a class=heading-link href=#insight-the-breaking-change><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>I checked the release notes for <strong>Authentik 2025.10</strong>:</p><blockquote><p><em>Breaking Change: Redis is no longer used for caching. All caching has been moved to the PostgreSQL database to simplify deployment.</em></p></blockquote><p>This architectural shift created a bottleneck specific to my storage backend:</p><ol><li><strong>The Change</strong>: Every API request triggers a cache write (session updates) to Postgres instead of Redis.</li><li><strong>The Default</strong>: Postgres defaults to <code>synchronous_commit = on</code>. A transaction is not considered &ldquo;committed&rdquo; until it is flushed to disk.</li><li><strong>The Storage</strong>: Ceph RBD replicates data across the network to multiple OSDs.</li></ol><p>Every time I loaded the dashboard, Authentik tried to update the cache. Postgres paused, verified the write was replicated to 3 other servers over the network (WAL Sync), and <em>then</em> responded.</p><h2 id=the-solution>The Solution
<a class=heading-link href=#the-solution><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>I couldn&rsquo;t move the database to local NVMe without losing the failover capabilities I built the cluster for. However, for a cache-heavy workload, I could compromise on strict durability.</p><p>I patched the Postgres configuration to disable synchronous commits:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-yaml data-lang=yaml><span style=display:flex><span><span style=color:#7ee787>spec</span>:<span style=color:#6e7681>
<span class=sr-only>Link to heading</span></a></h2><p>I couldn&rsquo;t move the database to local NVMe without losing the failover capabilities I built the cluster for. However, for a cache-heavy workload, I could compromise on strict durability.</p><p>I patched the Postgres configuration to disable synchronous commits:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-yaml data-lang=yaml><span style=display:flex><span><span style=color:#7ee787>spec</span>:<span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#7ee787>postgresql</span>:<span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#7ee787>parameters</span>:<span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#7ee787>synchronous_commit</span>:<span style=color:#6e7681> </span><span style=color:#a5d6ff>&#34;off&#34;</span><span style=color:#6e7681> </span><span style=color:#8b949e;font-style:italic># The magic switch</span><span style=color:#6e7681>
@@ -44,4 +44,4 @@ My detailed Grafana dashboards said everything was fine. But my wife said the SS
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -20,4 +20,4 @@ Our overarching philosophy is simple: isolate and change only one variable at a
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -31,7 +31,7 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
<a class=heading-link href=#configuration><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>Given the constraint of not having an x86 laptop, initial attempts used a QEMU/KVM virtual machine running Ubuntu 22.04 x86_64 on an Apple M4 Mac via UTM (a QEMU frontend for macOS). This approach allowed running SDK Manager on an emulated x86_64 system while connecting the Jetson device via USB passthrough configured through UTM&rsquo;s USB settings.</p><p>While this satisfied the requirement of having an x86_64 environment without using the Proxmox hosts, it introduced additional virtualization overhead as the entire x86_64 instruction set was being emulated on ARM64 hardware.</p><h3 id=issues-encountered>Issues Encountered
<a class=heading-link href=#issues-encountered><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>The flash process consistently failed during the USB communication phase with the error:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-fallback data-lang=fallback><span style=display:flex><span>ERROR: might be timeout in USB write.
<span class=sr-only>Link to heading</span></a></h3><p>The flash process consistently failed during the USB communication phase with the error:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-fallback data-lang=fallback><span style=display:flex><span>ERROR: might be timeout in USB write.
</span></span></code></pre></div><h3 id=root-cause-analysis>Root Cause Analysis
<a class=heading-link href=#root-cause-analysis><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>QEMU/KVM&rsquo;s USB passthrough implementation has known reliability issues with complex USB protocols. The Jetson&rsquo;s initrd flash process requires:</p><ol><li>Rapid USB re-enumeration when switching between recovery mode and initrd mode</li><li>High-throughput data transfer for writing the root filesystem</li><li>Bidirectional USB network communication with strict timing requirements</li></ol><p>Individual USB device passthrough in QEMU emulates USB at the device level, introducing latency and potential timing issues. The Jetson&rsquo;s USB networking during initrd boot is particularly sensitive to these delays, causing the timeout errors.</p><h3 id=conclusion>Conclusion
@@ -42,7 +42,7 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
<a class=heading-link href=#rationale><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>After the Mac-based VM approach failed, attention shifted to the Proxmox infrastructure. LXC containers provide near-native performance with minimal virtualization overhead compared to full VMs. Unlike running SDK Manager directly on the Proxmox host (which was ruled out for stability reasons), an LXC container offers:</p><ol><li><strong>Isolation</strong>: Complete separation from the host OS with its own filesystem and process space</li><li><strong>Near-Native Performance</strong>: Containers share the host kernel, eliminating instruction emulation overhead</li><li><strong>Easy Management</strong>: Containers can be created, destroyed, and backed up without affecting the host</li><li><strong>USB Access</strong>: Proxmox supports passing USB devices to containers via cgroup device permissions</li></ol><p>The hypothesis was that an LXC container with proper USB device access would provide the necessary USB timing characteristics while maintaining the clean separation requirement.</p><h3 id=configuration-progression>Configuration Progression
<a class=heading-link href=#configuration-progression><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>The LXC container (ID 106, Ubuntu 22.04) required extensive configuration on the Proxmox host (<code>/etc/pve/lxc/106.conf</code>):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Enable mknod capability for creating device nodes</span>
<span class=sr-only>Link to heading</span></a></h3><p>The LXC container (ID 106, Ubuntu 22.04) required extensive configuration on the Proxmox host (<code>/etc/pve/lxc/106.conf</code>):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Enable mknod capability for creating device nodes</span>
</span></span><span style=display:flex><span>features: <span style=color:#79c0ff>nesting</span><span style=color:#ff7b72;font-weight:700>=</span>1,mknod<span style=color:#ff7b72;font-weight:700>=</span><span style=color:#a5d6ff>1</span>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic># USB device passthrough (Bus 003)</span>
@@ -59,23 +59,23 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
<a class=heading-link href=#issues-encountered-and-resolutions><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><h4 id=1-mknod-permission-errors>1. mknod Permission Errors
<a class=heading-link href=#1-mknod-permission-errors><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p><strong>Error</strong>: <code>mknod: .../rootfs/dev/random: Operation not permitted</code></p><p><strong>Cause</strong>: LXC containers lack <code>CAP_MKNOD</code> capability by default, required by L4T flash scripts to create device nodes in the rootfs.</p><p><strong>Solution</strong>: Enable <code>mknod=1</code> feature on the Proxmox host:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>pct set <span style=color:#a5d6ff>106</span> -features <span style=color:#79c0ff>nesting</span><span style=color:#ff7b72;font-weight:700>=</span>1,mknod<span style=color:#ff7b72;font-weight:700>=</span><span style=color:#a5d6ff>1</span>
<span class=sr-only>Link to heading</span></a></h4><p><strong>Error</strong>: <code>mknod: .../rootfs/dev/random: Operation not permitted</code></p><p><strong>Cause</strong>: LXC containers lack <code>CAP_MKNOD</code> capability by default, required by L4T flash scripts to create device nodes in the rootfs.</p><p><strong>Solution</strong>: Enable <code>mknod=1</code> feature on the Proxmox host:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>pct set <span style=color:#a5d6ff>106</span> -features <span style=color:#79c0ff>nesting</span><span style=color:#ff7b72;font-weight:700>=</span>1,mknod<span style=color:#ff7b72;font-weight:700>=</span><span style=color:#a5d6ff>1</span>
</span></span></code></pre></div><h4 id=2-arm64-binary-execution>2. ARM64 Binary Execution
<a class=heading-link href=#2-arm64-binary-execution><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p><strong>Error</strong>: <code>chroot: failed to run command 'dpkg': Exec format error</code></p><p><strong>Cause</strong>: The L4T rootfs contains ARM64 binaries that cannot execute on x86_64 without emulation.</p><p><strong>Solution</strong>: Install and enable <code>qemu-user-static</code> and <code>binfmt-support</code> on the <strong>Proxmox host</strong> (not the container):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>apt-get install qemu-user-static binfmt-support
<span class=sr-only>Link to heading</span></a></h4><p><strong>Error</strong>: <code>chroot: failed to run command 'dpkg': Exec format error</code></p><p><strong>Cause</strong>: The L4T rootfs contains ARM64 binaries that cannot execute on x86_64 without emulation.</p><p><strong>Solution</strong>: Install and enable <code>qemu-user-static</code> and <code>binfmt-support</code> on the <strong>Proxmox host</strong> (not the container):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>apt-get install qemu-user-static binfmt-support
</span></span><span style=display:flex><span>update-binfmts --enable qemu-aarch64
</span></span></code></pre></div><h4 id=3-loop-device-access>3. Loop Device Access
<a class=heading-link href=#3-loop-device-access><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p><strong>Error</strong>: <code>losetup: cannot find an unused loop device</code></p><p><strong>Cause</strong>: The L4T flash scripts use loop devices to mount disk images. LXC containers don&rsquo;t have loop device access by default.</p><p><strong>Solution</strong>: Add loop device permissions and mount entries to the container configuration.</p><h4 id=4-usb-networking-failure>4. USB Networking Failure
<a class=heading-link href=#4-usb-networking-failure><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p><strong>Error</strong>: <code>Device failed to boot to the initrd flash kernel</code></p><p><strong>Cause</strong>: This was the most complex issue. When the Jetson boots into initrd mode (<code>0955:7035</code>), it creates a USB network interface (<code>enx*</code> or <code>usb0</code>). However, in LXC containers, this interface appeared in the <strong>host&rsquo;s network namespace</strong>, not the container&rsquo;s namespace.</p><p><strong>Attempted Solution</strong>:</p><ol><li>Loaded USB networking kernel modules on the Proxmox host:</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>modprobe rndis_host cdc_ether cdc_ncm cdc_subset
<span class=sr-only>Link to heading</span></a></h4><p><strong>Error</strong>: <code>Device failed to boot to the initrd flash kernel</code></p><p><strong>Cause</strong>: This was the most complex issue. When the Jetson boots into initrd mode (<code>0955:7035</code>), it creates a USB network interface (<code>enx*</code> or <code>usb0</code>). However, in LXC containers, this interface appeared in the <strong>host&rsquo;s network namespace</strong>, not the container&rsquo;s namespace.</p><p><strong>Attempted Solution</strong>:</p><ol><li>Loaded USB networking kernel modules on the Proxmox host:</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>modprobe rndis_host cdc_ether cdc_ncm cdc_subset
</span></span><span style=display:flex><span>echo <span style=color:#a5d6ff>&#34;rndis_host&#34;</span> &gt;&gt; /etc/modules
</span></span><span style=display:flex><span>echo <span style=color:#a5d6ff>&#34;cdc_ether&#34;</span> &gt;&gt; /etc/modules
</span></span><span style=display:flex><span>echo <span style=color:#a5d6ff>&#34;cdc_ncm&#34;</span> &gt;&gt; /etc/modules
</span></span><span style=display:flex><span>echo <span style=color:#a5d6ff>&#34;cdc_subset&#34;</span> &gt;&gt; /etc/modules
</span></span></code></pre></div><ol start=2><li>Created udev rules to automatically move USB network interfaces to the container:</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># /etc/udev/rules.d/99-jetson-usb-network.rules</span>
</span></span></code></pre></div><ol start=2><li>Created udev rules to automatically move USB network interfaces to the container:</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># /etc/udev/rules.d/99-jetson-usb-network.rules</span>
</span></span><span style=display:flex><span><span style=color:#79c0ff>ACTION</span><span style=color:#ff7b72;font-weight:700>==</span><span style=color:#a5d6ff>&#34;add&#34;</span>, <span style=color:#79c0ff>SUBSYSTEM</span><span style=color:#ff7b72;font-weight:700>==</span><span style=color:#a5d6ff>&#34;net&#34;</span>, <span style=color:#79c0ff>KERNEL</span><span style=color:#ff7b72;font-weight:700>==</span><span style=color:#a5d6ff>&#34;enx*&#34;</span>, <span style=color:#79c0ff>RUN</span><span style=color:#ff7b72;font-weight:700>+=</span><span style=color:#a5d6ff>&#34;/usr/local/bin/handle-jetson-usb-network.sh %k&#34;</span>
</span></span></code></pre></div><ol start=3><li>Created handler script to move interfaces into container namespace:</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-weight:700;font-style:italic>#!/bin/bash
</span></span></code></pre></div><ol start=3><li>Created handler script to move interfaces into container namespace:</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-weight:700;font-style:italic>#!/bin/bash
</span></span></span><span style=display:flex><span><span style=color:#79c0ff>INTERFACE</span><span style=color:#ff7b72;font-weight:700>=</span><span style=color:#79c0ff>$1</span>
</span></span><span style=display:flex><span><span style=color:#79c0ff>CONTAINER_ID</span><span style=color:#ff7b72;font-weight:700>=</span><span style=color:#a5d6ff>106</span>
</span></span><span style=display:flex><span><span style=color:#79c0ff>CONTAINER_PID</span><span style=color:#ff7b72;font-weight:700>=</span><span style=color:#ff7b72>$(</span>pct exec <span style=color:#79c0ff>$CONTAINER_ID</span> -- pidof systemd | awk <span style=color:#a5d6ff>&#39;{print $1}&#39;</span><span style=color:#ff7b72>)</span>
@@ -94,7 +94,7 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
<a class=heading-link href=#implementation><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><h4 id=1-identify-usb-controller>1. Identify USB Controller
<a class=heading-link href=#1-identify-usb-controller><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Find which USB controller the Jetson is connected to</span>
<span class=sr-only>Link to heading</span></a></h4><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Find which USB controller the Jetson is connected to</span>
</span></span><span style=display:flex><span>lsusb -t | grep -B5 <span style=color:#a5d6ff>&#34;0955:7523&#34;</span>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Map USB buses to PCI addresses</span>
@@ -102,11 +102,11 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
</span></span><span style=display:flex><span> <span style=color:#79c0ff>pci</span><span style=color:#ff7b72;font-weight:700>=</span><span style=color:#ff7b72>$(</span>readlink /sys/bus/usb/devices/usb<span style=color:#79c0ff>$bus</span> 2&gt;/dev/null | grep -oE <span style=color:#a5d6ff>&#39;[0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9]&#39;</span><span style=color:#ff7b72>)</span>
</span></span><span style=display:flex><span> echo <span style=color:#a5d6ff>&#34;USB Bus </span><span style=color:#79c0ff>$bus</span><span style=color:#a5d6ff> → PCI </span><span style=color:#79c0ff>$pci</span><span style=color:#a5d6ff>&#34;</span>
</span></span><span style=display:flex><span><span style=color:#ff7b72>done</span>
</span></span></code></pre></div><p>Result: Jetson on Bus 4, controlled by PCI device <code>0000:22:00.3</code></p><p>Verification that no other critical devices shared this controller:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>lsusb | grep <span style=color:#a5d6ff>&#34;Bus 003&#34;</span> <span style=color:#8b949e;font-style:italic># Empty except root hub</span>
</span></span></code></pre></div><p>Result: Jetson on Bus 4, controlled by PCI device <code>0000:22:00.3</code></p><p>Verification that no other critical devices shared this controller:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>lsusb | grep <span style=color:#a5d6ff>&#34;Bus 003&#34;</span> <span style=color:#8b949e;font-style:italic># Empty except root hub</span>
</span></span><span style=display:flex><span>lsusb | grep <span style=color:#a5d6ff>&#34;Bus 004&#34;</span> <span style=color:#8b949e;font-style:italic># Only Jetson device</span>
</span></span></code></pre></div><h4 id=2-create-vm-with-pci-passthrough>2. Create VM with PCI Passthrough
<a class=heading-link href=#2-create-vm-with-pci-passthrough><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Create VM</span>
<span class=sr-only>Link to heading</span></a></h4><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Create VM</span>
</span></span><span style=display:flex><span>qm create <span style=color:#a5d6ff>200</span> --name jetson-flash --memory <span style=color:#a5d6ff>4096</span> --cores <span style=color:#a5d6ff>4</span> <span style=color:#79c0ff>\
</span></span></span><span style=display:flex><span> --net0 virtio,bridge<span style=color:#ff7b72;font-weight:700>=</span>vmbr0 --scsihw virtio-scsi-pci
</span></span><span style=display:flex><span>
@@ -134,7 +134,7 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
</span></span><span style=display:flex><span>qm start <span style=color:#a5d6ff>200</span>
</span></span></code></pre></div><h4 id=3-critical-usb-networking-kernel-modules>3. Critical: USB Networking Kernel Modules
<a class=heading-link href=#3-critical-usb-networking-kernel-modules><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p>The Ubuntu cloud image does not include USB networking kernel modules by default. This is critical because when the Jetson boots into initrd mode, it requires the host to have these modules loaded immediately.</p><p><strong>Solution</strong>: Install and load modules before starting the flash:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Install extra kernel modules</span>
<span class=sr-only>Link to heading</span></a></h4><p>The Ubuntu cloud image does not include USB networking kernel modules by default. This is critical because when the Jetson boots into initrd mode, it requires the host to have these modules loaded immediately.</p><p><strong>Solution</strong>: Install and load modules before starting the flash:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Install extra kernel modules</span>
</span></span><span style=display:flex><span>apt-get install linux-modules-extra-<span style=color:#ff7b72>$(</span>uname -r<span style=color:#ff7b72>)</span>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Load USB networking modules</span>
@@ -147,7 +147,7 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
</span></span><span style=display:flex><span>lsmod | grep -E <span style=color:#a5d6ff>&#39;rndis|cdc&#39;</span>
</span></span></code></pre></div><p>When the Jetson transitions to initrd mode (<code>0955:7035</code>), the USB network interface (<code>usb0</code>) now appears immediately in the VM&rsquo;s network namespace.</p><h4 id=4-network-configuration>4. Network Configuration
<a class=heading-link href=#4-network-configuration><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p>The Jetson&rsquo;s initrd uses <strong>IPv6</strong> for USB networking by default:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Interface appears automatically</span>
<span class=sr-only>Link to heading</span></a></h4><p>The Jetson&rsquo;s initrd uses <strong>IPv6</strong> for USB networking by default:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Interface appears automatically</span>
</span></span><span style=display:flex><span>ip addr show usb0
</span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Output:</span>
</span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic># usb0: inet6 fc00:1:1::1/64 scope global</span>
@@ -168,4 +168,4 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -18,4 +18,4 @@ The answer lies in creating a universal language—a bridge between the continuo
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -1,6 +1,7 @@
<!doctype html><html lang=en><head><title>Posts · Eric X. Liu's Personal Page</title><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=color-scheme content="light dark"><meta http-equiv=Content-Security-Policy content="upgrade-insecure-requests; block-all-mixed-content; default-src 'self'; child-src 'self'; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net/; form-action 'self'; frame-src 'self' https://www.youtube.com https://disqus.com; img-src 'self' https://referrer.disqus.com https://c.disquscdn.com https://*.disqus.com; object-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https://cdn.jsdelivr.net/; script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://cdn.jsdelivr.net/ https://pagead2.googlesyndication.com https://static.cloudflareinsights.com https://unpkg.com https://ericxliu-me.disqus.com https://disqus.com https://*.disqus.com https://*.disquscdn.com https://unpkg.com; connect-src 'self' https://www.google-analytics.com https://pagead2.googlesyndication.com https://cloudflareinsights.com ws://localhost:1313 ws://localhost:* wss://localhost:* https://links.services.disqus.com https://*.disqus.com;"><meta name=author content="Eric X. Liu"><meta name=description content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta name=keywords content="software engineer,performance engineering,Google engineer,tech blog,software development,performance optimization,Eric Liu,engineering blog,mountain biking,Jeep enthusiast,overlanding,camping,outdoor adventures"><meta name=twitter:card content="summary"><meta name=twitter:title content="Posts"><meta name=twitter:description content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta property="og:url" content="https://ericxliu.me/posts/"><meta property="og:site_name" content="Eric X. Liu's Personal Page"><meta property="og:title" content="Posts"><meta property="og:description" content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta property="og:locale" content="en"><meta property="og:type" content="website"><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=canonical href=https://ericxliu.me/posts/><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-regular-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=stylesheet href=/css/coder.min.4b392a85107b91dbdabc528edf014a6ab1a30cd44cafcd5325c8efe796794fca.css integrity="sha256-SzkqhRB7kdvavFKO3wFKarGjDNRMr81TJcjv55Z5T8o=" crossorigin=anonymous media=screen><link rel=stylesheet href=/css/coder-dark.min.a00e6364bacbc8266ad1cc81230774a1397198f8cfb7bcba29b7d6fcb54ce57f.css integrity="sha256-oA5jZLrLyCZq0cyBIwd0oTlxmPjPt7y6KbfW/LVM5X8=" crossorigin=anonymous media=screen><link rel=icon type=image/svg+xml href=/images/favicon.svg sizes=any><link rel=icon type=image/png href=/images/favicon-32x32.png sizes=32x32><link rel=icon type=image/png href=/images/favicon-16x16.png sizes=16x16><link rel=apple-touch-icon href=/images/apple-touch-icon.png><link rel=apple-touch-icon sizes=180x180 href=/images/apple-touch-icon.png><link rel=manifest href=/site.webmanifest><link rel=mask-icon href=/images/safari-pinned-tab.svg color=#5bbad5><link rel=alternate type=application/rss+xml href=/posts/index.xml title="Eric X. Liu's Personal Page"><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3972604619956476" crossorigin=anonymous></script><script type=application/ld+json>{"@context":"http://schema.org","@type":"Person","name":"Eric X. Liu","url":"https:\/\/ericxliu.me\/","description":"Software \u0026 Performance Engineer at Google","sameAs":["https:\/\/www.linkedin.com\/in\/eric-x-liu-46648b93\/","https:\/\/git.ericxliu.me\/eric"]}</script></head><body class="preload-transitions colorscheme-auto"><div class=float-container><a id=dark-mode-toggle class=colorscheme-toggle><i class="fa-solid fa-adjust fa-fw" aria-hidden=true></i></a></div><main class=wrapper><nav class=navigation><section class=container><a class=navigation-title href=https://ericxliu.me/>Eric X. Liu's Personal Page
</a><input type=checkbox id=menu-toggle>
<label class="menu-button float-right" for=menu-toggle><i class="fa-solid fa-bars fa-fw" aria-hidden=true></i></label><ul class=navigation-list><li class=navigation-item><a class=navigation-link href=/posts/>Posts</a></li><li class=navigation-item><a class=navigation-link href=https://chat.ericxliu.me>Chat</a></li><li class=navigation-item><a class=navigation-link href=https://git.ericxliu.me/user/oauth2/Authenitk>Git</a></li><li class=navigation-item><a class=navigation-link href=https://coder.ericxliu.me/api/v2/users/oidc/callback>Coder</a></li><li class=navigation-item><a class=navigation-link href=/about/>About</a></li><li class=navigation-item><a class=navigation-link href=/>|</a></li><li class=navigation-item><a class=navigation-link href=https://sso.ericxliu.me>Sign in</a></li></ul></section></nav><div class=content><section class="container list"><header><h1 class=title><a class=title-link href=https://ericxliu.me/posts/>Posts</a></h1></header><ul><li><span class=date>January 21, 2026</span>
<label class="menu-button float-right" for=menu-toggle><i class="fa-solid fa-bars fa-fw" aria-hidden=true></i></label><ul class=navigation-list><li class=navigation-item><a class=navigation-link href=/posts/>Posts</a></li><li class=navigation-item><a class=navigation-link href=https://chat.ericxliu.me>Chat</a></li><li class=navigation-item><a class=navigation-link href=https://git.ericxliu.me/user/oauth2/Authenitk>Git</a></li><li class=navigation-item><a class=navigation-link href=https://coder.ericxliu.me/api/v2/users/oidc/callback>Coder</a></li><li class=navigation-item><a class=navigation-link href=/about/>About</a></li><li class=navigation-item><a class=navigation-link href=/>|</a></li><li class=navigation-item><a class=navigation-link href=https://sso.ericxliu.me>Sign in</a></li></ul></section></nav><div class=content><section class="container list"><header><h1 class=title><a class=title-link href=https://ericxliu.me/posts/>Posts</a></h1></header><ul><li><span class=date>February 3, 2026</span>
<a class=title href=/posts/blog-draft/>Deployment Lessons and My Take on Self-Hosting OpenClaw</a></li><li><span class=date>January 21, 2026</span>
<a class=title href=/posts/vibe-coding-from-the-jeep/>Hacking a Chinese Car Stereo to fulfill my Knight Rider dreams</a></li><li><span class=date>January 16, 2026</span>
<a class=title href=/posts/reverse-engineering-antigravity-ide/>How I Built a Blog Agent that Writes About Itself</a></li><li><span class=date>January 7, 2026</span>
<a class=title href=/posts/rooting-pixel-2-xl-for-reverse-engineering/>Why I Downgraded Magisk to Root My Pixel 2 XL</a></li><li><span class=date>January 2, 2026</span>
@@ -9,9 +10,8 @@
<a class=title href=/posts/technical-deep-dive-llm-categorization/>From Gemini-3-Flash to T5-Gemma-2: A Journey in Distilling a Family Finance LLM</a></li><li><span class=date>December 19, 2025</span>
<a class=title href=/posts/the-convergence-of-fast-weights-linear-attention-and-state-space-models/>The Convergence of Fast Weights, Linear Attention, and State Space Models</a></li><li><span class=date>December 8, 2025</span>
<a class=title href=/posts/vattention/>vAttention</a></li><li><span class=date>November 15, 2025</span>
<a class=title href=/posts/jellyfin-sso-with-authentik/>Setting Up Jellyfin SSO with Authentik: Surviving the Beta</a></li><li><span class=date>October 4, 2025</span>
<a class=title href=/posts/benchmarking-llms-on-jetson-orin-nano/>Why Your Jetson Orin Nano's 40 TOPS Goes Unused (And What That Means for Edge AI)</a></li></ul><ul class=pagination><li>1</li><li><a href=/posts/page/2/>2</a></li><li><a href=/posts/page/3/>3</a></li><li class=hidden><a href=/posts/page/2/>&#8250;</a></li><li><a href=/posts/page/3/>&#187;</a></li></ul></section></div><footer class=footer><section class=container>©
<a class=title href=/posts/jellyfin-sso-with-authentik/>Setting Up Jellyfin SSO with Authentik: Surviving the Beta</a></li></ul><ul class=pagination><li>1</li><li><a href=/posts/page/2/>2</a></li><li><a href=/posts/page/3/>3</a></li><li class=hidden><a href=/posts/page/2/>&#8250;</a></li><li><a href=/posts/page/3/>&#187;</a></li></ul></section></div><footer class=footer><section class=container>©
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on Eric X. Liu's Personal Page</title><link>https://ericxliu.me/posts/</link><description>Recent content in Posts on Eric X. Liu's Personal Page</description><generator>Hugo</generator><language>en</language><lastBuildDate>Thu, 22 Jan 2026 06:48:07 +0000</lastBuildDate><atom:link href="https://ericxliu.me/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Hacking a Chinese Car Stereo to fulfill my Knight Rider dreams</title><link>https://ericxliu.me/posts/vibe-coding-from-the-jeep/</link><pubDate>Wed, 21 Jan 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/vibe-coding-from-the-jeep/</guid><description>&lt;p&gt;&amp;ldquo;Vibe coding&amp;rdquo; has become my latest obsession. It&amp;rsquo;s that flow state where the tools disappear, and you&amp;rsquo;re just manipulating logic at the speed of thought. Usually, this happens in a high-end IDE like Antigravity. But lately, I&amp;rsquo;ve been trying to answer a childhood dream.&lt;/p&gt;
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on Eric X. Liu's Personal Page</title><link>https://ericxliu.me/posts/</link><description>Recent content in Posts on Eric X. Liu's Personal Page</description><generator>Hugo</generator><language>en</language><lastBuildDate>Wed, 04 Feb 2026 06:18:45 +0000</lastBuildDate><atom:link href="https://ericxliu.me/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Deployment Lessons and My Take on Self-Hosting OpenClaw</title><link>https://ericxliu.me/posts/blog-draft/</link><pubDate>Tue, 03 Feb 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/blog-draft/</guid><description>&lt;p&gt;Deploying autonomous agents like OpenClaw on a self-hosted Kubernetes cluster offers significantly more control and integration potential than cloud-hosted alternatives. However, moving from a standard SaaS model to running your own intelligence infrastructure introduces several deployment challenges.&lt;/p&gt;
&lt;p&gt;Here are the practical lessons learned, organized by the layers of the agentic stack: Environment, Runtime, and Capabilities.&lt;/p&gt;
&lt;h2 id="layer-1-the-environment--breaking-the-sandbox"&gt;
Layer 1: The Environment Breaking the Sandbox
&lt;a class="heading-link" href="#layer-1-the-environment--breaking-the-sandbox"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;To move beyond being a chatbot, an agent needs to be able to affect its world. Deep integration starts with networking.&lt;/p&gt;</description></item><item><title>Hacking a Chinese Car Stereo to fulfill my Knight Rider dreams</title><link>https://ericxliu.me/posts/vibe-coding-from-the-jeep/</link><pubDate>Wed, 21 Jan 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/vibe-coding-from-the-jeep/</guid><description>&lt;p&gt;&amp;ldquo;Vibe coding&amp;rdquo; has become my latest obsession. It&amp;rsquo;s that flow state where the tools disappear, and you&amp;rsquo;re just manipulating logic at the speed of thought. Usually, this happens in a high-end IDE like Antigravity. But lately, I&amp;rsquo;ve been trying to answer a childhood dream.&lt;/p&gt;
&lt;p&gt;Growing up in China before the internet age, my window to the outside world was CCTV-6. Along with &lt;em&gt;Baywatch&lt;/em&gt;, one of the first American TV shows I ever watched was &lt;em&gt;Knight Rider&lt;/em&gt;. I don&amp;rsquo;t remember the exact plot lines, but the core concept stuck with me forever: KITT. A car that could talk, think, and do things for you.&lt;/p&gt;</description></item><item><title>How I Built a Blog Agent that Writes About Itself</title><link>https://ericxliu.me/posts/reverse-engineering-antigravity-ide/</link><pubDate>Fri, 16 Jan 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/reverse-engineering-antigravity-ide/</guid><description>&lt;p&gt;I&amp;rsquo;ve been spending a lot of time &amp;ldquo;vibe coding&amp;rdquo; in the Antigravity IDE lately. It&amp;rsquo;s an incredible flow state—intense, iterative, and fast. But it has a major flaw: the context is ephemeral. Once the session is over, that rich history of decisions, wrong turns, and &amp;ldquo;aha!&amp;rdquo; moments is locked away in an opaque, internal format.&lt;/p&gt;
&lt;p&gt;I wanted to capture that value. I wanted a system that could take my chaotic coding sessions and distill them into structured, technical blog posts (like the one you&amp;rsquo;re reading right now).&lt;/p&gt;</description></item><item><title>Why I Downgraded Magisk to Root My Pixel 2 XL</title><link>https://ericxliu.me/posts/rooting-pixel-2-xl-for-reverse-engineering/</link><pubDate>Wed, 07 Jan 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/rooting-pixel-2-xl-for-reverse-engineering/</guid><description>&lt;p&gt;For the past few weeks, I&amp;rsquo;ve been stuck in a stalemate with my EcoFlow Bluetooth Protocol Reverse Engineering Project. I have the hci snoop logs, I have the decompiled APK, and I have a strong suspicion about where the authentication logic is hiding. But suspicion isn&amp;rsquo;t proof.&lt;/p&gt;
&lt;p&gt;Static analysis has its limits. I found the &amp;ldquo;smoking gun&amp;rdquo; function—a native method responsible for encrypting the login payload—but understanding &lt;em&gt;how&lt;/em&gt; it constructs that payload within a strict 13-byte limit purely from assembly (ARM64) was proving to be a headache.&lt;/p&gt;</description></item><item><title>Why Your "Resilient" Homelab is Slower Than a Raspberry Pi</title><link>https://ericxliu.me/posts/debugging-authentik-performance/</link><pubDate>Fri, 02 Jan 2026 00:00:00 +0000</pubDate><guid>https://ericxliu.me/posts/debugging-authentik-performance/</guid><description>&lt;p&gt;In the world of self-hosting, there are many metrics for success: 99.9% uptime, sub-second latency, or a perfect GitOps pipeline. But for those of us running &amp;ldquo;production&amp;rdquo; at home, there is only one metric that truly matters: &lt;strong&gt;The Wife Acceptance Factor (WAF)&lt;/strong&gt;.&lt;/p&gt;

View File

@@ -17,7 +17,7 @@ The Setup Link to heading The configuration is best handled via API (curl) rathe
<a class=heading-link href=#the-setup><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>The configuration is best handled via API (curl) rather than the UI, as it ensures all fields are correctly typed and persistent.</p><h3 id=1-authentik-terraform>1. Authentik (Terraform)
<a class=heading-link href=#1-authentik-terraform><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>Let Authentik manage the secrets. Don&rsquo;t hardcode them.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-hcl data-lang=hcl><span style=display:flex><span><span style=color:#ff7b72>resource</span> <span style=color:#a5d6ff>&#34;authentik_provider_oauth2&#34; &#34;jellyfin&#34;</span> {
<span class=sr-only>Link to heading</span></a></h3><p>Let Authentik manage the secrets. Don&rsquo;t hardcode them.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-hcl data-lang=hcl><span style=display:flex><span><span style=color:#ff7b72>resource</span> <span style=color:#a5d6ff>&#34;authentik_provider_oauth2&#34; &#34;jellyfin&#34;</span> {
</span></span><span style=display:flex><span> name <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#a5d6ff>&#34;Jellyfin&#34;</span>
</span></span><span style=display:flex><span> client_id <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#a5d6ff>&#34;jellyfin-ericxliu-me&#34;</span><span style=color:#8b949e;font-style:italic>
</span></span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic> # client_secret omitted -&gt; auto-generated
@@ -31,7 +31,7 @@ The Setup Link to heading The configuration is best handled via API (curl) rathe
</span></span></span><span style=display:flex><span>}
</span></span></code></pre></div><h3 id=2-jellyfin-plugin-bashcurl>2. Jellyfin Plugin (Bash/Curl)
<a class=heading-link href=#2-jellyfin-plugin-bashcurl><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># ... (retrieve secret from terraform) ...</span>
<span class=sr-only>Link to heading</span></a></h3><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># ... (retrieve secret from terraform) ...</span>
</span></span><span style=display:flex><span>curl -X POST <span style=color:#a5d6ff>&#34;https://jellyfin.ericxliu.me/SSO/OID/Add/authentik&#34;</span> ... -d <span style=color:#a5d6ff>&#39;{
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> &#34;OidClientId&#34;: &#34;jellyfin-ericxliu-me&#34;,
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> &#34;OidSecret&#34;: &#34;&#39;</span><span style=color:#a5d6ff>&#34;</span><span style=color:#a5d6ff>${</span><span style=color:#79c0ff>SECRET</span><span style=color:#a5d6ff>}</span><span style=color:#a5d6ff>&#34;</span><span style=color:#a5d6ff>&#39;&#34;,
@@ -44,13 +44,13 @@ The Setup Link to heading The configuration is best handled via API (curl) rathe
<span class=sr-only>Link to heading</span></a></h2><p>Because the plugin is still maturing, it doesn&rsquo;t always handle configuration errors gracefully. Here are the two main &ldquo;cryptic&rdquo; failures I encountered.</p><h3 id=1-the-value-cannot-be-null-crash>1. The &ldquo;Value cannot be null&rdquo; Crash
<a class=heading-link href=#1-the-value-cannot-be-null-crash><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p><strong>The Symptom</strong>:
You attempt to start the SSO flow and get a generic 500 error. The Jellyfin logs show a C# exception:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-fallback data-lang=fallback><span style=display:flex><span>System.ArgumentNullException: Value cannot be null. (Parameter &#39;source&#39;)
You attempt to start the SSO flow and get a generic 500 error. The Jellyfin logs show a C# exception:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-fallback data-lang=fallback><span style=display:flex><span>System.ArgumentNullException: Value cannot be null. (Parameter &#39;source&#39;)
</span></span><span style=display:flex><span> at System.Linq.Enumerable.Prepend[TSource](IEnumerable`1 source, TSource element)
</span></span><span style=display:flex><span> at Jellyfin.Plugin.SSO.Api.SSOController.OidChallenge(...)
</span></span></code></pre></div><p><strong>The Reality</strong>:
This looks like deep internal failure, but it&rsquo;s actually a simple configuration miss. The plugin code attempts to prepend &ldquo;openid profile&rdquo; to your configured scopes without checking if your scopes array exists first.
<strong>The Fix</strong>:
You <strong>must</strong> explicitly provide <code>"OidScopes"</code> in your JSON configuration. It cannot be null or omitted.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-json data-lang=json><span style=display:flex><span><span style=color:#a5d6ff>&#34;OidScopes&#34;</span><span style=color:#f85149>:</span> [<span style=color:#a5d6ff>&#34;openid&#34;</span>, <span style=color:#a5d6ff>&#34;profile&#34;</span>, <span style=color:#a5d6ff>&#34;email&#34;</span>, <span style=color:#a5d6ff>&#34;groups&#34;</span>]
You <strong>must</strong> explicitly provide <code>"OidScopes"</code> in your JSON configuration. It cannot be null or omitted.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-json data-lang=json><span style=display:flex><span><span style=color:#a5d6ff>&#34;OidScopes&#34;</span><span style=color:#f85149>:</span> [<span style=color:#a5d6ff>&#34;openid&#34;</span>, <span style=color:#a5d6ff>&#34;profile&#34;</span>, <span style=color:#a5d6ff>&#34;email&#34;</span>, <span style=color:#a5d6ff>&#34;groups&#34;</span>]
</span></span></code></pre></div><h3 id=2-the-httphttps-mismatch-redirect-loop>2. The HTTP/HTTPS Mismatch (Redirect Loop)
<a class=heading-link href=#2-the-httphttps-mismatch-redirect-loop><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p><strong>The Symptom</strong>:
@@ -58,7 +58,7 @@ Authentik rejects the authorization request with &ldquo;Redirect URI mismatch&rd
<strong>The Reality</strong>:
Jellyfin often sits behind a reverse proxy (Ingress/Traefik) terminating TLS. Use <code>Browser Developer Tools</code> to inspect the network requests. You will likely see the <code>redirect_uri</code> parameter encoded as <code>http://jellyfin...</code> instead of <code>https://</code>. configuration.
<strong>The Fix</strong>:
Do not rely on header forwarding magic. Force the scheme in the plugin configuration:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-json data-lang=json><span style=display:flex><span><span style=color:#a5d6ff>&#34;SchemeOverride&#34;</span><span style=color:#f85149>:</span> <span style=color:#a5d6ff>&#34;https&#34;</span>
Do not rely on header forwarding magic. Force the scheme in the plugin configuration:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-json data-lang=json><span style=display:flex><span><span style=color:#a5d6ff>&#34;SchemeOverride&#34;</span><span style=color:#f85149>:</span> <span style=color:#a5d6ff>&#34;https&#34;</span>
</span></span></code></pre></div><h3 id=3-case-sensitivity-in-json>3. Case Sensitivity in JSON
<a class=heading-link href=#3-case-sensitivity-in-json><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p><strong>The Symptom</strong>: Configuration seems to be ignored or fields remain empty after a POST.
@@ -71,4 +71,4 @@ Do not rely on header forwarding magic. Force the scheme in the plugin configura
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -44,4 +44,4 @@ The <strong>Top-K routing</strong> mechanism, as illustrated in the provided ima
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -10,7 +10,7 @@ This post documents the final setup, the hotfix script that keeps LiteLLM honest
<a class=heading-link href=#why-open-webui-broke><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><ol><li><strong>Wrong API surface.</strong> <code>/v1/chat/completions</code> still rejects <code>type: "web_search"</code> with <code>Invalid value: 'web_search'. Supported values are: 'function' and 'custom'.</code></li><li><strong>LiteLLM tooling gap.</strong> The OpenAI TypedDicts in <code>litellm/types/llms/openai.py</code> only allow <code>Literal["function"]</code>. Even if the backend call succeeded, streaming would crash when it saw a new tool type.</li><li><strong>Open WebUI assumptions.</strong> The UI eagerly parses every tool delta, so when LiteLLM streamed the raw <code>web_search_call</code> chunk, the UI tried to execute it, failed to parse the arguments, and aborted the chat.</li></ol><p>Fixing all three required touching both the proxy configuration and the LiteLLM transformation path.</p><h2 id=step-1--route-gpt5-through-the-responses-api>Step 1 Route GPT5 Through the Responses API
<a class=heading-link href=#step-1--route-gpt5-through-the-responses-api><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>LiteLLMs Responses bridge activates whenever the backend model name starts with <code>openai/responses/</code>. I added a dedicated alias, <code>gpt-5.2-search</code>, that hardcodes the Responses API plus web search metadata. Existing models (reasoning, embeddings, TTS) stay untouched.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-yaml data-lang=yaml><span style=display:flex><span><span style=color:#8b949e;font-style:italic># proxy-config.yaml (sanitized)</span><span style=color:#6e7681>
<span class=sr-only>Link to heading</span></a></h2><p>LiteLLMs Responses bridge activates whenever the backend model name starts with <code>openai/responses/</code>. I added a dedicated alias, <code>gpt-5.2-search</code>, that hardcodes the Responses API plus web search metadata. Existing models (reasoning, embeddings, TTS) stay untouched.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-yaml data-lang=yaml><span style=display:flex><span><span style=color:#8b949e;font-style:italic># proxy-config.yaml (sanitized)</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#7ee787>model_list</span>:<span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span>- <span style=color:#7ee787>model_name</span>:<span style=color:#6e7681> </span><span style=color:#a5d6ff>gpt-5.2-search</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#7ee787>litellm_params</span>:<span style=color:#6e7681>
@@ -25,7 +25,7 @@ This post documents the final setup, the hotfix script that keeps LiteLLM honest
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#7ee787>country</span>:<span style=color:#6e7681> </span><span style=color:#a5d6ff>US</span><span style=color:#6e7681>
</span></span></span></code></pre></div><p>Any client (Open WebUI included) can now request <code>model: "gpt-5.2-search"</code> over the standard <code>/v1/chat/completions</code> endpoint, and LiteLLM handles the Responses API hop transparently.</p><h2 id=step-2--mask-web_search_call-chunks-inside-litellm>Step 2 Mask <code>web_search_call</code> Chunks Inside LiteLLM
<a class=heading-link href=#step-2--mask-web_search_call-chunks-inside-litellm><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>Even with the right API, LiteLLM still needs to stream deltas Open WebUI can digest. My <a href=https://ericxliu.me/hotfix.py class=external-link target=_blank rel=noopener>hotfix.py</a> script copies the LiteLLM source into <code>/tmp/patch/litellm</code>, then rewrites two files. This script runs as part of the Helm releases init hook so I can inject fixes directly into the container filesystem at pod start. That saves me from rebuilding and pushing new images every time LiteLLM upstream changes (or refuses a patch), which is critical while waiting for issue #13042 to land. Ill try to upstream the fix, but this is admittedly hacky, so timelines are uncertain.</p><ol><li><strong><code>openai.py</code> TypedDicts</strong>: extend the tool chunk definitions to accept <code>Literal["web_search"]</code>.</li><li><strong><code>litellm_responses_transformation/transformation.py</code></strong>: intercept every streaming item and short-circuit anything with <code>type == "web_search_call"</code>, returning an empty assistant delta instead of a tool call.</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-python data-lang=python><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Excerpt from hotfix.py</span>
<span class=sr-only>Link to heading</span></a></h2><p>Even with the right API, LiteLLM still needs to stream deltas Open WebUI can digest. My <a href=https://ericxliu.me/hotfix.py class=external-link target=_blank rel=noopener>hotfix.py</a> script copies the LiteLLM source into <code>/tmp/patch/litellm</code>, then rewrites two files. This script runs as part of the Helm releases init hook so I can inject fixes directly into the container filesystem at pod start. That saves me from rebuilding and pushing new images every time LiteLLM upstream changes (or refuses a patch), which is critical while waiting for issue #13042 to land. Ill try to upstream the fix, but this is admittedly hacky, so timelines are uncertain.</p><ol><li><strong><code>openai.py</code> TypedDicts</strong>: extend the tool chunk definitions to accept <code>Literal["web_search"]</code>.</li><li><strong><code>litellm_responses_transformation/transformation.py</code></strong>: intercept every streaming item and short-circuit anything with <code>type == "web_search_call"</code>, returning an empty assistant delta instead of a tool call.</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-python data-lang=python><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Excerpt from hotfix.py</span>
</span></span><span style=display:flex><span>tool_call_chunk_original <span style=color:#ff7b72;font-weight:700>=</span> (
</span></span><span style=display:flex><span> <span style=color:#a5d6ff>&#39;class ChatCompletionToolCallChunk(TypedDict): # result of /chat/completions call</span><span style=color:#79c0ff>\n</span><span style=color:#a5d6ff>&#39;</span>
</span></span><span style=display:flex><span> <span style=color:#a5d6ff>&#39; id: Optional[str]</span><span style=color:#79c0ff>\n</span><span style=color:#a5d6ff>&#39;</span>
@@ -37,7 +37,7 @@ This post documents the final setup, the hotfix script that keeps LiteLLM honest
</span></span><span style=display:flex><span><span style=color:#ff7b72;font-weight:700>...</span>
</span></span><span style=display:flex><span><span style=color:#ff7b72>if</span> tool_call_chunk_original <span style=color:#ff7b72;font-weight:700>in</span> content:
</span></span><span style=display:flex><span> content <span style=color:#ff7b72;font-weight:700>=</span> content<span style=color:#ff7b72;font-weight:700>.</span>replace(tool_call_chunk_original, tool_call_chunk_patch, <span style=color:#a5d6ff>1</span>)
</span></span></code></pre></div><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-python data-lang=python><span style=display:flex><span>added_block <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#a5d6ff>&#34;&#34;&#34; elif output_item.get(&#34;type&#34;) == &#34;web_search_call&#34;:
</span></span></code></pre></div><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-python data-lang=python><span style=display:flex><span>added_block <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#a5d6ff>&#34;&#34;&#34; elif output_item.get(&#34;type&#34;) == &#34;web_search_call&#34;:
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> # Mask the call: Open WebUI should never see tool metadata
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> action_payload = output_item.get(&#34;action&#34;)
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff> verbose_logger.debug(
@@ -57,7 +57,7 @@ This post documents the final setup, the hotfix script that keeps LiteLLM honest
</span></span></span><span style=display:flex><span><span style=color:#a5d6ff>&#34;&#34;&#34;</span>
</span></span></code></pre></div><p>These patches ensure LiteLLM never emits a <code>tool_calls</code> delta for <code>web_search</code>. Open WebUI only receives assistant text chunks, so it happily renders the model response and the inline citations the Responses API already provides.</p><h2 id=step-3--prove-it-with-curl-and-open-webui>Step 3 Prove It with cURL (and Open WebUI)
<a class=heading-link href=#step-3--prove-it-with-curl-and-open-webui><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>I keep a simple smoke test (<code>litellm_smoke_test.sh</code>) that hits the public ingress with and without streaming. The only secrets are placeholders here, but the structure is the same.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-weight:700;font-style:italic>#!/usr/bin/env bash
<span class=sr-only>Link to heading</span></a></h2><p>I keep a simple smoke test (<code>litellm_smoke_test.sh</code>) that hits the public ingress with and without streaming. The only secrets are placeholders here, but the structure is the same.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-weight:700;font-style:italic>#!/usr/bin/env bash
</span></span></span><span style=display:flex><span>set -euo pipefail
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span>echo <span style=color:#a5d6ff>&#34;Testing non-streaming...&#34;</span>
@@ -86,4 +86,4 @@ This post documents the final setup, the hotfix script that keeps LiteLLM honest
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -16,7 +16,7 @@ When using WireGuard together with MWAN3 on OpenWrt, the tunnel can fail to esta
<a class=heading-link href=#environment><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><ul><li>OpenWrt 24.10.x</li><li>MWAN3 for multi-WAN policy routing</li><li>WireGuard interface configured with broad <code>allowed_ips</code> covering default route (<code>0.0.0.0/1</code> and <code>128.0.0.0/1</code> or <code>0.0.0.0/0</code>)</li></ul><h3 id=symptoms>Symptoms
<a class=heading-link href=#symptoms><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><ul><li><code>wg show</code> indicates the interface is listening, but <code>transfer: 0 B received</code> persists after bringing the tunnel up.</li><li>Intermittent reachability to public IPs until routing settles.</li><li><code>ip route</code> shows multiple defaults via WANs; a host route to the peer IP may exist but is still overridden by policy routing once MWAN3 applies rules.</li></ul><p>Example observations (sanitized):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>wg show
<span class=sr-only>Link to heading</span></a></h3><ul><li><code>wg show</code> indicates the interface is listening, but <code>transfer: 0 B received</code> persists after bringing the tunnel up.</li><li>Intermittent reachability to public IPs until routing settles.</li><li><code>ip route</code> shows multiple defaults via WANs; a host route to the peer IP may exist but is still overridden by policy routing once MWAN3 applies rules.</li></ul><p>Example observations (sanitized):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>wg show
</span></span><span style=display:flex><span>interface: wireguard
</span></span><span style=display:flex><span> public key: &lt;redacted&gt;
</span></span><span style=display:flex><span> listening port: <span style=color:#a5d6ff>39345</span>
@@ -33,22 +33,22 @@ When using WireGuard together with MWAN3 on OpenWrt, the tunnel can fail to esta
<a class=heading-link href=#root-cause><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>With default-route <code>allowed_ips</code>, WireGuard installs routes so that all outbound traffic prefers the tunnel. MWAN3 then applies policy rules that also match “all traffic,” including the UDP packets to the WireGuard peers public IP. If those packets are selected to go via the <code>wireguard</code> interface (or a table whose default is the tunnel), the handshake cannot succeed. This creates a chicken-and-egg dependency.</p><h3 id=fix-exclude-the-wireguard-endpoint-from-mwan3-default-policy>Fix: Exclude the WireGuard Endpoint from MWAN3 Default Policy
<a class=heading-link href=#fix-exclude-the-wireguard-endpoint-from-mwan3-default-policy><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>Force traffic to the WireGuard peer public endpoint to use a physical WAN policy. This guarantees the handshake packets always reach the Internet outside of the tunnel.</p><p>Steps:</p><ol><li>Resolve the peer endpoint IP (if you only have a hostname)</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>nslookup vpn.example.com
<span class=sr-only>Link to heading</span></a></h3><p>Force traffic to the WireGuard peer public endpoint to use a physical WAN policy. This guarantees the handshake packets always reach the Internet outside of the tunnel.</p><p>Steps:</p><ol><li>Resolve the peer endpoint IP (if you only have a hostname)</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>nslookup vpn.example.com
</span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic># =&gt; use the returned A/AAAA address(es) in the rule below</span>
</span></span></code></pre></div><ol start=2><li>Add an MWAN3 rule targeting the endpoint IP</li></ol><p>Edit <code>/etc/config/mwan3</code> and place this rule before the default v4 rule so it takes precedence:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-fallback data-lang=fallback><span style=display:flex><span>config rule &#39;wireguard_endpoint&#39;
</span></span></code></pre></div><ol start=2><li>Add an MWAN3 rule targeting the endpoint IP</li></ol><p>Edit <code>/etc/config/mwan3</code> and place this rule before the default v4 rule so it takes precedence:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-fallback data-lang=fallback><span style=display:flex><span>config rule &#39;wireguard_endpoint&#39;
</span></span><span style=display:flex><span> option dest_ip &#39;203.0.113.55&#39; # peer public IP
</span></span><span style=display:flex><span> option proto &#39;udp&#39;
</span></span><span style=display:flex><span> option use_policy &#39;wan_only&#39; # a policy that prefers a physical WAN
</span></span><span style=display:flex><span> option family &#39;ipv4&#39;
</span></span></code></pre></div><p>Notes:</p><ul><li>Use the actual public IP of your WireGuard server. MWAN3 rules match IPs, not hostnames.</li><li>If you have multiple WAN policies (e.g., <code>wan_only</code>, <code>wphone_only</code>), choose the one that must carry the VPN handshake.</li></ul><ol start=3><li>(Optional) Assign a metric on the WireGuard interface</li></ol><p>This is not strictly required for the fix but keeps routing behavior deterministic when multiple defaults exist.</p><p>Edit <code>/etc/config/network</code>:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-fallback data-lang=fallback><span style=display:flex><span>config interface &#39;wireguard&#39;
</span></span></code></pre></div><p>Notes:</p><ul><li>Use the actual public IP of your WireGuard server. MWAN3 rules match IPs, not hostnames.</li><li>If you have multiple WAN policies (e.g., <code>wan_only</code>, <code>wphone_only</code>), choose the one that must carry the VPN handshake.</li></ul><ol start=3><li>(Optional) Assign a metric on the WireGuard interface</li></ol><p>This is not strictly required for the fix but keeps routing behavior deterministic when multiple defaults exist.</p><p>Edit <code>/etc/config/network</code>:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-fallback data-lang=fallback><span style=display:flex><span>config interface &#39;wireguard&#39;
</span></span><span style=display:flex><span> option proto &#39;wireguard&#39;
</span></span><span style=display:flex><span> option private_key &#39;&lt;redacted&gt;&#39;
</span></span><span style=display:flex><span> list addresses &#39;192.168.3.2/32&#39;
</span></span><span style=display:flex><span> option metric &#39;5&#39;
</span></span></code></pre></div><ol start=4><li>Apply changes</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>/etc/init.d/network restart <span style=color:#ff7b72;font-weight:700>&amp;&amp;</span> /etc/init.d/mwan3 restart
</span></span></code></pre></div><ol start=4><li>Apply changes</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>/etc/init.d/network restart <span style=color:#ff7b72;font-weight:700>&amp;&amp;</span> /etc/init.d/mwan3 restart
</span></span></code></pre></div><h3 id=validation>Validation
<a class=heading-link href=#validation><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>Confirm that the endpoint is routed via a physical WAN and that the tunnel is passing traffic.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Verify policy routing for the endpoint</span>
<span class=sr-only>Link to heading</span></a></h3><p>Confirm that the endpoint is routed via a physical WAN and that the tunnel is passing traffic.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Verify policy routing for the endpoint</span>
</span></span><span style=display:flex><span>ip route get 203.0.113.55
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic># MWAN3 status should show your WANs online</span>
@@ -60,7 +60,7 @@ When using WireGuard together with MWAN3 on OpenWrt, the tunnel can fail to esta
<a class=heading-link href=#operational-considerations><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><ul><li>Endpoint IP changes: If the server endpoint is behind DDNS, you must update the rule when its IP changes. Options include:<ul><li>Use a small script triggered by DDNS updates to modify the MWAN3 rule and reload.</li><li>Maintain an IP set and populate it from DDNS; match the set in firewall/PBR and keep MWAN3 in sync.</li></ul></li><li>IPv6: Repeat the approach with an IPv6 rule if your peer uses IPv6. Ensure <code>family 'ipv6'</code> and the correct policy are set.</li><li>Multiple peers: Create one rule per peer endpoint IP.</li><li>Ordering: Keep the endpoint rule above broad default rules so it always wins.</li></ul><h3 id=minimal-example-config-snippets-sanitized>Minimal Example Config Snippets (Sanitized)
<a class=heading-link href=#minimal-example-config-snippets-sanitized><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p><code>/etc/config/network</code> (relevant parts):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-fallback data-lang=fallback><span style=display:flex><span>config interface &#39;wireguard&#39;
<span class=sr-only>Link to heading</span></a></h3><p><code>/etc/config/network</code> (relevant parts):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-fallback data-lang=fallback><span style=display:flex><span>config interface &#39;wireguard&#39;
</span></span><span style=display:flex><span> option proto &#39;wireguard&#39;
</span></span><span style=display:flex><span> option private_key &#39;&lt;redacted&gt;&#39;
</span></span><span style=display:flex><span> list addresses &#39;192.168.3.2/32&#39;
@@ -74,7 +74,7 @@ When using WireGuard together with MWAN3 on OpenWrt, the tunnel can fail to esta
</span></span><span style=display:flex><span> list allowed_ips &#39;0.0.0.0/1&#39;
</span></span><span style=display:flex><span> list allowed_ips &#39;128.0.0.0/1&#39;
</span></span><span style=display:flex><span> option route_allowed_ips &#39;1&#39;
</span></span></code></pre></div><p><code>/etc/config/mwan3</code> (relevant parts):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-fallback data-lang=fallback><span style=display:flex><span>config policy &#39;wan_only&#39;
</span></span></code></pre></div><p><code>/etc/config/mwan3</code> (relevant parts):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-fallback data-lang=fallback><span style=display:flex><span>config policy &#39;wan_only&#39;
</span></span><span style=display:flex><span> list use_member &#39;wan_m1_w3&#39;
</span></span><span style=display:flex><span> option last_resort &#39;unreachable&#39;
</span></span><span style=display:flex><span>
@@ -98,4 +98,4 @@ When using WireGuard together with MWAN3 on OpenWrt, the tunnel can fail to esta
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -1,6 +1,7 @@
<!doctype html><html lang=en><head><title>Posts · Eric X. Liu's Personal Page</title><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=color-scheme content="light dark"><meta http-equiv=Content-Security-Policy content="upgrade-insecure-requests; block-all-mixed-content; default-src 'self'; child-src 'self'; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net/; form-action 'self'; frame-src 'self' https://www.youtube.com https://disqus.com; img-src 'self' https://referrer.disqus.com https://c.disquscdn.com https://*.disqus.com; object-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https://cdn.jsdelivr.net/; script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://cdn.jsdelivr.net/ https://pagead2.googlesyndication.com https://static.cloudflareinsights.com https://unpkg.com https://ericxliu-me.disqus.com https://disqus.com https://*.disqus.com https://*.disquscdn.com https://unpkg.com; connect-src 'self' https://www.google-analytics.com https://pagead2.googlesyndication.com https://cloudflareinsights.com ws://localhost:1313 ws://localhost:* wss://localhost:* https://links.services.disqus.com https://*.disqus.com;"><meta name=author content="Eric X. Liu"><meta name=description content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta name=keywords content="software engineer,performance engineering,Google engineer,tech blog,software development,performance optimization,Eric Liu,engineering blog,mountain biking,Jeep enthusiast,overlanding,camping,outdoor adventures"><meta name=twitter:card content="summary"><meta name=twitter:title content="Posts"><meta name=twitter:description content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta property="og:url" content="https://ericxliu.me/posts/"><meta property="og:site_name" content="Eric X. Liu's Personal Page"><meta property="og:title" content="Posts"><meta property="og:description" content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta property="og:locale" content="en"><meta property="og:type" content="website"><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=canonical href=https://ericxliu.me/posts/><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-regular-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=stylesheet href=/css/coder.min.4b392a85107b91dbdabc528edf014a6ab1a30cd44cafcd5325c8efe796794fca.css integrity="sha256-SzkqhRB7kdvavFKO3wFKarGjDNRMr81TJcjv55Z5T8o=" crossorigin=anonymous media=screen><link rel=stylesheet href=/css/coder-dark.min.a00e6364bacbc8266ad1cc81230774a1397198f8cfb7bcba29b7d6fcb54ce57f.css integrity="sha256-oA5jZLrLyCZq0cyBIwd0oTlxmPjPt7y6KbfW/LVM5X8=" crossorigin=anonymous media=screen><link rel=icon type=image/svg+xml href=/images/favicon.svg sizes=any><link rel=icon type=image/png href=/images/favicon-32x32.png sizes=32x32><link rel=icon type=image/png href=/images/favicon-16x16.png sizes=16x16><link rel=apple-touch-icon href=/images/apple-touch-icon.png><link rel=apple-touch-icon sizes=180x180 href=/images/apple-touch-icon.png><link rel=manifest href=/site.webmanifest><link rel=mask-icon href=/images/safari-pinned-tab.svg color=#5bbad5><link rel=alternate type=application/rss+xml href=/posts/index.xml title="Eric X. Liu's Personal Page"><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3972604619956476" crossorigin=anonymous></script><script type=application/ld+json>{"@context":"http://schema.org","@type":"Person","name":"Eric X. Liu","url":"https:\/\/ericxliu.me\/","description":"Software \u0026 Performance Engineer at Google","sameAs":["https:\/\/www.linkedin.com\/in\/eric-x-liu-46648b93\/","https:\/\/git.ericxliu.me\/eric"]}</script></head><body class="preload-transitions colorscheme-auto"><div class=float-container><a id=dark-mode-toggle class=colorscheme-toggle><i class="fa-solid fa-adjust fa-fw" aria-hidden=true></i></a></div><main class=wrapper><nav class=navigation><section class=container><a class=navigation-title href=https://ericxliu.me/>Eric X. Liu's Personal Page
</a><input type=checkbox id=menu-toggle>
<label class="menu-button float-right" for=menu-toggle><i class="fa-solid fa-bars fa-fw" aria-hidden=true></i></label><ul class=navigation-list><li class=navigation-item><a class=navigation-link href=/posts/>Posts</a></li><li class=navigation-item><a class=navigation-link href=https://chat.ericxliu.me>Chat</a></li><li class=navigation-item><a class=navigation-link href=https://git.ericxliu.me/user/oauth2/Authenitk>Git</a></li><li class=navigation-item><a class=navigation-link href=https://coder.ericxliu.me/api/v2/users/oidc/callback>Coder</a></li><li class=navigation-item><a class=navigation-link href=/about/>About</a></li><li class=navigation-item><a class=navigation-link href=/>|</a></li><li class=navigation-item><a class=navigation-link href=https://sso.ericxliu.me>Sign in</a></li></ul></section></nav><div class=content><section class="container list"><header><h1 class=title><a class=title-link href=https://ericxliu.me/posts/>Posts</a></h1></header><ul><li><span class=date>October 2, 2025</span>
<label class="menu-button float-right" for=menu-toggle><i class="fa-solid fa-bars fa-fw" aria-hidden=true></i></label><ul class=navigation-list><li class=navigation-item><a class=navigation-link href=/posts/>Posts</a></li><li class=navigation-item><a class=navigation-link href=https://chat.ericxliu.me>Chat</a></li><li class=navigation-item><a class=navigation-link href=https://git.ericxliu.me/user/oauth2/Authenitk>Git</a></li><li class=navigation-item><a class=navigation-link href=https://coder.ericxliu.me/api/v2/users/oidc/callback>Coder</a></li><li class=navigation-item><a class=navigation-link href=/about/>About</a></li><li class=navigation-item><a class=navigation-link href=/>|</a></li><li class=navigation-item><a class=navigation-link href=https://sso.ericxliu.me>Sign in</a></li></ul></section></nav><div class=content><section class="container list"><header><h1 class=title><a class=title-link href=https://ericxliu.me/posts/>Posts</a></h1></header><ul><li><span class=date>October 4, 2025</span>
<a class=title href=/posts/benchmarking-llms-on-jetson-orin-nano/>Why Your Jetson Orin Nano's 40 TOPS Goes Unused (And What That Means for Edge AI)</a></li><li><span class=date>October 2, 2025</span>
<a class=title href=/posts/flashing-jetson-orin-nano-in-virtualized-environments/>Flashing Jetson Orin Nano in Virtualized Environments</a></li><li><span class=date>September 28, 2025</span>
<a class=title href=/posts/openwrt-mwan3-wireguard-endpoint-exclusion/>OpenWrt: Fix WireGuard Connectivity with MWAN3 by Excluding the VPN Endpoint</a></li><li><span class=date>September 22, 2025</span>
<a class=title href=/posts/unifi-vlan-migration-to-zone-based-architecture/>UniFi VLAN Migration to Zone-Based Architecture</a></li><li><span class=date>August 19, 2025</span>
@@ -9,9 +10,8 @@
<a class=title href=/posts/secure-boot-dkms-and-mok-on-proxmox-debian/>Fixing GPU Operator Pods Stuck in Init: Secure Boot, DKMS, and MOK on Proxmox + Debian</a></li><li><span class=date>August 7, 2025</span>
<a class=title href=/posts/how-rvq-teaches-llms-to-see-and-hear/>Beyond Words: How RVQ Teaches LLMs to See and Hear</a></li><li><span class=date>August 3, 2025</span>
<a class=title href=/posts/supabase-deep-dive/>Supabase Deep Dive: It's Not Magic, It's Just Postgres</a></li><li><span class=date>August 2, 2025</span>
<a class=title href=/posts/ppo-for-language-models/>A Deep Dive into PPO for Language Models</a></li><li><span class=date>July 2, 2025</span>
<a class=title href=/posts/mixture-of-experts-moe-models-challenges-solutions-in-practice/>Mixture-of-Experts (MoE) Models Challenges & Solutions in Practice</a></li></ul><ul class=pagination><li><a href=/posts/>&#171;</a></li><li class=hidden><a href=/posts/>&#8249;</a></li><li><a href=/posts/>1</a></li><li>2</li><li><a href=/posts/page/3/>3</a></li><li class=hidden><a href=/posts/page/3/>&#8250;</a></li><li><a href=/posts/page/3/>&#187;</a></li></ul></section></div><footer class=footer><section class=container>©
<a class=title href=/posts/ppo-for-language-models/>A Deep Dive into PPO for Language Models</a></li></ul><ul class=pagination><li><a href=/posts/>&#171;</a></li><li class=hidden><a href=/posts/>&#8249;</a></li><li><a href=/posts/>1</a></li><li>2</li><li><a href=/posts/page/3/>3</a></li><li class=hidden><a href=/posts/page/3/>&#8250;</a></li><li><a href=/posts/page/3/>&#187;</a></li></ul></section></div><footer class=footer><section class=container>©
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -1,6 +1,7 @@
<!doctype html><html lang=en><head><title>Posts · Eric X. Liu's Personal Page</title><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=color-scheme content="light dark"><meta http-equiv=Content-Security-Policy content="upgrade-insecure-requests; block-all-mixed-content; default-src 'self'; child-src 'self'; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net/; form-action 'self'; frame-src 'self' https://www.youtube.com https://disqus.com; img-src 'self' https://referrer.disqus.com https://c.disquscdn.com https://*.disqus.com; object-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https://cdn.jsdelivr.net/; script-src 'self' 'unsafe-inline' https://www.google-analytics.com https://cdn.jsdelivr.net/ https://pagead2.googlesyndication.com https://static.cloudflareinsights.com https://unpkg.com https://ericxliu-me.disqus.com https://disqus.com https://*.disqus.com https://*.disquscdn.com https://unpkg.com; connect-src 'self' https://www.google-analytics.com https://pagead2.googlesyndication.com https://cloudflareinsights.com ws://localhost:1313 ws://localhost:* wss://localhost:* https://links.services.disqus.com https://*.disqus.com;"><meta name=author content="Eric X. Liu"><meta name=description content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta name=keywords content="software engineer,performance engineering,Google engineer,tech blog,software development,performance optimization,Eric Liu,engineering blog,mountain biking,Jeep enthusiast,overlanding,camping,outdoor adventures"><meta name=twitter:card content="summary"><meta name=twitter:title content="Posts"><meta name=twitter:description content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta property="og:url" content="https://ericxliu.me/posts/"><meta property="og:site_name" content="Eric X. Liu's Personal Page"><meta property="og:title" content="Posts"><meta property="og:description" content="Eric X. Liu - Software & Performance Engineer at Google. Sharing insights about software engineering, performance optimization, tech industry experiences, mountain biking adventures, Jeep overlanding, and outdoor activities."><meta property="og:locale" content="en"><meta property="og:type" content="website"><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=canonical href=https://ericxliu.me/posts/><link rel=preload href=/fonts/fa-brands-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-regular-400.woff2 as=font type=font/woff2 crossorigin><link rel=preload href=/fonts/fa-solid-900.woff2 as=font type=font/woff2 crossorigin><link rel=stylesheet href=/css/coder.min.4b392a85107b91dbdabc528edf014a6ab1a30cd44cafcd5325c8efe796794fca.css integrity="sha256-SzkqhRB7kdvavFKO3wFKarGjDNRMr81TJcjv55Z5T8o=" crossorigin=anonymous media=screen><link rel=stylesheet href=/css/coder-dark.min.a00e6364bacbc8266ad1cc81230774a1397198f8cfb7bcba29b7d6fcb54ce57f.css integrity="sha256-oA5jZLrLyCZq0cyBIwd0oTlxmPjPt7y6KbfW/LVM5X8=" crossorigin=anonymous media=screen><link rel=icon type=image/svg+xml href=/images/favicon.svg sizes=any><link rel=icon type=image/png href=/images/favicon-32x32.png sizes=32x32><link rel=icon type=image/png href=/images/favicon-16x16.png sizes=16x16><link rel=apple-touch-icon href=/images/apple-touch-icon.png><link rel=apple-touch-icon sizes=180x180 href=/images/apple-touch-icon.png><link rel=manifest href=/site.webmanifest><link rel=mask-icon href=/images/safari-pinned-tab.svg color=#5bbad5><link rel=alternate type=application/rss+xml href=/posts/index.xml title="Eric X. Liu's Personal Page"><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3972604619956476" crossorigin=anonymous></script><script type=application/ld+json>{"@context":"http://schema.org","@type":"Person","name":"Eric X. Liu","url":"https:\/\/ericxliu.me\/","description":"Software \u0026 Performance Engineer at Google","sameAs":["https:\/\/www.linkedin.com\/in\/eric-x-liu-46648b93\/","https:\/\/git.ericxliu.me\/eric"]}</script></head><body class="preload-transitions colorscheme-auto"><div class=float-container><a id=dark-mode-toggle class=colorscheme-toggle><i class="fa-solid fa-adjust fa-fw" aria-hidden=true></i></a></div><main class=wrapper><nav class=navigation><section class=container><a class=navigation-title href=https://ericxliu.me/>Eric X. Liu's Personal Page
</a><input type=checkbox id=menu-toggle>
<label class="menu-button float-right" for=menu-toggle><i class="fa-solid fa-bars fa-fw" aria-hidden=true></i></label><ul class=navigation-list><li class=navigation-item><a class=navigation-link href=/posts/>Posts</a></li><li class=navigation-item><a class=navigation-link href=https://chat.ericxliu.me>Chat</a></li><li class=navigation-item><a class=navigation-link href=https://git.ericxliu.me/user/oauth2/Authenitk>Git</a></li><li class=navigation-item><a class=navigation-link href=https://coder.ericxliu.me/api/v2/users/oidc/callback>Coder</a></li><li class=navigation-item><a class=navigation-link href=/about/>About</a></li><li class=navigation-item><a class=navigation-link href=/>|</a></li><li class=navigation-item><a class=navigation-link href=https://sso.ericxliu.me>Sign in</a></li></ul></section></nav><div class=content><section class="container list"><header><h1 class=title><a class=title-link href=https://ericxliu.me/posts/>Posts</a></h1></header><ul><li><span class=date>June 1, 2025</span>
<label class="menu-button float-right" for=menu-toggle><i class="fa-solid fa-bars fa-fw" aria-hidden=true></i></label><ul class=navigation-list><li class=navigation-item><a class=navigation-link href=/posts/>Posts</a></li><li class=navigation-item><a class=navigation-link href=https://chat.ericxliu.me>Chat</a></li><li class=navigation-item><a class=navigation-link href=https://git.ericxliu.me/user/oauth2/Authenitk>Git</a></li><li class=navigation-item><a class=navigation-link href=https://coder.ericxliu.me/api/v2/users/oidc/callback>Coder</a></li><li class=navigation-item><a class=navigation-link href=/about/>About</a></li><li class=navigation-item><a class=navigation-link href=/>|</a></li><li class=navigation-item><a class=navigation-link href=https://sso.ericxliu.me>Sign in</a></li></ul></section></nav><div class=content><section class="container list"><header><h1 class=title><a class=title-link href=https://ericxliu.me/posts/>Posts</a></h1></header><ul><li><span class=date>July 2, 2025</span>
<a class=title href=/posts/mixture-of-experts-moe-models-challenges-solutions-in-practice/>Mixture-of-Experts (MoE) Models Challenges & Solutions in Practice</a></li><li><span class=date>June 1, 2025</span>
<a class=title href=/posts/t5-the-transformer-that-zigged-when-others-zagged-an-architectural-deep-dive/>An Architectural Deep Dive of T5</a></li><li><span class=date>May 1, 2025</span>
<a class=title href=/posts/espresso-theory-application-a-guide-for-the-breville-barista-pro/>Mastering Your Breville Barista Pro: The Ultimate Guide to Dialing In Espresso</a></li><li><span class=date>April 1, 2025</span>
<a class=title href=/posts/transformer-s-core-mechanics/>Transformer's Core Mechanics</a></li><li><span class=date>October 26, 2020</span>
@@ -8,4 +9,4 @@
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -25,4 +25,4 @@ where <code>δ_t = r_t + γV(s_{t+1}) - V(s_t)</code></p><ul><li><strong>γ (gam
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -7,4 +7,4 @@
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -24,4 +24,4 @@ I wanted to capture that value. I wanted a system that could take my chaotic cod
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -15,17 +15,17 @@ Static analysis has its limits. I found the “smoking gun” function—a nativ
<span class=sr-only>Link to heading</span></a></h2><p>Once in, I checked the version: <code>Android 10 (QP1A.190711.020)</code>. This was ancient. The Pixel 2 XL officially supports Android 11, and I wanted the latest possible base for compatibility with modern tools.</p><p>I tried the easy route: <strong>Settings > System Update</strong>.
<strong>The Result</strong>: Failure. The phone refused to pull the final OTA (<code>RP1A.201005.004.A1</code>), likely due to the Google update servers no longer prioritizing this EOL device.</p><h3 id=the-fix-manual-flashing>The Fix: Manual Flashing
<a class=heading-link href=#the-fix-manual-flashing><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>I had to bypass the OTA system entirely. I downloaded the <a href=https://developers.google.com/android/images class=external-link target=_blank rel=noopener>final Factory Image</a> from Google.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Don&#39;t rely on OTA. Flash the whole valid state.</span>
<span class=sr-only>Link to heading</span></a></h3><p>I had to bypass the OTA system entirely. I downloaded the <a href=https://developers.google.com/android/images class=external-link target=_blank rel=noopener>final Factory Image</a> from Google.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Don&#39;t rely on OTA. Flash the whole valid state.</span>
</span></span><span style=display:flex><span>fastboot -w update image-taimen-rp1a.201005.004.a1.zip
</span></span></code></pre></div><p><em>Note: I used the <code>-w</code> flag here since I had just wiped the device anyway. This gave me a pristine, stock Android 11 environment to break.</em></p><h2 id=phase-3-the-magisk-time-travel>Phase 3: The Magisk &ldquo;Time Travel&rdquo;
<a class=heading-link href=#phase-3-the-magisk-time-travel><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>This is where &ldquo;modern tools meets old hardware&rdquo; caused the most pain.</p><p><strong>The Hypothesis</strong>: Rooting a Pixel is standard procedure.</p><ol><li>Extract <code>boot.img</code> from the factory zip.</li><li>Patch it with the latest <strong>Magisk</strong> app.</li><li>Flash it back.</li></ol><p><strong>The Reality</strong>: Bootloop.
I used <strong>Magisk v30.6</strong> (the latest as of writing). The patch process &ldquo;succeeded,&rdquo; but flashing the resulting image caused the phone to immediately crash back to the bootloader with a &ldquo;Cannot find valid operating system&rdquo; error.</p><h3 id=debugging-the-bootloop>Debugging the Bootloop
<a class=heading-link href=#debugging-the-bootloop><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>I suspected a regression in how modern Magisk handles the antiquated boot partition structure of the Pixel 2 (A/B partitions, but pre-GKI).</p><p>I decided to perform some &ldquo;software archaeology&rdquo; and use a version of Magisk that was contemporary with the device&rsquo;s lifespan. I grabbed <strong>Magisk v25.0</strong> (released around 2022).</p><ol><li><strong>Repatch</strong>: I patched the <em>exact same</em> stock <code>boot.img</code> using the v25.0 app.</li><li><strong>Reflash</strong>:</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Flash to both slots to be safe</span>
<span class=sr-only>Link to heading</span></a></h3><p>I suspected a regression in how modern Magisk handles the antiquated boot partition structure of the Pixel 2 (A/B partitions, but pre-GKI).</p><p>I decided to perform some &ldquo;software archaeology&rdquo; and use a version of Magisk that was contemporary with the device&rsquo;s lifespan. I grabbed <strong>Magisk v25.0</strong> (released around 2022).</p><ol><li><strong>Repatch</strong>: I patched the <em>exact same</em> stock <code>boot.img</code> using the v25.0 app.</li><li><strong>Reflash</strong>:</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Flash to both slots to be safe</span>
</span></span><span style=display:flex><span>fastboot flash boot_a magisk_patched_25000.img
</span></span><span style=display:flex><span>fastboot flash boot_b magisk_patched_25000.img
</span></span></code></pre></div><p><strong>The Result</strong>: Success. The phone booted, and the Magisk app confirmed <code>Installed: 25.0</code>.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span> adb shell <span style=color:#a5d6ff>&#34;su -c id&#34;</span>
</span></span></code></pre></div><p><strong>The Result</strong>: Success. The phone booted, and the Magisk app confirmed <code>Installed: 25.0</code>.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span> adb shell <span style=color:#a5d6ff>&#34;su -c id&#34;</span>
</span></span><span style=display:flex><span><span style=color:#79c0ff>uid</span><span style=color:#ff7b72;font-weight:700>=</span>0<span style=color:#ff7b72;font-weight:700>(</span>root<span style=color:#ff7b72;font-weight:700>)</span> <span style=color:#79c0ff>gid</span><span style=color:#ff7b72;font-weight:700>=</span>0<span style=color:#ff7b72;font-weight:700>(</span>root<span style=color:#ff7b72;font-weight:700>)</span> <span style=color:#79c0ff>groups</span><span style=color:#ff7b72;font-weight:700>=</span>0<span style=color:#ff7b72;font-weight:700>(</span>root<span style=color:#ff7b72;font-weight:700>)</span> <span style=color:#79c0ff>context</span><span style=color:#ff7b72;font-weight:700>=</span>u:r:magisk:s0
</span></span></code></pre></div><h2 id=key-insights>Key Insights
<a class=heading-link href=#key-insights><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
@@ -35,4 +35,4 @@ I used <strong>Magisk v30.6</strong> (the latest as of writing). The patch proce
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -18,36 +18,36 @@ nvidia-smi failed to communicate with the NVIDIA driver modprobe nvidia → “K
<a class=heading-link href=#strategy><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>Keep Secure Boot on; get modules trusted. That requires:</p><ol><li>Ensure the VM boots via shim (so MOK can work)</li><li>Make sure DKMS signs modules with a MOK key/cert</li><li>Enroll that MOK into the firmware via shims MokManager</li></ol><h3 id=step-1--boot-via-shim-and-persist-efi-variables>Step 1 — Boot via shim and persist EFI variables
<a class=heading-link href=#step-1--boot-via-shim-and-persist-efi-variables><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>In Proxmox (VM stopped):</p><ul><li>BIOS: OVMF (UEFI)</li><li>Add EFI Disk (stores OVMF VARS; required for MOK)</li><li>Machine: q35</li><li>Enable Secure Boot (option shows only with OVMF + EFI Disk)</li></ul><p>Inside Debian:</p><ul><li>Ensure ESP is mounted at <code>/boot/efi</code></li><li>Install signed boot stack:<div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo apt install shim-signed grub-efi-amd64-signed efibootmgr mokutil
<span class=sr-only>Link to heading</span></a></h3><p>In Proxmox (VM stopped):</p><ul><li>BIOS: OVMF (UEFI)</li><li>Add EFI Disk (stores OVMF VARS; required for MOK)</li><li>Machine: q35</li><li>Enable Secure Boot (option shows only with OVMF + EFI Disk)</li></ul><p>Inside Debian:</p><ul><li>Ensure ESP is mounted at <code>/boot/efi</code></li><li>Install signed boot stack:<div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>sudo apt install shim-signed grub-efi-amd64-signed efibootmgr mokutil
</span></span><span style=display:flex><span>sudo grub-install --target<span style=color:#ff7b72;font-weight:700>=</span>x86_64-efi --efi-directory<span style=color:#ff7b72;font-weight:700>=</span>/boot/efi --bootloader-id<span style=color:#ff7b72;font-weight:700>=</span>debian
</span></span><span style=display:flex><span>sudo update-grub
</span></span></code></pre></div></li><li>Create/verify a boot entry that points to shim:<div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo efibootmgr -c -d /dev/sda -p <span style=color:#a5d6ff>15</span> -L <span style=color:#a5d6ff>&#34;debian&#34;</span> -l <span style=color:#a5d6ff>&#39;\EFI\debian\shimx64.efi&#39;</span>
</span></span></code></pre></div></li><li>Create/verify a boot entry that points to shim:<div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>sudo efibootmgr -c -d /dev/sda -p <span style=color:#a5d6ff>15</span> -L <span style=color:#a5d6ff>&#34;debian&#34;</span> -l <span style=color:#a5d6ff>&#39;\EFI\debian\shimx64.efi&#39;</span>
</span></span><span style=display:flex><span>sudo efibootmgr -o 0002,0001,0000 <span style=color:#8b949e;font-style:italic># make shim (0002) first</span>
</span></span><span style=display:flex><span>sudo efibootmgr -n <span style=color:#a5d6ff>0002</span> <span style=color:#8b949e;font-style:italic># BootNext shim for the next reboot</span>
</span></span></code></pre></div></li></ul><p>Tip: If NVRAM resets or fallback path is used, copy as a fallback:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo mkdir -p /boot/efi/EFI/BOOT
</span></span></code></pre></div></li></ul><p>Tip: If NVRAM resets or fallback path is used, copy as a fallback:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>sudo mkdir -p /boot/efi/EFI/BOOT
</span></span><span style=display:flex><span>sudo cp /boot/efi/EFI/debian/shimx64.efi /boot/efi/EFI/BOOT/BOOTX64.EFI
</span></span><span style=display:flex><span>sudo cp /boot/efi/EFI/debian/<span style=color:#ff7b72;font-weight:700>{</span>mmx64.efi,grubx64.efi<span style=color:#ff7b72;font-weight:700>}</span> /boot/efi/EFI/BOOT/
</span></span></code></pre></div><h3 id=step-2--make-dkms-sign-nvidia-modules-with-a-mok>Step 2 — Make DKMS sign NVIDIA modules with a MOK
<a class=heading-link href=#step-2--make-dkms-sign-nvidia-modules-with-a-mok><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>Debian already generated a DKMS key at <code>/var/lib/dkms/mok.key</code>. Create an X.509 cert in DER format:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo openssl req -new -x509 <span style=color:#79c0ff>\
<span class=sr-only>Link to heading</span></a></h3><p>Debian already generated a DKMS key at <code>/var/lib/dkms/mok.key</code>. Create an X.509 cert in DER format:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>sudo openssl req -new -x509 <span style=color:#79c0ff>\
</span></span></span><span style=display:flex><span> -key /var/lib/dkms/mok.key <span style=color:#79c0ff>\
</span></span></span><span style=display:flex><span> -out /var/lib/dkms/mok.der <span style=color:#79c0ff>\
</span></span></span><span style=display:flex><span> -outform DER <span style=color:#79c0ff>\
</span></span></span><span style=display:flex><span> -subj <span style=color:#a5d6ff>&#34;/CN=DKMS MOK/&#34;</span> <span style=color:#79c0ff>\
</span></span></span><span style=display:flex><span> -days <span style=color:#a5d6ff>36500</span>
</span></span></code></pre></div><p>Enable DKMS signing:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo sed -i <span style=color:#a5d6ff>&#39;s|^mok_signing_key=.*|mok_signing_key=/var/lib/dkms/mok.key|&#39;</span> /etc/dkms/framework.conf
</span></span></code></pre></div><p>Enable DKMS signing:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>sudo sed -i <span style=color:#a5d6ff>&#39;s|^mok_signing_key=.*|mok_signing_key=/var/lib/dkms/mok.key|&#39;</span> /etc/dkms/framework.conf
</span></span><span style=display:flex><span>sudo sed -i <span style=color:#a5d6ff>&#39;s|^mok_certificate=.*|mok_certificate=/var/lib/dkms/mok.der|&#39;</span> /etc/dkms/framework.conf
</span></span></code></pre></div><p>Rebuild/install modules (signs them now):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo dkms build nvidia/<span style=color:#ff7b72>$(</span>modinfo -F version nvidia<span style=color:#ff7b72>)</span> -k <span style=color:#ff7b72>$(</span>uname -r<span style=color:#ff7b72>)</span> --force
</span></span></code></pre></div><p>Rebuild/install modules (signs them now):</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>sudo dkms build nvidia/<span style=color:#ff7b72>$(</span>modinfo -F version nvidia<span style=color:#ff7b72>)</span> -k <span style=color:#ff7b72>$(</span>uname -r<span style=color:#ff7b72>)</span> --force
</span></span><span style=display:flex><span>sudo dkms install nvidia/<span style=color:#ff7b72>$(</span>modinfo -F version nvidia<span style=color:#ff7b72>)</span> -k <span style=color:#ff7b72>$(</span>uname -r<span style=color:#ff7b72>)</span> --force
</span></span></code></pre></div><h3 id=step-3--enroll-the-mok-via-shim-mokmanager>Step 3 — Enroll the MOK via shim (MokManager)
<a class=heading-link href=#step-3--enroll-the-mok-via-shim-mokmanager><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>Queue the cert and set a longer prompt timeout:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo mokutil --revoke-import
<span class=sr-only>Link to heading</span></a></h3><p>Queue the cert and set a longer prompt timeout:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>sudo mokutil --revoke-import
</span></span><span style=display:flex><span>sudo mokutil --import /var/lib/dkms/mok.der
</span></span><span style=display:flex><span>sudo mokutil --timeout <span style=color:#a5d6ff>30</span>
</span></span><span style=display:flex><span>sudo efibootmgr -n <span style=color:#a5d6ff>0002</span> <span style=color:#8b949e;font-style:italic># ensure next boot goes through shim</span>
</span></span></code></pre></div><p>Reboot to the VM console (not SSH). In the blue MOK UI:</p><ul><li>Enroll MOK → Continue → Yes → enter password → reboot</li></ul><p>If arrow keys dont work in Proxmox noVNC:</p><ul><li>Use SPICE (virt-viewer), or</li><li>From the Proxmox host, send keys:<ul><li><code>qm sendkey &lt;VMID> down</code>, <code>qm sendkey &lt;VMID> ret</code>, <code>qm sendkey &lt;VMID> esc</code></li></ul></li></ul><h3 id=verification>Verification
<a class=heading-link href=#verification><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo mokutil --test-key /var/lib/dkms/mok.der <span style=color:#8b949e;font-style:italic># “already enrolled”</span>
<span class=sr-only>Link to heading</span></a></h3><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>sudo mokutil --test-key /var/lib/dkms/mok.der <span style=color:#8b949e;font-style:italic># “already enrolled”</span>
</span></span><span style=display:flex><span>sudo modprobe nvidia
</span></span><span style=display:flex><span>nvidia-smi
</span></span><span style=display:flex><span>kubectl -n gpu-operator get pods -o wide
@@ -59,4 +59,4 @@ nvidia-smi failed to communicate with the NVIDIA driver modprobe nvidia → “K
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -16,8 +16,8 @@ Supabase enters this space with a radically different philosophy: transparency.
<a class=heading-link href=#phase-2-the-foundation-schema--migrations><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p>Translate your model into SQL. For any serious project, use the <strong>Supabase CLI</strong> to manage this process.</p><ol><li><strong>Develop Locally:</strong> Run a full Supabase stack on your machine with <code>supabase start</code>.</li><li><strong>Create Migration Files:</strong> Write your <code>CREATE TABLE</code> statements in SQL files. Define columns, data types, and foreign key <code>REFERENCES</code> to enforce your relationships.</li><li><strong>Version Control:</strong> Commit these migration files to Git. Your database schema is now version-controlled alongside your application code.</li><li><strong>Deploy:</strong> Use <code>supabase db push</code> to apply your migrations to your live production database. This workflow is safe, repeatable, and professional.</li></ol><h4 id=phase-3-the-security-layer-row-level-security>Phase 3: The Security Layer (Row Level Security)
<a class=heading-link href=#phase-3-the-security-layer-row-level-security><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p>This is not an optional step. RLS is the cornerstone of Supabase security.</p><ol><li><strong>Deny by Default:</strong> For any table holding user data, immediately enable RLS. This blocks all access until you explicitly grant it.</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-sql data-lang=sql><span style=display:flex><span><span style=color:#ff7b72>ALTER</span><span style=color:#6e7681> </span><span style=color:#ff7b72>TABLE</span><span style=color:#6e7681> </span>tasks<span style=color:#6e7681> </span>ENABLE<span style=color:#6e7681> </span><span style=color:#ff7b72>ROW</span><span style=color:#6e7681> </span><span style=color:#ff7b72>LEVEL</span><span style=color:#6e7681> </span><span style=color:#ff7b72>SECURITY</span>;<span style=color:#6e7681>
</span></span></span></code></pre></div><ol start=2><li><strong>Write &ldquo;Allow&rdquo; Policies:</strong> Create policies based on your user stories. Policies are SQL rules that the database enforces on every single query.</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-sql data-lang=sql><span style=display:flex><span><span style=color:#8b949e;font-style:italic>-- Users can see tasks in projects they are a member of.
<span class=sr-only>Link to heading</span></a></h4><p>This is not an optional step. RLS is the cornerstone of Supabase security.</p><ol><li><strong>Deny by Default:</strong> For any table holding user data, immediately enable RLS. This blocks all access until you explicitly grant it.</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-sql data-lang=sql><span style=display:flex><span><span style=color:#ff7b72>ALTER</span><span style=color:#6e7681> </span><span style=color:#ff7b72>TABLE</span><span style=color:#6e7681> </span>tasks<span style=color:#6e7681> </span>ENABLE<span style=color:#6e7681> </span><span style=color:#ff7b72>ROW</span><span style=color:#6e7681> </span><span style=color:#ff7b72>LEVEL</span><span style=color:#6e7681> </span><span style=color:#ff7b72>SECURITY</span>;<span style=color:#6e7681>
</span></span></span></code></pre></div><ol start=2><li><strong>Write &ldquo;Allow&rdquo; Policies:</strong> Create policies based on your user stories. Policies are SQL rules that the database enforces on every single query.</li></ol><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-sql data-lang=sql><span style=display:flex><span><span style=color:#8b949e;font-style:italic>-- Users can see tasks in projects they are a member of.
</span></span></span><span style=display:flex><span><span style=color:#ff7b72>CREATE</span><span style=color:#6e7681> </span>POLICY<span style=color:#6e7681> </span><span style=color:#a5d6ff>&#34;Allow read access to tasks in user&#39;s projects&#34;</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#ff7b72>ON</span><span style=color:#6e7681> </span>tasks<span style=color:#6e7681> </span><span style=color:#ff7b72>FOR</span><span style=color:#6e7681> </span><span style=color:#ff7b72>SELECT</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#ff7b72>USING</span><span style=color:#6e7681> </span>(<span style=color:#6e7681>
@@ -34,8 +34,8 @@ Supabase enters this space with a radically different philosophy: transparency.
</span></span></span><span style=display:flex><span><span style=color:#ff7b72>WITH</span><span style=color:#6e7681> </span><span style=color:#ff7b72>CHECK</span><span style=color:#6e7681> </span>(<span style=color:#6e7681> </span>auth.uid()<span style=color:#6e7681> </span><span style=color:#ff7b72;font-weight:700>=</span><span style=color:#6e7681> </span>tasks.assignee_id<span style=color:#6e7681> </span>);<span style=color:#6e7681>
</span></span></span></code></pre></div><p>The <code>auth.uid()</code> function is a special Supabase utility that securely returns the ID of the logged-in user making the request.</p><h4 id=phase-4-the-apis-data-access>Phase 4: The APIs (Data Access)
<a class=heading-link href=#phase-4-the-apis-data-access><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p>With your data structured and secured, you can now build the access points.</p><ul><li><strong>For Simple CRUD:</strong> Use Supabase&rsquo;s auto-generated API. It&rsquo;s convenient, respects all your RLS policies, and is perfect for simple reads and writes on a single table.</li></ul><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-javascript data-lang=javascript><span style=display:flex><span><span style=color:#ff7b72>const</span> { data, error } <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#ff7b72>await</span> supabase.from(<span style=color:#a5d6ff>&#39;tasks&#39;</span>).select(<span style=color:#a5d6ff>&#39;*&#39;</span>);
</span></span></code></pre></div><ul><li><strong>For Complex Logic:</strong> Use PostgreSQL Functions (RPC). Encapsulate complex <code>JOIN</code>s or multi-step transactions into a single, callable function. This reduces network chattiness and keeps your business logic secure on the server.</li></ul><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-sql data-lang=sql><span style=display:flex><span><span style=color:#8b949e;font-style:italic>-- A function to get a task and its project name in one call
<span class=sr-only>Link to heading</span></a></h4><p>With your data structured and secured, you can now build the access points.</p><ul><li><strong>For Simple CRUD:</strong> Use Supabase&rsquo;s auto-generated API. It&rsquo;s convenient, respects all your RLS policies, and is perfect for simple reads and writes on a single table.</li></ul><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-javascript data-lang=javascript><span style=display:flex><span><span style=color:#ff7b72>const</span> { data, error } <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#ff7b72>await</span> supabase.from(<span style=color:#a5d6ff>&#39;tasks&#39;</span>).select(<span style=color:#a5d6ff>&#39;*&#39;</span>);
</span></span></code></pre></div><ul><li><strong>For Complex Logic:</strong> Use PostgreSQL Functions (RPC). Encapsulate complex <code>JOIN</code>s or multi-step transactions into a single, callable function. This reduces network chattiness and keeps your business logic secure on the server.</li></ul><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-sql data-lang=sql><span style=display:flex><span><span style=color:#8b949e;font-style:italic>-- A function to get a task and its project name in one call
</span></span></span><span style=display:flex><span><span style=color:#ff7b72>CREATE</span><span style=color:#6e7681> </span><span style=color:#ff7b72>OR</span><span style=color:#6e7681> </span><span style=color:#ff7b72>REPLACE</span><span style=color:#6e7681> </span><span style=color:#ff7b72>FUNCTION</span><span style=color:#6e7681> </span>get_task_with_project(task_id_input<span style=color:#6e7681> </span>int)<span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#ff7b72>RETURNS</span><span style=color:#6e7681> </span><span style=color:#ff7b72>TABLE</span><span style=color:#6e7681> </span>(task_title<span style=color:#6e7681> </span>text,<span style=color:#6e7681> </span>project_name<span style=color:#6e7681> </span>text)<span style=color:#6e7681> </span><span style=color:#ff7b72>AS</span><span style=color:#6e7681> </span><span style=color:#f85149>$$</span><span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#ff7b72>BEGIN</span><span style=color:#6e7681>
@@ -46,18 +46,18 @@ Supabase enters this space with a radically different philosophy: transparency.
</span></span></span><span style=display:flex><span><span style=color:#6e7681> </span><span style=color:#ff7b72>WHERE</span><span style=color:#6e7681> </span>tasks.id<span style=color:#6e7681> </span><span style=color:#ff7b72;font-weight:700>=</span><span style=color:#6e7681> </span>task_id_input;<span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#ff7b72>END</span>;<span style=color:#6e7681>
</span></span></span><span style=display:flex><span><span style=color:#f85149>$$</span><span style=color:#6e7681> </span><span style=color:#ff7b72>LANGUAGE</span><span style=color:#6e7681> </span>plpgsql;<span style=color:#6e7681>
</span></span></span></code></pre></div><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-javascript data-lang=javascript><span style=display:flex><span><span style=color:#8b949e;font-style:italic>// Called simply from the frontend
</span></span></span></code></pre></div><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-javascript data-lang=javascript><span style=display:flex><span><span style=color:#8b949e;font-style:italic>// Called simply from the frontend
</span></span></span><span style=display:flex><span><span style=color:#ff7b72>const</span> { data, error } <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#ff7b72>await</span> supabase.rpc(<span style=color:#a5d6ff>&#39;get_task_with_project&#39;</span>, { task_id_input<span style=color:#ff7b72;font-weight:700>:</span> <span style=color:#a5d6ff>123</span> });
</span></span></code></pre></div><h3 id=a-tour-of-the-core-services>A Tour of the Core Services
<a class=heading-link href=#a-tour-of-the-core-services><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>Beyond the database, Supabase provides a suite of essential tools.</p><h4 id=authentication>Authentication
<a class=heading-link href=#authentication><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p>A complete user management system that integrates directly with your database. When a user signs up, a corresponding entry is created in the managed <code>auth.users</code> table, which you can then reference in your own tables.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-javascript data-lang=javascript><span style=display:flex><span><span style=color:#8b949e;font-style:italic>// Sign up a new user and handle social logins with ease
<span class=sr-only>Link to heading</span></a></h4><p>A complete user management system that integrates directly with your database. When a user signs up, a corresponding entry is created in the managed <code>auth.users</code> table, which you can then reference in your own tables.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-javascript data-lang=javascript><span style=display:flex><span><span style=color:#8b949e;font-style:italic>// Sign up a new user and handle social logins with ease
</span></span></span><span style=display:flex><span><span style=color:#ff7b72>const</span> { data, error } <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#ff7b72>await</span> supabase.auth.signUp({ email, password });
</span></span><span style=display:flex><span><span style=color:#ff7b72>const</span> { data, error } <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#ff7b72>await</span> supabase.auth.signInWithOAuth({ provider<span style=color:#ff7b72;font-weight:700>:</span> <span style=color:#a5d6ff>&#39;github&#39;</span> });
</span></span></code></pre></div><h4 id=storage>Storage
<a class=heading-link href=#storage><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p>A simple, S3-compatible object store for managing files like user avatars or documents. It&rsquo;s integrated with Postgres and RLS, allowing you to write fine-grained access policies on files and folders (buckets).</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-javascript data-lang=javascript><span style=display:flex><span><span style=color:#8b949e;font-style:italic>// Upload a user avatar to a public &#39;avatars&#39; bucket
<span class=sr-only>Link to heading</span></a></h4><p>A simple, S3-compatible object store for managing files like user avatars or documents. It&rsquo;s integrated with Postgres and RLS, allowing you to write fine-grained access policies on files and folders (buckets).</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-javascript data-lang=javascript><span style=display:flex><span><span style=color:#8b949e;font-style:italic>// Upload a user avatar to a public &#39;avatars&#39; bucket
</span></span></span><span style=display:flex><span><span style=color:#ff7b72>const</span> { error } <span style=color:#ff7b72;font-weight:700>=</span> <span style=color:#ff7b72>await</span> supabase.storage
</span></span><span style=display:flex><span> .from(<span style=color:#a5d6ff>&#39;avatars&#39;</span>)
</span></span><span style=display:flex><span> .upload(<span style=color:#a5d6ff>`public/</span><span style=color:#a5d6ff>${</span>userId<span style=color:#a5d6ff>}</span><span style=color:#a5d6ff>.png`</span>, file);
@@ -71,7 +71,7 @@ Supabase enters this space with a radically different philosophy: transparency.
<a class=heading-link href=#transactional-integrity><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><p>The most important guarantee of this system is its relationship with database transactions. An event is <strong>only broadcast <em>after</em> a transaction is fully and successfully committed.</strong> If a transaction is rolled back due to an error, the replication slot receives nothing, and no Realtime event is ever sent. This means you can trust that every Realtime message you receive corresponds to data that is permanently and consistently stored in your database.</p><h4 id=use-cases-and-limitations>Use Cases and Limitations
<a class=heading-link href=#use-cases-and-limitations><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h4><ul><li><strong>Use For:</strong> Small, JSON-based messages like chat messages, live notifications, activity feeds, and presence indicators (&ldquo;who&rsquo;s online&rdquo;). Use the <code>broadcast</code> feature for ephemeral data like cursor positions that you don&rsquo;t need to save.</li><li><strong>Do NOT Use For:</strong> Large, continuous data streams. It is <strong>not</strong> a replacement for WebRTC for video/audio calls. The system is designed for small, infrequent payloads.</li></ul><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-javascript data-lang=javascript><span style=display:flex><span><span style=color:#ff7b72>const</span> channel <span style=color:#ff7b72;font-weight:700>=</span> supabase.channel(<span style=color:#a5d6ff>&#39;public:messages&#39;</span>);
<span class=sr-only>Link to heading</span></a></h4><ul><li><strong>Use For:</strong> Small, JSON-based messages like chat messages, live notifications, activity feeds, and presence indicators (&ldquo;who&rsquo;s online&rdquo;). Use the <code>broadcast</code> feature for ephemeral data like cursor positions that you don&rsquo;t need to save.</li><li><strong>Do NOT Use For:</strong> Large, continuous data streams. It is <strong>not</strong> a replacement for WebRTC for video/audio calls. The system is designed for small, infrequent payloads.</li></ul><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-javascript data-lang=javascript><span style=display:flex><span><span style=color:#ff7b72>const</span> channel <span style=color:#ff7b72;font-weight:700>=</span> supabase.channel(<span style=color:#a5d6ff>&#39;public:messages&#39;</span>);
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic>// Subscribe to new rows in the &#39;messages&#39; table
</span></span></span><span style=display:flex><span>channel
@@ -90,4 +90,4 @@ Supabase enters this space with a radically different philosophy: transparency.
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -30,4 +30,4 @@ But to truly understand the field, we must look at the pivotal models that explo
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -10,7 +10,7 @@ For years, I relied on a rule-based system to categorize our credit card transac
<a class=heading-link href=#phase-1-the-proof-of-concept-with-commercial-llms><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>My first step was to replace the spaghetti code of regex rules with a prompt. I used <strong>Gemini-3-Flash</strong> (via <code>litellm</code>) as my categorization engine.</p><p>The core challenge was context. A transaction like <code>MCDONALDS</code> could be:</p><ul><li><strong>Dining</strong>: A quick lunch during work.</li><li><strong>Travel-Dining</strong>: A meal while on a road trip.</li></ul><p>To solve this, I integrated my <strong>private Google Calendar</strong> (via <code>.ics</code> export). The prompt doesn&rsquo;t just see the transaction; it sees <em>where I was</em> and <em>what I was doing</em> on that day.</p><h3 id=the-god-prompt>The &ldquo;God Prompt&rdquo;
<a class=heading-link href=#the-god-prompt><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>The system prompt was designed to return strict JSON, adhering to a schema of Categories (e.g., <code>Dining</code>, <code>Travel</code>, <code>Bills</code>) and Sub-Categories (e.g., <code>Travel</code> -> <code>Accommodation</code>).</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-json data-lang=json><span style=display:flex><span>{
<span class=sr-only>Link to heading</span></a></h3><p>The system prompt was designed to return strict JSON, adhering to a schema of Categories (e.g., <code>Dining</code>, <code>Travel</code>, <code>Bills</code>) and Sub-Categories (e.g., <code>Travel</code> -> <code>Accommodation</code>).</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-json data-lang=json><span style=display:flex><span>{
</span></span><span style=display:flex><span> <span style=color:#7ee787>&#34;Category&#34;</span>: <span style=color:#a5d6ff>&#34;Travel&#34;</span>,
</span></span><span style=display:flex><span> <span style=color:#7ee787>&#34;Travel Category&#34;</span>: <span style=color:#a5d6ff>&#34;Dining&#34;</span>,
</span></span><span style=display:flex><span> <span style=color:#7ee787>&#34;Reasoning&#34;</span>: <span style=color:#a5d6ff>&#34;User is on &#39;Trip: 34TH ARCH CANYON 2025&#39;, distinguishing this from regular dining.&#34;</span>
@@ -19,7 +19,7 @@ For years, I relied on a rule-based system to categorize our credit card transac
<a class=heading-link href=#phase-2-distilling-knowledge><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>I wanted to train a smaller model to mimic Gemini&rsquo;s performance. But I didn&rsquo;t want to manually label thousands of transactions.</p><h3 id=consistency-filtering>Consistency Filtering
<a class=heading-link href=#consistency-filtering><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>I had a massive CSV of historical transactions (years of data). However, that data was &ldquo;noisy&rdquo;—some manual labels were outdated or inconsistent.</p><p>I built a <strong>Distillation Pipeline</strong> (<code>distill_reasoning.py</code>) that uses the Teacher Model (Gemini) to re-label the historical data. But here&rsquo;s the twist: I only added a data point to my training set if the <strong>Teacher&rsquo;s prediction matched the Historical Ground Truth</strong>.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-python data-lang=python><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Pseudo-code for consistency filtering</span>
<span class=sr-only>Link to heading</span></a></h3><p>I had a massive CSV of historical transactions (years of data). However, that data was &ldquo;noisy&rdquo;—some manual labels were outdated or inconsistent.</p><p>I built a <strong>Distillation Pipeline</strong> (<code>distill_reasoning.py</code>) that uses the Teacher Model (Gemini) to re-label the historical data. But here&rsquo;s the twist: I only added a data point to my training set if the <strong>Teacher&rsquo;s prediction matched the Historical Ground Truth</strong>.</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-python data-lang=python><span style=display:flex><span><span style=color:#8b949e;font-style:italic># Pseudo-code for consistency filtering</span>
</span></span><span style=display:flex><span>teacher_pred <span style=color:#ff7b72;font-weight:700>=</span> gemini<span style=color:#ff7b72;font-weight:700>.</span>categorize(transaction)
</span></span><span style=display:flex><span>historical_label <span style=color:#ff7b72;font-weight:700>=</span> row[<span style=color:#a5d6ff>&#39;Category&#39;</span>]
</span></span><span style=display:flex><span>
@@ -73,4 +73,4 @@ It turned out to be a syntax error in my arguments passed to the <code>Trainer</
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -26,4 +26,4 @@ This article explores the mathematical equivalence between Hintons concept of
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -36,4 +36,4 @@ In deep learning, a &ldquo;channel&rdquo; can be thought of as a feature dimensi
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -28,4 +28,4 @@ This article documents that journey. It details the pitfalls encountered, the co
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -9,4 +9,4 @@ One-minute read</span></div></div></header><div class=post-content><ul><li><a hr
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -31,4 +31,4 @@ The GPU TLB hierarchy is sensitive to page sizes.</p><ul><li><strong>4KB Pages:<
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -12,17 +12,17 @@ Growing up in China before the internet age, my window to the outside world was
<a class=heading-link href=#the-hardware-blocker-getting-input><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>The first hurdle was mundane but blocking: My Bluetooth keyboard wouldn&rsquo;t pair. The head unit could see other devices, but refused to connect to my keyboard.</p><h3 id=attempt-1-the-usb-dongle-bypass>Attempt 1: The USB Dongle Bypass
<a class=heading-link href=#attempt-1-the-usb-dongle-bypass><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>My first instinct was to blame the cheap Chinese head unit hardware. I grabbed a spare TP-Link USB Bluetooth dongle and plugged it in, hoping to bypass the internal stack entirely.</p><p>The device showed up in <code>lsusb</code>, but it remained inert. A quick check of the kernel config via <code>zcat /proc/config.gz</code> revealed the bad news:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># CONFIG_BT is not set</span>
<span class=sr-only>Link to heading</span></a></h3><p>My first instinct was to blame the cheap Chinese head unit hardware. I grabbed a spare TP-Link USB Bluetooth dongle and plugged it in, hoping to bypass the internal stack entirely.</p><p>The device showed up in <code>lsusb</code>, but it remained inert. A quick check of the kernel config via <code>zcat /proc/config.gz</code> revealed the bad news:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#8b949e;font-style:italic># CONFIG_BT is not set</span>
</span></span></code></pre></div><p>The kernel was compiled without generic Bluetooth driver support (<code>btusb</code>). Even with root access, I couldn&rsquo;t load the drivers because they simply didn&rsquo;t exist in the firmware. I was stuck with the internal hardware.</p><h3 id=attempt-2-the-dual-bluetooth-fix>Attempt 2: The &ldquo;Dual Bluetooth&rdquo; Fix
<a class=heading-link href=#attempt-2-the-dual-bluetooth-fix><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h3><p>Forced back to the built-in Bluetooth, I tried to diagnose why it was ignoring my keyboard. Standard debugging tools painted a grim picture:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span> hciconfig -a
<span class=sr-only>Link to heading</span></a></h3><p>Forced back to the built-in Bluetooth, I tried to diagnose why it was ignoring my keyboard. Standard debugging tools painted a grim picture:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span> hciconfig -a
</span></span><span style=display:flex><span><span style=color:#8b949e;font-style:italic># (Empty output - no standard HCI interface found)</span>
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> ps -A | grep -iE <span style=color:#a5d6ff>&#34;goc|ivt|syu&#34;</span>
</span></span><span style=display:flex><span>u0_a50 <span style=color:#a5d6ff>3456</span> ... com.goc.sdk <span style=color:#8b949e;font-style:italic># Accessing the proprietary BT chip</span>
</span></span></code></pre></div><p>The diagnosis was clear: The internal Bluetooth chip is acting in <strong>Slave Mode</strong> (Client), managed by a proprietary <code>com.goc.sdk</code> service instead of the standard Android Bluetooth stack. It&rsquo;s designed to <em>be</em> a speaker for your phone, not to <em>host</em> a keyboard.</p><p><strong>The Fix</strong>: Hidden deep in the Factory Settings (password <code>8888</code>), there&rsquo;s a toggle called <strong>&ldquo;Dual Bluetooth&rdquo;</strong>. Enabling this flips the proprietary stack to expose a standard Host interface. Enable that, and suddenly my mechanical keyboard connected instantly.</p><h2 id=the-software-termux--claude>The Software: Termux + Claude
<a class=heading-link href=#the-software-termux--claude><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
<span class=sr-only>Link to heading</span></a></h2><p>With input sorted, the software setup was surprisingly straightforward. <strong>Termux</strong> was the obvious choice for a terminal.</p><p>I discovered that <strong>Claude Code</strong> works on Termux with zero hassle.</p><p>The setup was shockingly simple:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>pkg install nodejs git ripgrep
<span class=sr-only>Link to heading</span></a></h2><p>With input sorted, the software setup was surprisingly straightforward. <strong>Termux</strong> was the obvious choice for a terminal.</p><p>I discovered that <strong>Claude Code</strong> works on Termux with zero hassle.</p><p>The setup was shockingly simple:</p><div class=highlight><pre tabindex=0 style=color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none><code class=language-bash data-lang=bash><span style=display:flex><span>pkg install nodejs git ripgrep
</span></span><span style=display:flex><span>npm install -g @anthropic-ai/claude-code
</span></span></code></pre></div><p>Authentication via <code>claude login</code> worked out of the box. Now, I have a fully capable coding agent running directly on my dashboard. I can pull a repo, ask Claude to refactor a module, and push the changes—all without opening a laptop.</p><p><img src=/images/vibe-coding-from-the-jeep/399000b0b5ee4f5e8961e1d76b6c23c8.png alt="S3 File"></p><h2 id=key-insights>Key Insights
<a class=heading-link href=#key-insights><i class="fa-solid fa-link" aria-hidden=true title="Link to heading"></i>
@@ -32,4 +32,4 @@ Growing up in China before the internet age, my window to the outside world was
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

View File

@@ -4,4 +4,4 @@
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>

File diff suppressed because one or more lines are too long

View File

@@ -4,4 +4,4 @@
2016 -
2026
Eric X. Liu
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/6100dca">[6100dca]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>
<a href="https://git.ericxliu.me/eric/ericxliu-me/commit/45629c5">[45629c5]</a></section></footer></main><script src=/js/coder.min.6ae284be93d2d19dad1f02b0039508d9aab3180a12a06dcc71b0b0ef7825a317.js integrity="sha256-auKEvpPS0Z2tHwKwA5UI2aqzGAoSoG3McbCw73gloxc="></script><script defer src=https://static.cloudflareinsights.com/beacon.min.js data-cf-beacon='{"token": "987638e636ce4dbb932d038af74c17d1"}'></script></body></html>