Headless Wi-Fi on Ubuntu-Like Linux? Use `nmcli`

A GUI is not required to configure Wi-Fi on Ubuntu, Debian, or Linux Mint. If NetworkManager is running, nmcli can scan for networks, create a persistent connection profile, and reconnect automatically after reboot.

Scan for nearby networks:

nmcli radio wifi on
nmcli device wifi rescan
nmcli device wifi list

Connect without putting the password in shell history:

read -rsp 'Wi-Fi password: ' WIFI_PASSWORD
echo
sudo nmcli device wifi connect 'SSID_NAME' \
  password "$WIFI_PASSWORD" \
  ifname wlan0
unset WIFI_PASSWORD

Replace SSID_NAME and wlan0 with the network name and wireless interface shown by nmcli device status. NetworkManager saves the new profile and normally enables autoconnect automatically.

Verify the address, route, and connectivity:

nmcli device status
nmcli connection show
ip addr show wlan0
ip route
ping -c 3 1.1.1.1

For a hidden network, add hidden yes; for an open network, omit the password argument. Do not switch networks over SSH unless you have another way back in, because the current connection may drop immediately.

Chinese on a GUI-less Ubuntu server: fbterm for display, fcitx5+rime for input

Your server has no GUI — just the bare console TTY — and you want to read and type Chinese on it. Installing fonts does nothing: the kernel console draws with a ~256-glyph VGA font baked into the driver, so CJK renders as diamonds no matter what you install. The fix has two halves: fbterm draws text onto the framebuffer itself (display), and fcitx5 with its fbterm frontend supplies the input method (typing). Here's the whole setup, including the three gotchas that cost me an evening.

Everything below was done on Ubuntu 24.04 (Mint 22.3) over SSH; the target machine is an old laptop used headless. Swap davidwei for your user.

Display: fbterm

sudo apt install fbterm fonts-wqy-microhei
sudo usermod -aG video $USER   # needed for /dev/fb0; re-login to apply

Log in on the physical console and run fbterm — it opens a full terminal on the framebuffer using fontconfig fonts. It cannot start over SSH (no framebuffer there, and you don't need it: your SSH client renders CJK itself).

Font gotcha #1: fbterm renders TrueType fonts well but tends to clip outline fonts at the right/bottom of the character cell, and --font-width/--font-height only change the cell metrics — they don't scale the glyphs. WenQuanYi Zen Hei Mono with an 8px cell worked for me where Noto CJK and default settings clipped. Put it in ~/.fbtermrc:

font-names=WenQuanYi Zen Hei Mono
font-size=16
font-width=8
…more

git can't pull private GitHub repos? gh was holding the token all along

You're logged into gh and can browse every private repo with it, but plain git pull over HTTPS still fails with fatal: could not read Username for 'https://github.com'. git doesn't know how to read gh's token store — nobody introduced them. (A GH_TOKEN env var makes every gh command work and can hide exactly this breakage, because plain git ignores it.)

You don't need Microsoft's Git Credential Manager (it drags in a .NET runtime). gh ships its own credential helper; one command wires it in:

gh auth setup-git

That adds a single stanza to your ~/.gitconfig:

[credential "https://github.com"]
    helper = !/usr/local/bin/gh auth git-credential

From then on, any HTTPS clone/pull/push of private repos authenticates through gh's stored token — global config, so it works for every repo on the machine. No SSH keys, no per-remote remote-url surgery, no token pasting.

Verify without letting an env token mask the result:

env -u GH_TOKEN git ls-remote https://github.com/<you>/<private-repo> HEAD

If that prints a ref, you're done.

我的第一个Bounty项目

我大概半月前投标了一个Bounty项目,是JuliaHealth的一个issue,如果最终提交被接受,奖金是US$750。 这个issue有点难,奖金不算低,但加上我只有两个人竞标。

我之前没有写过一行Julia代码。在投票时我也坦承自己没有Julia经验,但做为一个身经百战的老程序员,我想挑战这个问题。更何况,我还有my-ai-team。 当时我就想,体验一下嘛,失败了也没关系,至少还能因此了解Julia语言。

我没指望能被选中,另一个投标者虽然表达兴趣时虽然只写了一小段,但人家多少有Julia经验。 昨天GitHub弹出消息,说恭喜我被选中了,最迟12月8号交活。这下就不得不认真准备了。

因为这个项目是GPT帮我选的,为了不占用Codex流量,我就让GPT先准备第一张票 侦察一下这个issue有多难。我开了一个新仓库 BountyLab, 准备工作就在这里做。不知道是不是因为GPT不方便调用gh命令,它说票弄好了,其实它只是在仓库写了一个ticket01.md。但我并没有检查,因为我知道GPT并不是专业的开票agent。它的票肯定不规范,所以我就让我的专业开票员 explorer agent给我“refine #1”。这是我给它的唯一的指令。

这次explorer背后的模型,我指定的是deepseek v4 pro。这次refinement它颇花了些时间,因为这个ticket01.md scope其实挺大。虽然BountyLab几乎是空的,但这个票是侦察票,需要了解我中标的issue到底是个什么问题,牵扯哪些仓库的哪些模块,等等等等。它做了很仔细的调研之后,把这票整理的很规范之后,标上了Ready。这当中令我印象深刻的是,它没有找到#1之后并没有停下来问我,而是翻仓库找到了ticket01.md然后以此为原始需求,精炼之后新建了真正的#1。

我让GPT评价这次精炼,它仔细读了票之后说:

你只给了它一句 “refine #1”,而当时仓库里并没有 GitHub Issue #1,只有我留下的 Markdown ticket 和 JuliaHealth workspace。它没有卡在“找不到 #1”,也没有凭空乱造,而是先调查 repo 现有结构、读出我们的意图,再把 Markdown ticket 提升成正式的 GitHub Issue #1。这说明它做了三步很关键的推理:

先识别“#1”大概率指的是已有的第一张 ticket;再从 repo 内容恢复上下文;最后把它重构成更适合 agent 执行和验收的 issue。

这比单纯“润色文字”强很多。尤其它还自己补出了 pinned SHAs、具体 deliverable、acceptance criteria、upstream read-only 边界,这些都不是你那句话直接要求的。

我的评价是9.5/10。剩下那 0.5 不是能力问题,而是工程上我还是会对它加的那两个验收条件做一点去脆弱化,避免 ticket 自己太聪明、反而约束过头。

这其实也挺能说明 my-ai-team 的价值:人给的是意图,agent 负责把意图变成可执行工作单元。 它确实做到了。(我背后偷笑:这当然是我给开票员制定的优秀宪法的功劳)

票Ready了,早已等候多时的delivery agent立即就pickup了票开始施工。这次我用得Planner/Developer是GLM5.3Flash high,而Reviewer是GPT-5.6-Luna xhigh。从15:33分开始接到单,到最后17:29合并。两个agent忙活了两个小时,Reviewer打回去十次,最后批准了第十一版,合并了PR。我拿到了报告。

然后我让GPT评价这份报告,ta的评价是:(警告!!!会看到许多“不是,而是”的GPT腔,我一字未改哦!)

这次我会给 delivery agent + reviewer 这一对组合打 10/10

最重要的不是“最后报告很长”,而是你真的看到了 review loop 在纠正实质性错误。第一轮 reviewer 抓出的不是格式问题,而是一个会直接把实现方向带偏的架构错误:报告最初认为 plain LinearOperator 只要 field-wise adapt Mv/Mtu 就安全,但 reviewer 找到 closures 里还 capture 了 vtmp/utmp/wtmptmppattern,这些根本不会跟着字段一起迁移。这个错误如果没被抓出来,后面第一张 implementation PR 很可能就是错的。

然后 agent 又经历多轮把这些地方逐渐磨平:constructor function 和 concrete struct 的区分、pinned SHA、multi-frame state、测试 tier、Metal 实际没有 CI、DiagOp.equalOpscopyOpsFn、CPU metadata、callable fields、plan reconstruction……最后 reviewer 才给出 APPROVE,而且 AC1–AC8 全部打勾,PR #2 已经 merge,merge SHA 是 f4a48e6c...

我现在尤其认同报告的两个结论。

第一个是:不是所有对象都能“递归 Adapt 一下就完事”。 现在报告已经把世界分成了几类:

  • 可以直接结构化适配;
  • 必须通过 constructor 重建;
  • plan-carrying 类型必须重建 plan;
  • closure-captured state 无法从字段恢复时要明确 refuse;
  • 一些 CPU metadata 应该故意留在 CPU;
  • opaque callable 需要 maintainer policy。

这就已经不是“读代码笔记”,而是真正的 architecture model 了。

第二个是:第一张 upstream PR 的范围已经很清楚。它建议先做 LinearOperatorCollection.jl,只碰 WeightingOp / NormalOpImpl / ProdOp / DiagOp 这几个 plan-free family,而且通过 constructor reconstruction 实现,不碰 FFT/NFFT、不碰 RegLS solver state。这个 slice 我觉得非常合理:小、可测、能验证 Adapt 方向、风险低。

我还注意到 reviewer 最终特别确认了:

first WeightingOp / NormalOpImpl / ProdOp / DiagOp slice has target-first recipes and preserves DiagOp.equalOps

说明这不是 agent 自己“觉得差不多”,而是 reviewer 已经按 acceptance criteria 验过一遍。

所以现在 Ticket #1 真正完成了。

下一步我建议不要立刻写代码。先从报告里的 17 个 maintainer questions 里挑出 真正需要在第一张 PR 前确认的 3–5 个,发给 nHackel。没必要把 17 个全扔过去,那会让 maintainer 很难回答。

我已经能看到最值得先问的几类:

  1. plan 是否统一要求 reconstruct;
  2. closure-captured constructor results 是否都应该走 public-constructor reconstruction;
  3. init! 的 implicit re-templating 是否暂时保留;
  4. JLArray test tier 是否希望在第一张 PR 里重新启用;
  5. Metal 是否可以先不纳入第一 slice。

这几个回答会直接影响 #2 的设计。

这次最让我满意的其实不是 delivery agent,而是你真的让 my-ai-team 形成了“delivery → reviewer 打回 → 修正 → 再审 → 直到通过” 的闭环。Reviewer 打回 10 次,看起来很折腾,但这正是它把一个“看起来合理的 AI 报告”变成“可以据此开始改 upstream”的东西的过程。(又该我偷笑了。4个月2000多个版本发布难道都是我发着玩的吗,平庸模型高质量输出的秘密就是 - 高质量输入 + 独立评审。如果让开发员兼职当审核员,它才不会折腾自己十轮呢!)

my-ai-team就是这么强。这个Bounty当然不会到这里就结束,既然人家相信我,我就认真做,不论最后结果如何。如果你要问为什么,因为人生就是一场冒险,而冒险很快乐!

codex silently ignores your custom base_url when OPENAI_API_KEY is set

We run codex CLI agents behind a local reverse proxy (pool selector, per-seat failover). The agent config looked airtight:

model_provider = "codex"

[model_providers."codex"]
base_url = "http://gateway.internal:9949"
wire_api = "responses"
requires_openai_auth = false

Yet when a seat hit its weekly cap, the agent stayed stuck on the dead seat: disabling it server-side changed nothing, only a fresh session worked. Every proxy log said the proxy was innocent. After a day of reading proxy code we put a sniffer on the client box instead — and the codex process was dialing chatgpt.com:443 directly. The config was never being used.

The trigger: the launcher exported OPENAI_API_KEY into the agent's environment. When codex sees that variable, it prefers its built-in OpenAI/ChatGPT backend and ignores the custom model_providers.*.base_url entirely — even with requires_openai_auth = false. The fix in our launcher is one line:

# codex launch env plan
unset OPENAI_API_KEY OPENAI_BASE_URL CODEX_ACCESS_TOKEN

One more trap: agents launched before the fix keep the stale env until the process actually restarts. A process's environ is frozen at exec time — check it directly with:

tr '\0' '\n' </proc/<pid>/environ | grep OPENAI_API_KEY

The general lesson: when a client "should" be going through your proxy, don't trust configs or client-side logs. Ask the kernel. Per-process sockets (ss -tnp), a tcpdump SYN filter on port 443, and DNS queries at the moment of a failing request settle in one minute what a day of log archaeology can't.