[{"content":" Before you read: every number here comes from a command I ran on this machine on 2026-10-11 — git log, wc -l, go.mod, xdev --version, /usr/bin/time -l. I wrote the commands into the text so you can argue with them. Nothing is estimated unless I say it is.\nI already wrote the story of the first seven days. This is the different story: what happened after the origin myth — the whole arc, from the first commit to the 743rd, and what the thing actually is now.\nIf you only want one fact: the first commit is 2026-09-09 and it is a document, not a program.\n1) Day one started with reading, not coding 2026-09-09 10:36 dcf378d docs: initial PRD, README, and omp/pi Go-rebuild research 2026-09-09 11:29 05b9352 docs: extend PRD/README to full omp feature parity; add M9-M14 issues 2026-09-09 13:06 7e516be docs: fix stale non-goal — theme engine is in scope (M12 CORE) 2026-09-09 13:40 ba1b809 docs: Claude Code cross-check — research, verdicts, and evidence 2026-09-09 15:22 000a14c docs: second-wave cross-harness research — omp/pi/CC/opencode/hermes internals 2026-09-09 17:27 a024b62 feat: MVP — providers, session core, agent loop, 4 core tools, print mode Six documentation commits before the first line of Go. That was not virtue, it was fear. I had just spent weeks reading that my own daily tools were 200 MB binaries wrapping a JS runtime, and the temptation was to start typing immediately and be proud of a main.go. What I did instead was write down what each of the five peer harnesses actually does — pi, omp, Claude Code, opencode, Hermes — with a verdict per feature: port, port differently, reject. All of it landed in docs/research/, which is now 29 documents.\nBy that evening there was a working MVP: providers, session core, agent loop, four tools, print mode. One more commit opened the TUI. A month later, that research directory is the single reason the project did not become a pile of one-off decisions.\n2. The first four days are a third of the project Here is the actual rhythm of the thing, from git log --format='%ad' --date=short | sort | uniq -c:\nRead that chart carefully, because it is the story:\nDay 1 (11 commits). Research, PRD, MVP, TUI. Milestones M0–M4 — skeleton, provider layer, session core, agent loop, terminal UI — closed the same day. Days 2–4 (24, 116, 132 commits). M5–M15 closed on 2026-09-11 and 2026-09-12: compaction and failover, RPC mode, subprocess extensions, the memory-hardening audit, model resolution and auth, session UX, the agent system, memory and skills, the extended tool set, the v2 modes, and the full-parity tail. Days 5–13. The field-driven pass: I used the thing daily, and every annoyance got fixed the day it annoyed me (more on this in §3). Days 14–18. The flat week — 4 commits, then 1. I was not working on xdev. Worth mentioning, because retrospectives usually delete this part. Days 20–32. The daily-driver pass: TUI polish, multi-session tabs, the sidebar, and on the last day the Telegram gateway. 283 of 743 commits landed in the first four days. That is 38% of the project in 12% of the calendar. Then the shape changes completely: no more big bang, just 4 to 30 commits a day of nervous small fixes in a UI I was using for hours at a time.\nThe longest gap between two commit days is three days. Zero Revert \u0026quot; commits in the entire history, which is what a repo looks like when the research was done first.\n3. The device that made this possible: the PRD as a diary Here is the process fact I am proudest of, and it is measurable:\nCommits that touch docs/PRD.md 364 \u0026hellip;of those, commits that also touch a .go file 266 *Last updated:* stamps inside the PRD 466 Mentions of \u0026ldquo;user report\u0026rdquo; in the PRD 23 Total PRD length 3,508 lines The rule was simple and absolute, and it is written into CONTRIBUTING.md:\nAny change to scope, architecture or status updates docs/PRD.md in the same commit.\nNot \u0026ldquo;in the same PR\u0026rdquo;. In the same commit. So when I run git log -- docs/PRD.md I get a diary, and when I read a stamp like this one, the bug is still readable months later:\n*Last updated: 2026-10-06 (fix/stop-at-the-report — user report: \u0026#34;my xdev alwasy in the overthinking mode which is for the simple task or just research task...\u0026#34;):* the symptom was one bug; the deepdive turned it into three Twenty-three of those stamps quote me, verbatim, being annoyed at my own tool. That is the highest-value artifact in the repository — higher than the code, because the code I can rewrite, and the reason I wrote it I cannot reconstruct.\nIt also keeps the milestone table honest. The PRD §4 table has one row per milestone M0–M15, with scope, exit criterion, status, and the issue numbers. There is no TASKS.md in the repo, on purpose: tasks are GitHub issues, the PRD is a status summary. 194 issues are open right now, mostly P2 breadth.\n4. What it is made of $ find . -name \u0026#39;*.go\u0026#39; -not -path \u0026#39;./.worktrees/*\u0026#39; | wc -l 816 $ # non-test lines 128,179 $ # test lines 102,085 $ find . -name \u0026#39;*_test.go\u0026#39; | wc -l 458 44% of the Go in this repo is tests. 458 test files against 358 source files. For a project where \u0026ldquo;does it still work\u0026rdquo; is answered by looking at a terminal, that ratio is the whole safety net.\ngo.mod has seven direct dependencies: tcell, go-runewidth, the official MCP Go SDK, golang.org/x/sys, golang.org/x/term, yaml.v3, and modernc.org/sqlite (used in exactly one file, to read Cursor\u0026rsquo;s local IDE state for xdev connect cursor). No CGO, so CGO_ENABLED=0 cross-compiles to six platforms from one machine. Everything else — git, rg, fd, ast-grep, gh, python3, /usr/bin/security — is shelled out to and probed, never required.\nAnd the numbers I re-measured today, on the installed release:\nBinary 21.8 MB xdev --version, median of 5 12 ms Peak RSS for that run 21.0–21.6 MB (footprint 11.1–11.4 MB) Same measurement, omp / Claude Code / opencode 198.6 MB / 229.5 MB / 171.9 MB A month in, the gap held. That was the hypothesis I was testing on day 1, and it survived contact with 743 commits of feature work — because the constraint was enforced in code, not in intent:\n// internal/memlimit const DefaultLimitBytes = 100 \u0026lt;\u0026lt; 20 // hard worst-case ceiling: 100 MB const HighPressure = 0.85 // live heap fraction that forces compaction 5. One turn, end to end This is what actually runs when you type something. Every box is a real file.\nFour things in that diagram are decisions I would defend in a review:\nThe tool batch is genuinely concurrent, and order is restored after the join. internal/agent/loop.go runs one goroutine per tool call under a semaphore of 6 (MaxToolWorkers = 6, which the PRD budgets as \u0026ldquo;~4–8\u0026rdquo;). Then, after wg.Wait() and on the single goroutine, it walks the results in model order to feed the repeat-call guard. That ordering is deliberate: the loop the guard exists to catch is a sequence of calls as the model wrote them, not as my workers happened to finish them.\nCompaction has multiple triggers, and two of them ignore tokens entirely. The token trigger fires at the context window minus a reserve, where the reserve is at least 20% of the window — so ~80% is where a 200k session compacts. internal/agent/compact.go adds two more: a heap-pressure trigger at 85% of that 100 MB limit (so a runaway transcript degrades into \u0026ldquo;compact now\u0026rdquo; instead of being OOM-killed), and an idle trigger. Behind them sits a ladder — threshold, overflow, promotion are the triggers; handoff, snapcompact, shake, soft are the products, and the first one that yields a retained context wins. snapcompact, shake and soft are deterministic: no model call, so they work offline. remote is a documented no-op that says so instead of dressing a local summary up as a provider-native one.\nThe session is append-only, and the leaf is a pointer. Not one entry is ever mutated. A fork moves a pointer; context is rebuilt by walking parent links; jq reads the file. This is the one design decision I ported wholesale from pi/omp\u0026rsquo;s data model rather than reinventing, and the interop test in internal/session/interop_test.go runs against a real omp session fixture to keep the format honest. xdev -from-claude and -from-codex import another harness\u0026rsquo;s transcript and continue it.\nBounded everything, including the load path. The M8 audit measured a 50k-entry session loading to a 130 MB heap — over budget. The fix (ce8fcae, 2026-09-11) was windowed materialization: entries before the latest reset or compaction boundary are dropped at load, indexes are rebuilt in batches, and the file on disk is untouched. The bound is on memory, not on history.\nThe system prompt next to all of this is deliberately tiny: SystemPromptBase is 1,889 characters (about 470 tokens at the crude 4-chars-per-token estimate), and there is a test that fails if the total prompt plus every tool description crosses 1,000 tokens. It is a gate, not a ruler — it exists so that growing the prompt requires a conscious PRD amendment instead of happening by accident.\n6. What the tool surface actually is now xdev has 42 subcommands (the map in cmd/xdev/main.go) and, today, this many tools the model can call. The core four get the eager schema; the long tail is behind a deferred catalog (tool_search → tool_describe → tool_call), which is why the prompt budget above is still possible:\nLayer Tools The eager core read write edit bash Search and structure grep glob ast_grep ast_edit Run things eval (a persistent Python kernel; .ipynb as cell blocks) lsp debug (DAP: dlv, debugpy, lldb-dap) browser (CDP) Reach out web_search github gmail security_scan computer tts generate_image typesafe Delegate task (subagents with their own tool allowlist) hub send_message inbox vibe_status advise propose Remember and undo learn recall retain reflect checkpoint rewind notes new_context Plan and organise todo goal ask schedule_create schedule_list schedule_delete The bridge tool_search tool_describe tool_call Plus anything from an MCP server (the official Go SDK, stdio and HTTP) or a subprocess extension speaking JSONL on stdio with a per-event timeout and SIGKILL. Both land in the same registry as the built-ins, so a foreign tool is policy-gated and output-sunk exactly like bash.\nEight wire formats, so any OpenAI-compatible gateway works: anthropic-messages, openai-completions, openai-responses, azure-openai-responses, openai-codex-responses, google-generative-ai, google-vertex, gemini-cli. Subscriptions work through the browser OAuth flow (xdev login claude), and xdev connect writes from a generated ~200-host catalog so you rarely hand-write a provider block.\n7. The part I did not plan: it moved into my pocket The last cluster of the 743 is the interesting one, because a month of building a terminal UI ended by making the terminal optional:\n2026-10-11 5bd5f1b feat(gateway): register the command menu in the Telegram client (#626) 2026-10-11 77258d2 feat(gateway): format messages for the phone like Hermes does (#625) 2026-10-11 dee214d fix(gateway): a transport error never carries the bot token (#624) 2026-10-11 bc003ec fix(gateway): a 409 no longer kills the daemon (#623) 2026-10-11 53bd7c5 feat(gateway): the Telegram bridge — background daemon, setup, LeanKG memory (#622) xdev gateway is a background daemon that long-polls Telegram and runs the same agent loop against the same session store, per chat. Five commits, one day, and the four after the first are all defect work:\na 409 from Telegram used to kill the daemon. It shouldn\u0026rsquo;t: Telegram terminates an old long poll when a new getUpdates arrives, so a 409 means someone poked the API — including me, checking it by hand. So it now paces and continues, and a real rival poller still shows up as a 409 on every poll. a transport error was printing the bot token into the log. model markdown is converted to Telegram\u0026rsquo;s HTML subset rather than MarkdownV2, and falls back to plain text when the parser rejects the parse. the bot registers its command menu, so /help exists where a user looks for it. This is the part I would not have predicted from the day-1 plan. The harness did not get better because I added features. It got better because I started driving it from places I did not design it for, and the seams held: one session core, one agent loop, one tool registry, and a different front door.\n7.5. How I tested a thing that only exists as text The repo has 12 scripts/tui-*-drive.py files. They are not unit tests. They drive the real binary in a real pty and assert on the frames it paints — tab policy, sidebar ink, sticky padding, overlay width, thinking colour, subagent transcripts. Plus scripts/tui-shot.py, which runs xdev tui in a detached tmux session, asks tmux for the pane with its colors (capture-pane -p -e), repaints that cell grid as an SVG and rasterizes it.\nNothing about the screenshots in this series is mocked up. Every cell came from what the TUI painted.\nThat test layer exists because of a specific class of bug I hit repeatedly: a modal that paints nothing but still eats keystrokes, or a draw path that re-locks a mutex it already holds and freezes the whole UI while the process looks perfectly alive. gh says 194 issues are open right now, and a large share of the closed ones are that shape. When the artifact is text, diffing text is the only review that scales.\n8. Five rules I would keep if I started over Research before main.go. Six doc commits before one line of Go paid for itself all month. Write down what the peer tools actually do, with a verdict per feature — not \u0026ldquo;they\u0026rsquo;re heavy\u0026rdquo; but \u0026ldquo;they do A, I will do B, because C\u0026rdquo;. One document that must move with the code. Same commit, not same PR. It converts a sprawling history into a readable diary and it is the only reason I can still explain a two-week-old decision. Encode the budget as a failing test. \u0026ldquo;Under 1,000 tokens\u0026rdquo; is a wish. t.Fatalf when it goes over is a constraint. Same for the memory ceiling, same for the theme\u0026rsquo;s mandatory tokens. Bounded everything, and prove it under load. The memory bug that mattered was not in the agent loop; it was in the session loader, at 50k entries. If a queue, a cache, a window or a sink can grow with its input, it will. Fix the shared function once. The gateway\u0026rsquo;s four post-ship commits were all at seams — the poll loop, the log line, the formatter. That is only possible because there was one of each. 9. What is still not good Saying this is more useful than the numbers:\nThe worst case is not measured. I have not driven a genuine 200k-token session to its floor and watched RSS. The \u0026lt;100 MB is design plus a lot of real sessions, not a proof of the deepest case. The PRD is 3,508 lines and has duplicated sections — ## 5. Key decisions and ## 6. Links each appear twice. The first time the count went to two was 2026-10-02 (42b235f, a sticky-prompt commit that added 997 lines to the PRD). I know. It is on the list, and there is a diff sitting there that says so. 194 open issues, most of them P2 breadth. Breadth is exactly what I said I would not chase on day 1, and the \u0026ldquo;all of them\u0026rdquo; directive on day 3 re-scoped the skip list. That tension is in the PRD by name, not hidden. No CI gate on pull requests. You run gofmt -l ., go vet ./..., go test -race ./... and the six-platform cross matrix yourself. That is a deliberate choice for a single-maintainer repo and it will not survive a second regular contributor. Memory recall is not policy. Recall ranks against the last user turn only, caches for 60 seconds and is capped at ~4 KB — so a \u0026ldquo;never do X\u0026rdquo; rule that lives only in memory gets truncated or discounted. Standing conventions belong in AGENTS.md or RULES.md. I measured this and wrote the decision down, which is how I found out. 10. Try it curl -fsSL https://raw.githubusercontent.com/FreePeak/xdev/main/scripts/install.sh | sh xdev setup # ~/.xdev/agent/, a starter config, and next steps xdev # the TUI Then xdev connect to reach ~200 hosts with one command, xdev login claude if you have a subscription, or point models.yml at any OpenAI-compatible gateway you already run.\nRepo: github.com/FreePeak/xdev — Apache-2.0, 743 commits, one author, 33 days.\nClosing The first post ended with a question about what a coding tool needs in memory. This one ends with a different observation: five of the last six commits of the 743 are about a Telegram gateway I did not plan on day 1, and the absolute last one is about compaction — the quiet thing that keeps a long session alive.\nThat is what a full arc looks like, I think. Four days to build the thing you imagined, and then weeks of finding out it wants to be something slightly different — and keeping the seams clean enough that it can.\n","permalink":"https://freepeak.github.io/posts/xdev-from-day-one-to-where-it-is-now/","summary":"I wrote the first xdev commit on 2026-09-09 and the 743rd on 2026-10-11. This is everything between: the research-first day, the four-day sprint that closed M0-M15, the weeks where every user report became a PRD stamp, the Telegram gateway, and the numbers I can measure today - 21.8 MB binary, 12 ms startup, 128k lines of Go, 7 direct dependencies.","title":"xdev from day one: 743 commits, 33 days, and what actually shipped"},{"content":" Before you read: every number below I measured on my personal machine (macOS, Apple Silicon) on 2026-09-16, with the command written out. Yours will differ. I am not comparing agent quality — only weight.\n1) It starts with a stupid question That afternoon I had 3 terminal tabs open: one running omp, one Claude Code, one opencode. All three were \u0026ldquo;thinking\u0026rdquo;.\nI opened htop.\nI sat there looking at the colour bars and asked myself a very stupid question: what does a tool for writing code actually need to keep in memory?\nNot a terminal app. Not an editor. A loop: send text to a model, get text back, call a tool, repeat.\nI answered it, and once I had answered it I couldn\u0026rsquo;t unsee it. So I wrote xdev. 7 days, 403 commits.\nThis is what happened in those 7 days: what I read, what I decided, where I was wrong, and what I measured.\nHere is the thing itself, mid-session — the real UI, captured from the running program, not a mockup:\n2) Measure first I am not writing this to say the other tools are bad. omp and Claude Code are two things I use daily, and without them I would never have thought about writing my own.\nTheir problem, for me, is not features. It is the price of features.\nHere is what I measured on my machine:\nTool Binary --version (avg of 5) Data dir xdev 20 MB 0.040 s 144 MB omp 186 MB 0.091 s 5.7 GB Claude Code 210 MB 0.060 s 1.1 GB opencode 144 MB 0.358 s 155 MB Binary size: ls -lL ~/.local/bin/xdev. Timing: a 5-iteration loop calling --version, summed with time.time(). I don\u0026rsquo;t have hyperfine, so my numbers are crude — but crude enough to see the gap.\nThe binary column is the interesting one. omp, Claude Code and opencode are all real Mach-O binaries, not scripts. But run strings on all three and you meet the same relative: bun-v1.4.2, bun-v1.4.3, bun-v1.3.14. What you download is not their program — it is their program plus an entire JS runtime packed inside.\n20MB versus ~200MB for the same job. This is not a \u0026ldquo;who codes better\u0026rdquo; contest; Go versus TS is not the reason. The reason is whether you need that runtime inside your binary at all, when most of its work is waiting on the network.\n3) Go to school first: pi and omp I did not invent the design. I sat down and read, and after reading I realised I no longer needed to be clever.\npi (github.com/earendil-works/pi) is a TypeScript coding agent harness by Mario Zechner, and it is what changed my mind. Its philosophy fits in a few lines I had to sit still after reading:\nA system prompt under 1000 tokens, including tool descriptions. Reason: frontier models are RL-trained to understand coding agents. 10,000 tokens of instructions is dead weight you carry. Exactly 4 core tools: read, write, edit, bash. Four is enough. YOLO by default. No permission theater. Because the read data + execute code + reach the network trifecta cannot be contained by prompting or pattern rules. If you want containment, contain it with a container or a micro-VM, not with a message. No built-in todo tool. Models get confused by state that a tool manages. Want a task list? Write TODO.md. Hand-roll the LLM layer instead of using an SDK. Because the world only has 4 wire APIs that matter: OpenAI Completions, OpenAI Responses, Anthropic Messages, Google GenAI. omp (Oh My Pi) is a fork of pi, and it is what I actually use every day. omp adds a lot of genuinely useful things: a Rust N-API layer for grep/glob/AST/PTY, a shared filesystem-scan cache, an in-process extension system, MCP, a subagent hub, compaction with 6 trigger paths, and a very disciplined session tree.\nThe price: a JS runtime as the floor, extensions loaded in the same process, event queues with no bound, and the whole session living in memory. On my machine ~/.omp has swollen to 5.7 GB.\nAnd here is the single most important decision of the 7 days, in one line:\nPort the data model. Don\u0026rsquo;t port the code.\nThe JSONL session tree (append-only + one leaf pointer), the unified stream contract, how context is rebuilt, compaction, the OutputSink, the frame-plan TUI — all of it is design that my own field usage had already stomped on for months. It is correct. My job was to copy the semantics (keeping entry names identical so existing tooling still reads them), not to copy the implementation of the JS era.\nThe research I wrote during those 7 days lives in docs/research/: 19 files dissecting omp, pi, Claude Code, opencode, hermes, fx — with a per-feature verdict table: what to port, what to drop, what to port differently and why. docs/parity-delta.md records every place I deliberately differ from omp, including the omp flags I accept for compatibility but that mean nothing (like --no-pty, because my bash tool is pipe-based).\n4) Architecture: what I chose, and what I deliberately did not build Module path github.com/FreePeak/xdev, Go 1.25, CGO-free, one static binary, exactly 5 direct dependencies (tcell, go-runewidth, the MCP Go SDK, yaml.v3, modernc.org/sqlite).\nThe session is an append-only JSONL tree with a leaf pointer No entry is ever mutated or deleted. Branching is just moving a pointer. Context is rebuilt by walking parent links. The format is inspectable — you can jq my session files, and I never had to ship a viewer.\nThis is a semantic port from pi/omp, and it is the part of the design I sleep best about. Because it allows something a \u0026ldquo;state lives in memory\u0026rdquo; harness cannot: look back and argue with the past.\nEverything is bounded Queues are bounded. Buffers are bounded. The session window is bounded. The output sink is bounded.\nConcretely: debug.SetMemoryLimit(100MB) (overridable with XDEV_MEMLIMIT), and when the live heap reaches 85% of that, the agent forces compaction instead of waiting for the token threshold.\nThe practical meaning: a runaway turn degrades into \u0026ldquo;compact now\u0026rdquo;, not into an OOM kill. Backpressure is a feature, not a bug.\nI measured ~16MB RSS at boot. 30–70MB estimated for a normal working session, worst case pinned under 100MB. This is the one number where I admit I have not measured enough: I have not run a genuine 200k-token session to the bottom. The 100MB figure is design plus real tests, not a measurement of the worst case.\n4 core tools, and the rest is optional Prompt + 4 tools sits under 1000 tokens, and here I don\u0026rsquo;t trust my own word — I test the limit:\n// cmd/xdev/prompt_test.go const maxPromptTokens = 1000 if tokens := len([]rune(got)) / 4; tokens \u0026gt;= maxPromptTokens { t.Fatalf(\u0026#34;system prompt is ~%d tokens (budget %d): %d chars across %d tools\u0026#34;+ \u0026#34; — trim a description or make a deliberate PRD change\u0026#34;, ...) } Estimating tokens as runes / 4 is crude. I know. But it is a gate, not a ruler — its purpose is to force me to amend the PRD consciously every time I want the prompt to grow, instead of letting it grow quietly.\ngrep, glob, lsp, eval, browser, debug, computer, tts, task\u0026hellip; all exist, but they are the layer added after the core 4 already stood. Totals: 544 Go files, ~90k lines excluding tests, ~63k lines of tests — tests are 41% of the codebase.\nThings I deliberately did not do Saying what you won\u0026rsquo;t do matters more than saying what you did:\nNo permission theater. No popup asking \u0026ldquo;xdev wants to run ls?\u0026rdquo;. YOLO by default, and the harness only documents sandbox patterns (docs/reference/container-and-sandbox.md) instead of building a paper wall. No PTY in the bash tool. Pipe-based. omp\u0026rsquo;s --no-pty flag is accepted for script compatibility and is\u0026hellip; a deliberate no-op, recorded in the parity delta. No in-process extension loading. Extensions run as subprocesses talking a JSONL handshake. An extension crashing must not be allowed to kill the agent. What I lose: custom in-process renderers — an extension only declares a spec (card | table | tree), the host renders it, and a malformed spec degrades to plain text. No claiming to be a clone. omp v18.1.17 ships 131 omp:// doc pages; I diffed omp --help against xdev -h mechanically and wrote down every gap, with reasons. 5) The TUI: where I spent the most time This is the part I most want to talk about, because it is where I was wrong most often.\ntcell, not bubbletea bubbletea is pretty, easy, well documented. But its Elm model allocates a lot, and in a TUI receiving thousands of streamed tokens per second, allocation churn is exactly what stutter is.\ntcell is rawer, faster, lower-allocation — and omp\u0026rsquo;s frame-plan model ports cleanly onto it. The trade: you manage everything yourself. I managed everything myself.\nFrame plan: three states of a block Every frame is: mandatory chrome (editor/status/HUD/overlay) + TerminalFramePlan { history?: {id, rows}, viewport: rows[] }.\nAnd transcript blocks have three states:\nState Meaning On resize active still mutable, lives in the viewport redraw settled finalised, can reflow reflow, until capacity pressure committed pushed into the terminal\u0026rsquo;s real scrollback no longer my business The insight I arrived at the expensive way: draw the viewport, not the session. The commit perf(tui): draw the viewport, not the session; trim aged tool output first is the commit I wish I had written on day one.\nResize is the most hateful thing about terminals Alt-screen (?1049h) is only for overlays; for the transcript, any line scrolled off screen goes into the terminal\u0026rsquo;s real scrollback. The reason is simple: once I can no longer see it, there is no reason for it to occupy my RAM.\nThe consequence: when the terminal resizes, my cursor is no longer where I think it is. I have to ask the terminal which row I\u0026rsquo;m on via a DSR anchor and rebuild. DSR-anchor resize recovery was in the PRD from the start, and it is among the most painful lines of text I have ever written.\nThe four TUI bugs I remember Bug 1 — a real freeze. fix(tui): the tree selector could own the keyboard while painting nothing — a \u0026quot;frozen TUI\u0026quot; with a live process. A selector that rendered nothing while still holding the keyboard. The user sees a hang; the process is alive. The nasty part: every signal I had for \u0026ldquo;is the process alive\u0026rdquo; said fine.\nBug 2 — self-deadlock. fix(tui): picker draw must not re-lock a.mu — self-deadlock hung every draw with content. Drawing re-locked an already-held lock. Hung every frame that had content.\nBug 3 — a modal that cannot paint must close. fix(tui): a modal that cannot paint must close, and cancel hits the turn. I used to leave the modal open when it couldn\u0026rsquo;t paint. Correct behaviour: if you can\u0026rsquo;t draw it, it disappears. A modal you cannot see but which still eats keystrokes is a trap.\nBug 4 — the scroll hint painted over content. fix(tui): the scroll hint was painted over the first transcript row. Small. But you see it the first time you open the tool.\nAfter the first three, I added a watchdog: any UI-loop iteration over 5 seconds dumps goroutine stacks to ~/.xdev/agent/dumps/ (dump capped at 1MB).\nReason: the three hangs I just described, without a dump I could only guess. And guessing about a hung TUI is a waste of time. I happily pay one goroutine to never guess again.\nThe mouse: the thing I redid and redo There is no spec for mouse behaviour in a TUI. There is only a feeling of \u0026ldquo;wrong\u0026rdquo;. And I had to answer that feeling 5 times in 4 hours:\n23:19 fix(tui): Shift hands the drag to the terminal, mid-stroke too 01:07 feat(tui): mouse selection covers the whole screen and stays visible 01:51 feat(tui): one mouse drag can select past the screen 02:19 feat(tui): the modal picker answers the mouse, like omp\u0026#39;s SelectList 02:40 feat(tui): click-to-focus on the live-agent roster The first one stung: the user presses Shift to copy using the terminal\u0026rsquo;s own selection, and I have to release the pointer mid-drag — even when they\u0026rsquo;re halfway through selecting. No design document teaches you that detail. Only using it does.\nThe last one is funny: I had built an agent roster panel controllable only by keyboard. In a terminal, in 2026.\nThe start screen, and the context dock The start screen is the one place the brand gets to have an opinion — a pixel wordmark, its binary tagline, and the four real actions with the keys that actually run them. Everything else stays monochrome so the transcript is the only thing competing for attention:\nThe context dock is the answer to \u0026ldquo;where am I in this session?\u0026rdquo;. It is a fixed 42-column panel that only opens when the terminal is wide enough to still leave the transcript its own floor, and its sections are whatever the session actually has in play — the plan proposal, the task list, the files the diffs touched, the live agents — each one foldable, and it refuses to rebuild a frame when nothing changed. Here plan mode is armed, so the dock is already carrying its section:\nTheme: 66 colour tokens I ported the Grok CLI theme system (GrokNight/GrokDay, switching automatically via OSC 11), and made all 66 colour tokens mandatory — spelled exactly as omp spells them, so an omp theme file paints xdev\u0026rsquo;s chrome unchanged. One token missing is a failing test, not a \u0026ldquo;later\u0026rdquo;.\nI spend all day inside this TUI. Letting it wear the terminal\u0026rsquo;s default colours was not survivable. This is a cost I paid voluntarily, and I think it was worth it.\n6) Testing a thing that only exists as text Sounds silly. But my TUI renders text, and text is not something anyone can diff by eye with discipline.\nSo most of my TUI tests are character-line tests: build a frame on a simulation terminal, then compare the text.\nKeymap (10 tests): every action must resolve, ChordOf must go the right way, a corrupt user keymap file must fall back to defaults rather than crash, /hotkeys must show every action, and a remap must actually change behaviour. Chrome (15 tests): the status row must say where you are, not list its own keys; the HUD meter must show used/total of the live context; the session clock must re-base when the session changes, and when there is no anchor it must hide itself rather than display 00:00. Composer: Up/Down in a multi-line draft must walk visual rows, not logical lines — obvious until your editor meets a wide character. No hangs: draw_hang_test.go has 5 tests that build frames on a simulation terminal under a timeout — a selector must not own the keyboard while painting nothing, a modal that cannot paint must close. tree_test.go pins the self-deadlock too: draw must not re-lock the app mutex. The three bugs above, now locked in a cage made of code. 63k lines of tests, many of which only say \u0026ldquo;this line must be identical to yesterday\u0026rdquo;. They cost maintenance. But every time I refactored the renderer, they were the only thing that let me refactor without fear.\n7) The thing models were born to surprise me with One change during the build I\u0026rsquo;m particularly fond of: an unparseable tool call is an error result, not a dead run.\nBefore, if the model returned a tool call whose arguments wouldn\u0026rsquo;t parse, the turn died. That simple, and that stupid.\nAfter: it becomes a tool error that flies back up to the model, and the model fixes it. One error-handling line turned into a self-healing loop. I didn\u0026rsquo;t make the model smarter; I just stopped cutting it off mid-sentence.\nSame idea in edit arg-repair + freshness guard: repair arguments when they\u0026rsquo;re repairable (models are often off-by-one on line numbers), but if the file changed since the agent read it, stop and say so — don\u0026rsquo;t overwrite.\nThose two are my whole philosophy about harnesses: patch what a machine can patch, stop where patching means losing data.\n8) Three things I learned One: \u0026ldquo;lightweight\u0026rdquo; is not cutting features. It is cutting the runtime.\nxdev ended up with a subagent hub, MCP client, LSP, memory backend, E2E collab, web search, browser over CDP, a DAP debugger. 35 packages under internal/. I did not cut features. I cut the runtime that came bundled, and pushed the heavy things into child processes.\nTwo: discipline does not live in intention. It lives in tests.\nEvery number I bragged about at the top has something holding it in place. 1000 tokens? prompt_test.go fails if I write longer. 66 colour tokens? m12_test.go. Help must name every subcommand and must not leak @both? usage_test.go — born after 13 subcommands vanished from the help at the same time.\nDiscipline without a test stops being discipline after 3 days and 100 commits. It becomes a memory.\nThree: don\u0026rsquo;t port code. Port the why.\nWhere I ported both the implementation and the reason, I was fast. Where I ported an implementation without its reason, I spent half a day discovering I had carried along somebody else\u0026rsquo;s decision.\ndocs/parity-delta.md is therefore worth as much as the code. It is where I wrote: omp does A, I do B, and why B.\n9) Try it if you want curl -fsSL https://raw.githubusercontent.com/FreePeak/xdev/main/scripts/install.sh | sh Then xdev setup to create ~/.xdev/agent/, point models.yml at a gateway you already have, and:\nxdev tui # interactive mode xdev \u0026#34;find every place in this repo that logs a password\u0026#34; xdev --resume 01a0 # resume by session-id prefix xdev bench --turns 5 --model @smol measures TTFT and decode p50/p95 for your provider — if you\u0026rsquo;re curious about my numbers.\nRepo: github.com/FreePeak/xdev (Apache-2.0).\n10) Closing The question \u0026ldquo;what does a tool for writing code need in memory\u0026rdquo; now has an answer for me: about 16MB, 4 tools, and a session format jq can read.\nThe rest of the 7 days was learning how not to do more than that.\nAnd the other 200MB in the tools you use? Not their fault. They chose breadth over weight, and for a lot of people that choice is right.\nIt\u0026rsquo;s just that this morning I opened 3 terminal tabs that were \u0026ldquo;thinking\u0026rdquo;, looked at htop, and for the first time didn\u0026rsquo;t feel like I was burning RAM to wait for a model to answer.\n","permalink":"https://freepeak.github.io/posts/xdev-how-i-built-a-lightweight-and-fast-agent-coding-cli/","summary":"I use omp, opencode and Claude Code every day, and I got more and more annoyed by how heavy they are. This is the story of the 7 days I spent writing xdev: reading the pi/omp architecture, porting the data model to Go, fighting the terminal UI, and the numbers I measured on my own machine — 20MB binary, ~16MB RAM, 40ms startup.","title":"xdev: how I built a lightweight and fast agent coding CLI"},{"content":" Nói trước cho rõ: mọi con số trong bài tôi đo trên máy cá nhân (macOS, Apple Silicon) vào ngày 16/09/2026, kèm đúng câu lệnh tôi dùng. Máy bạn sẽ ra số khác. Và tôi không so chất lượng agent — tôi chỉ cân độ nặng.\n1) It starts with a stupid question Chiều hôm đó tôi mở ba tab terminal: một tab chạy omp, một tab Claude Code, một tab opencode. Cả ba đang \u0026ldquo;nghĩ\u0026rdquo;.\nTôi mở htop lên.\nNgồi nhìn mấy cái thanh màu nhảy lên nhảy xuống, tôi tự hỏi một câu rất ngớ ngẩn: một công cụ để viết code thì thật ra cần giữ gì trong bộ nhớ?\nKhông phải terminal app. Cũng không phải trình soạn thảo. Chỉ là một vòng lặp: gửi text lên model, nhận text về, gọi tool, lặp lại.\nTôi tự trả lời được câu đó. Và trả lời xong thì không quên đi được nữa.\nThế là tôi viết xdev. 7 ngày, 403 commit.\nBài này kể lại đúng 7 ngày đó: tôi đọc gì, quyết định gì, sai ở đâu, và đo được gì.\nĐây là nó, đang chạy giữa một session — UI thật, chụp từ chương trình đang chạy, không phải mockup:\n2) Measure first, then talk Tôi không viết bài này để bảo mấy tool kia tệ. omp và Claude Code là hai thứ tôi dùng hằng ngày; nếu không có chúng thì tôi đã không bao giờ nghĩ tới chuyện tự viết một cái.\nVấn đề của chúng với tôi không nằm ở tính năng. Nằm ở cái giá phải trả cho tính năng.\nSố đo trên máy tôi:\nTool Binary --version (TB 5 lần) Data dir xdev 20 MB 0.040 s 144 MB omp 186 MB 0.091 s 5.7 GB Claude Code 210 MB 0.060 s 1.1 GB opencode 144 MB 0.358 s 155 MB Cột binary đo bằng ls -lL ~/.local/bin/xdev. Cột thời gian thì gọi --version 5 lần rồi cộng lại bằng time.time(). Tôi không có hyperfine, nên số thô — nhưng thô cũng đủ để thấy khoảng cách.\nCột \u0026ldquo;binary\u0026rdquo; mới là chỗ đáng nói. omp, Claude Code và opencode đều là binary Mach-O thật, không phải script. Nhưng strings cả ba thì gặp chung một họ hàng: bun-v1.4.2, bun-v1.4.3, bun-v1.3.14. Thứ bạn tải về không phải chương trình của họ — nó là chương trình của họ cộng thêm cả một JS runtime bị nhét vào trong.\n20MB với ~200MB, cho cùng một việc. Đây không phải cuộc thi \u0026ldquo;ai code giỏi hơn ai\u0026rdquo;; Go hay TS không phải lý do. Lý do là bạn có thật sự cần cái runtime đó nằm trong binary của mình không, khi phần lớn thời gian nó chỉ ngồi chờ network.\n3) Go to school first: pi and omp Tôi không tự nghĩ ra thiết kế. Tôi ngồi đọc. Và đọc xong thì thấy mình không cần phải thông minh nữa.\npi (github.com/earendil-works/pi) là một coding agent harness bằng TypeScript của Mario Zechner, và chính nó làm tôi đổi cách nghĩ. Triết lý của nó gói trong vài dòng mà đọc xong tôi phải ngồi im một lúc:\nSystem prompt dưới 1000 token, tính luôn cả mô tả tool. Lý do: model frontier đã được RL-train để hiểu coding agent. Nhét 10.000 token hướng dẫn vào chỉ là mang nặng thêm. Đúng 4 tool lõi: read, write, edit, bash. Bốn cái đó là đủ. YOLO mặc định. Không có sân khấu xin quyền. Vì combo đọc dữ liệu + chạy code + gọi mạng thì không cách nào nhốt bằng prompt hay bằng rule pattern được. Muốn nhốt thì nhốt bằng container hoặc micro-VM, đừng nhốt bằng lời nhắn. Không có todo tool built-in. Model bị confuse bởi state mà tool quản lý. Muốn list việc thì viết ra TODO.md. Tự tay viết tầng LLM, không dùng SDK. Vì thế giới chỉ có 4 wire API đáng kể: OpenAI Completions, OpenAI Responses, Anthropic Messages, Google GenAI. omp (Oh My Pi) là fork của pi, và nó là thứ tôi dùng hằng ngày. omp thêm đủ thứ hữu ích: tầng Rust N-API cho grep/glob/AST/PTY, filesystem-scan cache dùng chung, extension system trong process, MCP, subagent hub, compaction với 6 đường trigger, session tree rất kỷ luật.\nCái giá: runtime JS làm nền, extension nạp trong cùng process, event queue không chặn biên, và cả session nằm trong memory. Trên máy tôi ~/.omp phình ra 5.7 GB.\nVà đây là quyết định quan trọng nhất của cả 7 ngày, tóm trong một câu:\nPort data model. Đừng port code.\nJSONL session tree (append-only + một con trỏ lá), unified stream contract, cách dựng lại context, compaction, OutputSink, frame-plan TUI — tất cả đều là thiết kế mà hiện trường công việc của tôi đã dẫm lên vài tháng rồi. Chúng đúng. Việc của tôi là chép ngữ nghĩa (tên entry giữ nguyên để tooling cũ còn đọc được), không phải chép implementation của thời JS.\nNghiên cứu tôi viết ra trong 7 ngày nằm ở docs/research/: 19 file phân tích omp, pi, Claude Code, opencode, hermes, fx — kèm bảng verdict từng tính năng: cái nào port, cái nào bỏ, cái nào port khác đi và tại sao. docs/parity-delta.md ghi lại mọi chỗ tôi cố tình khác omp, và cả những flag omp tôi nhận cho tương thích nhưng không có nghĩa (như --no-pty, vì bash của tôi là pipe-based).\n4) Architecture: what I chose, and what I deliberately did not do Module path github.com/FreePeak/xdev, Go 1.25, CGO-free, một binary tĩnh, đúng 5 direct dependency (tcell, go-runewidth, MCP Go SDK, yaml.v3, modernc.org/sqlite).\nThe session is an append-only JSONL tree with a leaf pointer Không có entry nào bị sửa hay xoá. Branch chỉ là dời một con trỏ. Context dựng lại bằng cách đi theo parent link. Format inspectable — bạn jq vào file session của tôi được, và tôi không cần viết riêng cái viewer nào.\nĐây là port ngữ nghĩa từ pi/omp, và nó là thứ khiến tôi yên tâm nhất trong thiết kế. Vì nó cho phép một thứ mà harness dạng \u0026ldquo;state trong memory\u0026rdquo; không cho: nhìn lại và cãi nhau với quá khứ.\nEverything is bounded Queue có biên. Buffer có biên. Session window có biên. Output sink có biên.\nCụ thể: debug.SetMemoryLimit(100MB) (override bằng XDEV_MEMLIMIT), và khi live heap chạm 85% ngưỡng đó thì agent buộc compaction, không đợi tới ngưỡng token.\nÝ nghĩa thực dụng: một turn mất kiểm soát sẽ degrade thành \u0026ldquo;compact ngay\u0026rdquo;, chứ không thành OOM kill. Backpressure là feature, không phải bug.\nTôi đo lúc boot: ~16MB RSS. 30–70MB ước tính cho phiên làm việc bình thường, worst case ép dưới 100MB. Đây là con số duy nhất tôi phải thừa nhận là mình chưa đo đủ: tôi chưa chạy một phiên 200k token thật sự tới đáy. Con số 100MB là thiết kế cộng với các test có thật, không phải measurement của tình huống tệ nhất.\n4 core tools, and the rest is optional Prompt + 4 tool nằm dưới 1000 token, và chỗ này tôi không tin lời mình nói — tôi test luôn cái giới hạn đó:\n// cmd/xdev/prompt_test.go const maxPromptTokens = 1000 if tokens := len([]rune(got)) / 4; tokens \u0026gt;= maxPromptTokens { t.Fatalf(\u0026#34;system prompt is ~%d tokens (budget %d): %d chars across %d tools\u0026#34;+ \u0026#34; — trim a description or make a deliberate PRD change\u0026#34;, ...) } Ước token bằng runes / 4 là thô. Tôi biết. Nhưng nó là một cái cổng, không phải cái thước — mục đích của nó là bắt tôi phải sửa PRD một cách có ý thức mỗi khi muốn prompt phình ra, thay vì để nó phình lên trong im lặng.\ngrep, glob, lsp, eval, browser, debug, computer, tts, task\u0026hellip; đều có, nhưng chúng là lớp thêm vào sau khi 4 cái lõi đã đứng vững. Tổng: 544 file Go, ~90k dòng không tính test, ~63k dòng test — tức test chiếm 41% codebase.\nThe things I deliberately did not do Nói rõ cái mình không làm quan trọng hơn nói cái mình làm:\nKhông permission theater. Không có popup \u0026ldquo;xdev muốn chạy ls?\u0026rdquo;. YOLO mặc định, và harness chỉ tài liệu hoá pattern sandbox (docs/reference/container-and-sandbox.md), không dựng tường giấy. Không PTY trong tool bash. Pipe-based. Cờ --no-pty của omp tôi nhận cho tương thích script, và nó\u0026hellip; no-op có chủ đích, ghi rõ trong parity-delta. Không nạp extension trong process. Extension chạy subprocess nói chuyện với nhau bằng JSONL handshake. Một extension crash không được phép làm chết agent. Cái mất: renderer custom trong process — extension chỉ khai báo spec (card | table | tree), host render, và spec sai thì degrade về plain text. Không tự nhận là bản sao. omp v18.1.17 có 131 trang docs omp://; tôi diff cơ học omp --help với xdev -h rồi ghi lại từng thứ còn thiếu, kèm lý do. 5) The TUI: where I spent the most time Đây là phần tôi muốn kể nhất, vì nó cũng là chỗ tôi sai nhiều nhất.\ntcell, not bubbletea bubbletea đẹp, dễ dùng, tài liệu tốt. Nhưng mô hình Elm của nó allocate nhiều, mà trong một cái TUI mỗi giây nhận cả nghìn token stream thì allocation churn chính là giật.\ntcell thô hơn, nhanh hơn, ít cấp phát hơn — và frame-plan model của omp port lên nó rất sạch. Đổi lại, bạn phải tự quản mọi thứ. Tôi tự quản mọi thứ.\nFrame plan: three states of a block Mỗi frame là: chrome bắt buộc (editor/status/HUD/overlay) + TerminalFramePlan { history?: {id, rows}, viewport: rows[] }.\nVà transcript block có ba trạng thái:\nTrạng thái Nghĩa Xử lý khi resize active còn thay đổi được, nằm trong viewport vẽ lại settled đã chốt, reflow được reflow, cho tới khi áp lực dung lượng committed đã đẩy vào terminal scrollback thật không còn là việc của mình Cái insight này tôi học được bằng cách trả giá: bạn phải vẽ viewport, đừng vẽ session. Commit perf(tui): draw the viewport, not the session; trim aged tool output first là commit tôi ước gì mình viết được ngay từ ngày đầu.\nResize is the most hateful thing about terminals Alt-screen (?1049h) chỉ dành cho overlay; còn transcript thì dòng nào cuộn mất khỏi màn hình, tôi đẩy vào scrollback thật của terminal. Lý do rất đơn giản: khi tôi không còn nhìn thấy nó, không có lý do gì để nó chiếm RAM của tôi.\nHậu quả: khi terminal resize, con trỏ của tôi không còn ở chỗ tôi nghĩ nữa. Phải hỏi lại terminal xem mình đang ở dòng nào bằng DSR anchor rồi dựng lại. DSR-anchor resize recovery nằm trong PRD từ đầu, và nó là một trong những dòng code đau đớn nhất tôi từng viết.\nThe four TUI bugs I will never forget Bug 1 — đóng băng thật sự. fix(tui): the tree selector could own the keyboard while painting nothing — a \u0026quot;frozen TUI\u0026quot; with a live process. Một selector vẽ ra không gì cả nhưng vẫn giữ bàn phím. Người dùng thấy treo, process vẫn sống. Ghê ở chỗ: mọi tín hiệu \u0026ldquo;process còn sống\u0026rdquo; của tôi đều báo ổn.\nBug 2 — self-deadlock. fix(tui): picker draw must not re-lock a.mu — self-deadlock hung every draw with content. Vẽ lại khoá lại cái lock đã có. Treo mọi frame có nội dung.\nBug 3 — modal không vẽ được thì phải đóng. fix(tui): a modal that cannot paint must close, and cancel hits the turn. Tôi từng để modal nằm lại khi nó không paint được. Đúng ra: không vẽ được thì biến mất. Một cái modal bạn không thấy mà vẫn ăn phím là một cái bẫy.\nBug 4 — scroll hint đè lên nội dung. fix(tui): the scroll hint was painted over the first transcript row. Nhỏ. Nhưng bạn thấy nó ngay lần đầu mở tool.\nSau ba bug đầu, tôi thêm watchdog: UI loop iteration nào quá 5 giây thì dump goroutine stack ra ~/.xdev/agent/dumps/ (giới hạn dump 1MB).\nLý do: ba vụ treo tôi vừa kể, nếu không có dump thì tôi chỉ còn cách đoán. Mà đoán một cái TUI treo là lãng phí thời gian. Tôi chấp nhận tốn một goroutine để đổi lấy việc không phải đoán nữa.\nThe mouse: the thing I did and redid Chuột trong TUI không có spec. Nó chỉ có cảm giác \u0026ldquo;sai\u0026rdquo;. Và cái cảm giác \u0026ldquo;sai\u0026rdquo; đó tôi phải trả lời 5 lần trong 4 tiếng:\n23:19 fix(tui): Shift hands the drag to the terminal, mid-stroke too 01:07 feat(tui): mouse selection covers the whole screen and stays visible 01:51 feat(tui): one mouse drag can select past the screen 02:19 feat(tui): the modal picker answers the mouse, like omp\u0026#39;s SelectList 02:40 feat(tui): click-to-focus on the live-agent roster Cái đầu tiên đau thật: người dùng nhấn Shift để copy bằng selection của chính terminal, và tôi phải nhả con trỏ ngay giữa thao tác kéo — kể cả khi họ đã kéo được nửa đường. Không có design doc nào dạy bạn chi tiết đó. Chỉ có dùng thật mới biết.\nCái cuối cùng thì buồn cười: tôi đã dựng một bảng roster agent chỉ điều khiển được bằng phím. Trong terminal, năm 2026.\nThe start screen, and the context dock Màn hình bắt đầu là chỗ duy nhất thương hiệu được phép có ý kiến — một wordmark pixel, dòng tagline nhị phân của nó, và bốn hành động thật kèm đúng phím chạy được chúng. Mọi thứ còn lại giữ đơn sắc để transcript là thứ duy nhất tranh sự chú ý:\nContext dock là câu trả lời cho \u0026ldquo;tôi đang ở đâu trong session này?\u0026rdquo;. Nó là một panel rộng cố định 42 cột, chỉ mở khi terminal đủ rộng để vẫn chừa cho transcript khoảng sống riêng của nó, và các mục của nó là đúng những gì session đang có: kế hoạch đang chờ, danh sách task, những file mà diff đã đụng tới, các agent đang chạy — mỗi mục gấp lại được, và nó từ chối dựng lại frame khi không có gì đổi. Ở đây plan mode đang bật, nên dock đã có sẵn mục của nó:\nTheme: 66 colour tokens Tôi port hệ theme của Grok CLI (GrokNight/GrokDay, tự đổi theo OSC 11), và bắt buộc phải đủ cả 66 color token — viết y hệt cách omp viết, để một file theme của omp sơn được lên chrome của xdev mà không phải sửa gì. Thiếu một token là test fail, không có chuyện \u0026ldquo;để sau\u0026rdquo;.\nCả ngày làm việc trong một cái TUI mà để nó dùng màu mặc định của terminal thì tôi chịu không nổi. Đây là chi phí tôi tự nguyện trả, và tôi nghĩ nó đáng.\n6) Testing a thing that only exists as text Nghe buồn cười thật. Nhưng TUI của tôi render chữ, mà chữ thì không ai diff bằng mắt một cách kỷ luật được.\nNên phần lớn test TUI của tôi là test dòng ký tự: dựng frame trên một simulterminal, rồi so text.\nKeymap (10 test): mọi action phải resolve được, ChordOf đúng chiều, file keymap user hỏng thì về default chứ không crash, /hotkeys phải hiển thị mọi action, và remap thật sự đổi hành vi. Chrome (15 test): status row phải nói bạn đang ở đâu chứ không phải liệt kê phím; HUD meter phải hiện used/total của context sống; đồng hồ phiên phải re-base khi đổi session, và khi không có neo thì ẩn đi chứ đừng hiển thị 00:00. Composer: Up/Down trong bản nháp nhiều dòng phải đi theo dòng hiển thị, không phải dòng logic — nghe hiển nhiên, cho tới khi editor của bạn đụng wide character. Không treo: draw_hang_test.go có 5 test dựng frame trên simulterminal rồi timeout — selector không được giữ bàn phím khi vẽ ra chỗ trống, modal không paint được thì phải đóng. tree_test.go ghim chặt luôn vụ self-deadlock: draw không được re-lock mutex của app. Ba cái bug ở mục trên, giờ bị nhốt lại bằng code. 63k dòng test, nhiều dòng chỉ để nói \u0026ldquo;dòng này phải giống hệt hôm qua\u0026rdquo;. Giữ chúng thì tốn công. Nhưng mỗi lần tôi refactor renderer, chúng là thứ duy nhất cho tôi dám refactor mà không sợ.\n7) The thing the model produced that surprised me Trong quá trình build có một thay đổi tôi rất thích: an unparseable tool call is an error result, not a dead run.\nTrước đó, nếu model trả về tool call với arguments parse không được, turn chết. Đơn giản vậy thôi, và ngu vậy thôi.\nSau: nó là một tool error bay ngược lên model, và model tự sửa. Một dòng xử lý lỗi đổi thành một vòng tự chữa. Tôi không dạy model thông minh hơn; tôi chỉ ngừng cắt lời nó giữa câu.\nTương tự với edit arg-repair + freshness guard. Args sửa được thì sửa (model hay off-by-one ở số dòng), còn file đã đổi kể từ lúc agent đọc thì dừng lại và báo, đừng ghi đè.\nCặp này là toàn bộ triết lý của tôi về harness: chỗ nào máy tự vá được thì vá, chỗ nào vá là mất dữ liệu thì dừng.\n8) Three things I learned Một: \u0026ldquo;nhẹ\u0026rdquo; không phải là cắt tính năng. Là cắt runtime.\nxdev cuối cùng cũng có subagent hub, MCP client, LSP, memory backend, collab E2E, web search, browser qua CDP, DAP debugger. 35 package trong internal/. Tính năng tôi không cắt. Tôi bỏ cái runtime bị nhét theo, và để các thứ nặng chạy ở process con.\nHai: kỷ luật không nằm trong ý định, nó nằm trong test.\nMọi con số tôi khoe ở đầu bài đều có thứ gì đó giữ lại. 1000 token? Có prompt_test.go fail nếu tôi viết dài hơn. 66 color token? Có m12_test.go. help phải nêu tên mọi subcommand và không được sót @both? Có usage_test.go — sinh ra sau vụ 13 subcommand biến mất khỏi help cùng một lúc.\nKỷ luật mà không có test thì sau 3 ngày và 100 commit, nó không còn là kỷ luật nữa — nó chỉ còn là ký ức.\nBa: đừng port code. Port cái vì sao.\nChỗ nào tôi port cả implementation lẫn lý do, tôi đi rất nhanh. Chỗ nào tôi port implementation mà không ghi lại lý do, tôi mất nửa ngày mới phát hiện ra mình vừa mang theo một quyết định của người khác.\ndocs/parity-delta.md vì thế có giá trị ngang với code. Nó là nơi tôi ghi: omp làm A, tôi làm B, và vì sao là B.\n9) Try it if you want curl -fsSL https://raw.githubusercontent.com/FreePeak/xdev/main/scripts/install.sh | sh Xong thì xdev setup để tạo ~/.xdev/agent/, sửa models.yml trỏ vào gateway bạn đang có, rồi:\nxdev tui # chế độ tương tác xdev \u0026#34;tìm chỗ nào trong repo này đang log ra password\u0026#34; xdev --resume 01a0 # resume theo prefix session id xdev bench --turns 5 --model @smol đo TTFT và decode p50/p95 cho provider của bạn — nếu bạn tò mò về con số của tôi.\nRepo: github.com/FreePeak/xdev (Apache-2.0).\n10) Closing Câu hỏi \u0026ldquo;một công cụ viết code thì cần gì trong bộ nhớ\u0026rdquo; với tôi giờ đã có đáp án: khoảng 16MB, 4 cái tool, và một cái session format mà jq đọc được.\nPhần còn lại của 7 ngày là học cách không làm nhiều hơn thế.\nCòn 200MB kia của những tool bạn đang dùng? Không phải lỗi của họ. Họ chọn rộng thay vì nhẹ, và với nhiều người thì lựa chọn đó đúng.\nChỉ là sáng nay, tôi mở ba tab terminal \u0026ldquo;đang nghĩ\u0026rdquo;, nhìn htop, và lần đầu tiên không thấy mình đang đốt RAM chỉ để chờ một cái model trả lời.\n","permalink":"https://freepeak.github.io/posts/xdev-cach-toi-viet-agent-coding-cli-nhe-va-nhanh/","summary":"I use omp, opencode and Claude Code every day, and I got more and more annoyed by how heavy they are. This is the story of the 7 days I spent writing xdev: reading the pi/omp architecture, porting the data model to Go, fighting the terminal UI, and the numbers I measured on my own machine - a 20MB binary, ~16MB RAM, 40ms startup.","title":"xdev: how I wrote a lightweight, fast agent coding CLI"},{"content":"EJBCA + GCP Cloud KMS: A Practical Installation and Configuration Guide Ngày: 2026-02-05\nTrạng thái: Thành công Test\nEJBCA Version: Community Edition 9.3.7\nOverview Hướng dẫn này tài liệu hóa việc tích hợp thành công EJBCA Community Edition với GCP Cloud KMS sử dụng PKCS#11 library. Điểm đột phá chính là sử dụng tùy chọn cấu hình generate_certs: true.\n[!IMPORTANT] Cấu hình Quan Trọng: Bạn PHẢI set generate_certs: true trong file config kmsp11.yaml. Nếu không có tùy chọn này, Java SunPKCS11 không thể phát hiện các khóa GCP KMS.\nPrerequisites GCP Project với Cloud KMS API enabled Cloud HSM keys đã tạo (EC P-256 recommended cho signing) VM với service account có cloudkms.signerVerifier và cloudkms.viewer roles EJBCA Community Edition deployed qua Docker GCP KMS PKCS#11 library (libkmsp11.so) installed Step-by-Step Configuration Step 1: Create Cloud HSM Keys # Create keyring gcloud kms keyrings create dev-pki-ca-keys \\ --location=us-east1 \\ --project=cb-device-services # Create EC P-256 signing key for Policy CA gcloud kms keys create dev-policy-ca-key-v2 \\ --location=us-east1 \\ --keyring=dev-pki-ca-keys \\ --purpose=asymmetric-signing \\ --default-algorithm=ec-sign-p256-sha256 \\ --protection-level=hsm \\ --project=cb-device-services Step 2: Install PKCS#11 Library # Download and install PKCS11_VERSION=\u0026#34;1.3\u0026#34; wget https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/pkcs11-v${PKCS11_VERSION}/libkmsp11-${PKCS11_VERSION}-linux-amd64.tar.gz tar -xzf libkmsp11-${PKCS11_VERSION}-linux-amd64.tar.gz sudo mkdir -p /opt/kmsp11 sudo cp libkmsp11.so /opt/kmsp11/ sudo chmod 755 /opt/kmsp11/libkmsp11.so Step 3: Configure PKCS#11 (CRITICAL) Tạo /etc/kmsp11/kmsp11.yaml:\n--- tokens: - key_ring: \u0026#34;projects/cb-device-services/locations/us-east1/keyRings/dev-pki-ca-keys\u0026#34; label: \u0026#34;policy-ca-token\u0026#34; generate_certs: true log_directory: \u0026#34;/var/log/kmsp11\u0026#34; [!CAUTION] Tùy chọn generate_certs: true là bắt buộc! Tùy chọn này tạo placeholder X.509 certificates cho mỗi khóa, mà Java SunPKCS11 yêu cầu để phát hiện private keys.\nStep 4: Verify Key Discovery # Test with pkcs11-tool export KMS_PKCS11_CONFIG=/etc/kmsp11/kmsp11.yaml pkcs11-tool --module /opt/kmsp11/libkmsp11.so --list-objects Output mong đợi sẽ hiển thị Certificate Objects cùng với keys:\nCertificate Object; type = X.509 cert label: dev-policy-ca-key-v2 subject: DN: CN=dev-policy-ca-key-v2 Private Key Object; EC label: dev-policy-ca-key-v2 Usage: sign Step 5: Configure EJBCA Docker Đảm bảo Docker container mounts PKCS#11 library và config:\n# docker-compose.yml services: ejbca: image: keyfactor/ejbca-ce:latest volumes: - /opt/kmsp11:/opt/kmsp11:ro - /etc/kmsp11:/etc/kmsp11:ro environment: - KMS_PKCS11_CONFIG=/etc/kmsp11/kmsp11.yaml Step 6: Configure PKCS#11 in EJBCA Thêm vào /opt/keyfactor/ejbca/conf/web.properties:\ncryptotoken.p11.lib.1.name=GCP Cloud HSM cryptotoken.p11.lib.1.file=/opt/kmsp11/libkmsp11.so Restart EJBCA:\ncd /opt/ejbca \u0026amp;\u0026amp; sudo docker-compose restart Step 7: Create Crypto Token in EJBCA Go to CA Functions \u0026gt; Crypto Tokens Click Create new\u0026hellip; Configure: Name: CloudHSM-PolicyCA Type: PKCS#11 Crypto Token PKCS#11 Library: GCP Cloud HSM Slot/Token Label: policy-ca-token Click Save Step 8: Create CA Go to CA Functions \u0026gt; Certificate Authorities Enter CA name: PolicyCA Click Create\u0026hellip; Configure: Crypto Token: CloudHSM-PolicyCA certSignKey: dev-policy-ca-key-v2 Signing Algorithm: SHA256withECDSA Subject DN: CN=Policy CA, O=Crossbar, C=US Signed By: Self Signed Certificate Profile: ROOTCA Validity: 10y Click Create Makefile Commands Command Description make scp-policy-ca ENV=dev Upload install script to Policy CA VM make ssh-policy-ca ENV=dev SSH to Policy CA VM make update-pkcs11-config ENV=dev Update kmsp11.yaml with generate_certs make restart-ejbca ENV=dev Restart EJBCA container make test-keys ENV=dev Test PKCS#11 key discovery make test-ejbca-keys ENV=dev Test EJBCA key discovery Troubleshooting Problem: EJBCA shows \u0026ldquo;CryptoToken does not contain any key pairs\u0026rdquo; Cause: generate_certs: true is not set in kmsp11.yaml\nSolution:\nmake update-pkcs11-config ENV=dev make restart-ejbca ENV=dev Problem: \u0026ldquo;Key with public key hash not on token\u0026rdquo; when importing certificate Cause: Java SunPKCS11 cannot enumerate keys without certificate objects\nSolution: Ensure generate_certs: true is configured. The GCP PKCS#11 library will generate placeholder certificates.\nProblem: \u0026ldquo;CKR_TEMPLATE_INCONSISTENT\u0026rdquo; when generating keys Cause: GCP KMS requires CKA_KMS_ALGORITHM vendor attribute for key generation\nSolution: Create keys via gcloud CLI, not EJBCA. Then use existing keys in EJBCA.\nTechnical Background Why generate_certs: true is Required Java\u0026rsquo;s SunPKCS11 provider requires private keys to have an associated certificate object (CKO_CERTIFICATE) with matching CKA_ID to be discoverable. By default, GCP Cloud KMS PKCS#11 only exposes:\nPrivate key objects Public key objects The generate_certs: true option tells the library to generate placeholder X.509 certificates at runtime for each asymmetric key. These certificates have:\nSubject: CN=\u0026lt;key-label\u0026gt; Matching CKA_ID with the private key This allows Java SunPKCS11 to enumerate the keys and EJBCA to discover them.\nTechnical Architecture \u0026amp; Flow 1. Key Discovery with generate_certs: true This diagram illustrates why the generate_certs: true configuration is critical for EJBCA to discover GCP KMS keys.\nsequenceDiagram participant EJBCA as EJBCA (Java) participant SunP11 as SunPKCS11 Provider participant LibKMS as libkmsp11.so participant GCP as GCP Cloud KMS Note over LibKMS: Config: generate_certs: true EJBCA-\u0026gt;\u0026gt;SunP11: Activate CryptoToken SunP11-\u0026gt;\u0026gt;LibKMS: C_GetAttributeValue(CKA_ID, CKA_LABEL) par Key Retrieval LibKMS-\u0026gt;\u0026gt;GCP: List Keys (CryptoKeyVersions) GCP--\u0026gt;\u0026gt;LibKMS: Returns Asymmetric Keys end Note over LibKMS: CRITICAL STEP:\u0026lt;br/\u0026gt;Generate Placeholder X.509 Certs\u0026lt;br/\u0026gt;for each Key with matching CKA_ID LibKMS--\u0026gt;\u0026gt;SunP11: Returns Objects: 1. Private Key (CKO_PRIVATE_KEY) 2. Public Key (CKO_PUBLIC_KEY) 3. Certificate (CKO_CERTIFICATE) Note over SunP11: SunPKCS11 requires CKO_CERTIFICATE\u0026lt;br/\u0026gt;to \u0026#34;see\u0026#34; the Private Key SunP11--\u0026gt;\u0026gt;EJBCA: Token Activated (Keys Visible) 2. Signing Flow (CA Operations) Once the keys are discovered, EJBCA can use them for signing certificates.\ngraph TD subgraph EJBCA_Container [EJBCA Docker Container] RA[RA/CA Logic] SunP11[Java SunPKCS11] LibKMS[GCP KMS PKCS#11 Library] end subgraph GCP_Cloud [Google Cloud Platform] KMS[Cloud KMS / Cloud HSM] end RA -- \u0026#34;Sign(Hash)\u0026#34; --\u0026gt; SunP11 SunP11 -- \u0026#34;C_Sign(Session, Data)\u0026#34; --\u0026gt; LibKMS LibKMS -- \u0026#34;AsymmetricSign(Key, Digest)\u0026#34; --\u0026gt; KMS KMS -- \u0026#34;Signature (P-256)\u0026#34; --\u0026gt; LibKMS LibKMS -- \u0026#34;Signature\u0026#34; --\u0026gt; SunP11 SunP11 -- \u0026#34;Signature\u0026#34; --\u0026gt; RA Limitations Key Generation: Cannot generate keys from EJBCA (requires GCP-specific attributes) Placeholder Certs: Generated certificates are for compatibility only, not for CA operations Session Restart: EJBCA CE requires server restart if HSM connection is lost References GCP KMS PKCS#11 User Guide EJBCA PKCS#11 Documentation Oracle Java SunPKCS11 Guide Quick Test Commands Verify PKCS#11 Configuration # Test native PKCS#11 export KMS_PKCS11_CONFIG=/etc/kmsp11/kmsp11.yaml pkcs11-tool --module /opt/kmsp11/libkmsp11.so --list-objects # Test EJBCA key discovery sudo docker exec ejbca /opt/keyfactor/bin/ejbca.sh cryptotoken listkeys \u0026#39;CloudHSM-PolicyCA\u0026#39; # Test CA creation (if previous steps worked) sudo docker exec ejbca /opt/keyfactor/bin/ejbca.sh ca createca \u0026#39;PolicyCA\u0026#39; \u0026#39;CloudHSM-PolicyCA\u0026#39; Check EJBCA Health # Check EJBCA status curl -s http://localhost:8080/ejbca/publicweb/healthcheck/ejbcahealth # Check PKCS#11 config in EJBCA sudo docker exec ejbca cat /opt/keyfactor/ejbca/conf/web.properties | grep -i p11 Conclusion Bài viết đã phân tích chi tiết về khả năng tương thích giữa EJBCA Community Edition và GCP Cloud KMS. Mặc dù có giới hạn kiến trúc cơ bản, nhưng với tùy chọn generate_certs: true, chúng ta có thể đạt được sự tích hợp hoạt động cho một số use cases cụ thể.\nTài liệu này được tạo để hướng dẫn cài đặt và cấu hình thực tế cho các bên liên quan.\n","permalink":"https://freepeak.github.io/posts/ejbca-gcp-kms-huong-dan-cai-dat/","summary":"A working integration of EJBCA CE with GCP Cloud KMS over PKCS#11, with the generate_certs: true option.","title":"EJBCA + GCP Cloud KMS: A Practical Installation and Configuration Guide"},{"content":"Tôi từng nghĩ về Cryptography (mật mã học) thuần túy là toán học. Bạn có một thuật toán tốt (RSA, ECC), bạn có một thư viện chuẩn (OpenSSL), bạn chạy nó trên một con server mạnh. Thế là an toàn.\nNhưng thế giới bảo mật phần cứng (Hardware Security) lại cười vào mặt suy nghĩ đó.\nVới dân làm phần cứng, một con server Linux bình thường là một kẻ \u0026ldquo;nhiều chuyện\u0026rdquo; (gossip).\nNó để lộ key trong RAM. Nó \u0026ldquo;hét\u0026rdquo; lên cho hacker biết nó đang tính toán cái gì thông qua tiếng ồn điện năng tiêu thụ. Và tệ nhất: root là chúa. Nếu ai đó có quyền root (hoặc xui hơn, một lỗ hổng kiểu Heartbleed đọc được bộ nhớ), thì private key của bạn không còn là của bạn nữa. Đó là lý do HSM (Hardware Security Module) ra đời. Không phải để tính toán nhanh hơn. Mà để tạo ra một \u0026ldquo;nhà tù\u0026rdquo; vật lý cho các bí mật.\nFIPS 140-2: when the casing costs more than the chip Nếu bạn cầm một con HSM chuẩn (ví dụ FIPS 140-2 Level 3) lên, bạn sẽ thấy nó nặng chịch. Không phải vì nhiều linh kiện, mà vì nó được đổ đặc ruột bằng Epoxy (keo resin cứng).\nTại sao? Để chống lại những gã hacker cầm khoan và dao mổ.\nỞ Level 3 và Level 4, HSM được thiết kế như một quả bom nổ chậm (theo nghĩa bảo vệ dữ liệu):\nLớp vỏ cảm ứng: Nếu phát hiện bị cạy, khoan, hoặc thay đổi nhiệt độ/điện áp bất thường\u0026hellip; Cơ chế Zeroization (Tự hủy): \u0026hellip;nó sẽ ngay lập tức xóa sạch toàn bộ key trong mili-giây. Biến cái cục kim loại giá $20,000 thành cục gạch (brick) đúng nghĩa đen. Đó là cái giá của sự an toàn: thà chết (mất key) chứ không để lọt vào tay giặc.\nThe obsession with randomness (entropy) Máy tính là những kẻ tuân thủ quy tắc tuyệt đối. Bắt nó \u0026ldquo;ngẫu nhiên\u0026rdquo; cũng khó như bắt một ông kế toán già làm thơ vậy.\nTrên server thông thường, khi bạn gọi sinh key, hệ điều hành sẽ cố gắng gom góp chút sự \u0026ldquo;hỗn loạn\u0026rdquo; (entropy) từ bất cứ đâu: tiếng gõ phím, di chuột, ổ cứng quay, gói tin mạng đến\u0026hellip;\nNhưng với những cặp key gốc (Root CA) sống 20 năm, tin vào sự hỗn loạn của chuột và bàn phím là một canh bạc. Nếu nguồn random bị đoán trước (predictable), thì key của bạn cũng vậy. Hacker không cần phá khóa, họ chỉ cần tái tạo lại quy trình sinh khóa.\nHSM giải quyết việc này bằng TRNG (True Random Number Generator). Nó không dùng thuật toán giả lập. Nó dùng vật lý thuần túy. Bên trong con chip là các mạch điện được thiết kế để đo \u0026ldquo;tiếng ồn\u0026rdquo; vật lý:\nNhiệt động học (Thermal noise). Hiệu ứng thác (Avalanche noise) của diode. Đó là sự ngẫu nhiên của vũ trụ, không phải của thuật toán. Và vũ trụ thì không ai đoán trước được.\nSide-Channel Attacks: when a hacker solves physics instead of maths Đây là phần tôi thích nhất. Dân phần mềm thường nghĩ hacker sẽ ngồi giải phương trình toán học để tìm Private Key $d$ từ Public Key $e$. Nhưng hacker phần cứng thực dụng hơn nhiều. Họ không tấn công vào toán học. Họ tấn công vào\u0026hellip; cái bóng của nó.\nKhi CPU thực hiện phép tính crypto (ví dụ: nhân số lớn trong RSA), nó tiêu thụ điện năng và tốn thời gian.\nTiming Attacks: Nếu CPU xử lý bit 1 lâu hơn bit 0 (dù chỉ vài nano-giây), hacker chỉ cần đo thời gian phản hồi của hàng triệu request là có thể thống kê ra chuỗi bit của key.\nPower Analysis (SPA/DPA): Mỗi lần CPU xử lý một phép tính nặng, điện áp sụt nhẹ một cái. Với thiết bị đo đủ nhạy (oscilloscope), hacker nhìn vào đồ thị tiêu thụ điện là biết server đang xử lý cái gì.\nHSM chống lại cái này bằng cách\u0026hellip; làm màu. Nó dùng các thuật toán Constant-time: xử lý bit 0 hay bit 1 cũng phải tốn thời gian y hệt nhau. Nó bọc các lớp lá chắn nhiễu điện từ (shielding) để tín hiệu không lọt ra ngoài.\nNói cách khác, HSM là một \u0026ldquo;poker face\u0026rdquo; hoàn hảo. Dù bên trong đang gào thét tính toán, bên ngoài mặt nó vẫn lạnh tanh, không biến sắc (không đổi dòng điện, không trễ thời gian).\nKey Wrapping: the import-export border rule Nguyên tắc vàng của HSM: Private Key không bao giờ được xuất ra dưới dạng plaintext.\nVậy làm sao để backup? Hay chuyển key sang HSM khác?\nChúng ta dùng Key Wrapping. Hãy tưởng tượng key của bạn là một bức thư mật. HSM không bao giờ cho phép bức thư đó ra khỏi cửa. Nếu bắt buộc phải gửi đi, HSM sẽ lấy một cái két sắt (Key Encryption Key - KEK), bỏ bức thư vào, khóa lại, rồi mới quăng cái két sắt ra ngoài.\nChỉ có một con HSM khác, có đúng chìa khóa của cái két đó (thường được chia sẻ qua quy trình Smart Card hoặc PED - PIN Entry Device), mới mở được.\nKey luôn luôn được bọc. Từ lúc sinh ra cho đến lúc chết đi.\nClosing: buying peace of mind Cuối cùng, khi bạn bỏ tiền mua HSM, bạn không mua tính năng. Bạn đang mua sự đảm bảo về vật lý.\nBạn mua một cái hộp mà bạn biết chắc rằng:\nSố ngẫu nhiên sinh ra là thật sự ngẫu nhiên. Key nằm trong đó an toàn hơn nằm trong tay bạn. Và nếu có ai đó cố gắng cạy nó ra, bí mật sẽ tự biến mất trước khi kịp bị nhìn thấy. Trong thế giới phần mềm đầy rẫy lỗi và lỗ hổng, HSM là một điểm tựa vật lý hiếm hoi mà chúng ta có thể tin tưởng (gần như) tuyệt đối.\nĐọc tiếp: Nếu bạn đã hiểu HSM là gì, hãy xem cách áp dụng nó vào quy trình thực tế qua bài viết: HSM Và Nghi Lễ Ký 3-trên-5: Biến Code Signing Từ Script Thành Quy Trình.\n","permalink":"https://freepeak.github.io/posts/hsm-concept-definition/","summary":"Why a regular Linux server is a terrible place to keep a private key. Decoding the hardcore concepts of HSMs: from self-destructing epoxy shells, to randomness harvested from physical noise, to keeping a poker face under power-analysis attacks.","title":"HSM: The Physical Fortress for Digital Secrets"},{"content":"Gần đây tôi phải build tính năng mới. Khoảng 2-3 lần một tuần. Đôi khi gấp hơn. Càng làm càng thấy time mình tốn vào việc setup, configure, fix bug nhiều hơn là logic chính.\nTôi vốn là engineer, không phải product manager. Nhưng vì deadline, tôi phải tự làm hết. Mỗi lần như thế lại tốn khoảng 1-2 giờ chỉ để setup environment, install dependencies, chạy test.\nVấn đề quan trọng nhất là: làm sao giảm time setup xuống 15 phút mà không sacrifice quality? Nếu sai ở bước này, mọi thứ downstream đều sai - deploy chậm, bug nhiều, team không có momentum.\nTrong AI coding có 2 hướng chính: autocomplete vs agent-based. Autocomplete thì nhanh như Copilot, nhưng chỉ help ở level dòng code. Agent thì mạnh hơn như Cursor, Claude Code, nhưng setup phức tạp và hay \u0026ldquo;hallucinate\u0026rdquo;.\nTôi chọn agent-based vì:\nProject phức tạp cần hiểu context lớn Task involve nhiều file, nhiều system Tôi muốn delegate cả quá trình reasoning chứ không chỉ syntax Nhưng rồi tôi nhận ra: buyer priorities thực tế của dev team khác hẳn với những gì vendor quảng cáo.\nDev team thực tế chỉ quan tâm:\nTăng tốc độ ship feature Giảm bug và regression Đừng break existing code Trong khi các AI tool quảng cáo: \u0026ldquo;rewrite your entire codebase in minutes\u0026rdquo;, \u0026ldquo;100x productivity\u0026rdquo;, \u0026ldquo;zero bug\u0026rdquo;. Nghe hay, nhưng thực tế thì giống như bán khóa học nâng cao cho người đang nằm liệt giường - họ cần basic trước.\nThời gian đầu tôi cứ cài hết. Copilot xong cài Cursor, Cursor xong thử Claude Code, rồi lại tìm Windsurf, Roo, Trae\u0026hellip; Mỗi tool có setup riêng, key riêng, workflow riêng.\nSang tháng 4 năm ngoái, tôi đọc một câu trong Hacker News: \u0026ldquo;Best tool is the one you already know how to use\u0026rdquo;.\nNghe có vẻ thừa. Nhưng tôi nhận ra: mình đang quá tập trung vào tool chứ không vào skill.\nSkills.sh ra đời với ý tưởng: thay vì cài từng tool, bạn cài \u0026ldquo;skill\u0026rdquo; - reusable capability mà bất kỳ agent nào cũng có thể dùng. Một lệnh:\nnpx skills add \u0026lt;owner/repo\u0026gt; Lúc này mới thấy rõ: AI agent tools thì nhiều, nhưng skills mới là thứ thực sự tăng productivity.\nFramework của tôi khá đơn giản, chỉ 3 bước:\nBước 1: Define task type\nCoding: web-design-guidelines, react-best-practices Review: code-review, test-driven-development Debug: systematic-debugging Deploy: cicd-workflows, deployment Bước 2: Match với agent strengths\nQuick fix: dùng Claude Code (CLI, fast) Long session: dùng Cursor (VSCode-based) Parallel tasks: dispatch multiple agents via skills Bước 3: Measure, iterate\nTime before vs after Bug rate before vs after Team satisfaction score Làm hiệu quả với AI coding tools là:\nBiết mình đang ở giai đoạn nào (exploration vs implementation) Chọn skill phù hợp với task đó Dùng agent như \u0026ldquo;orchestrator\u0026rdquo; chứ không phải \u0026ldquo;writer\u0026rdquo; Anti-pattern tôi thấy nhiều:\nTeam setup 10+ AI tools, nhưng mỗi dev chỉ dùng 1-2 cái. Kết quả? License phí cao, adoption thấp, ROI âm.\nMột ví dụ khác: dùng AI để refactor toàn bộ codebase mà không có test coverage. Nghe hợp lý, nhưng thực tế thì như sửa nhà mà không kiểm tra nền móng. Nó sẽ sập.\nFramework này không chỉ áp dụng cho cá nhân, mà cho cả team, company. Level càng cao thì càng cần consistency - ai cũng phải hiểu same process, same expectation.\nCuối cùng, chiến lược dùng AI coding tools là chọn skill cần học và tool cần dùng. Biết mình đang làm gì, chọn tool phù hợp, và measure kết quả.\nĐó là cách tôi giảm time setup từ 2 giờ xuống 15 phút. Không phải do tool magic, mà do mình biết cần skill gì và dùng như thế nào.\nAI Agent Coding Tools List Dưới đây là tổng hợp các AI agent coding tools hỗ trợ bởi Skills.sh:\nAMP - ampcode.com Antigravity - antigravity.google Claude Code - claude.com ClawdBot - clawd.bot Codex - openai.com Cursor - cursor.sh Droid - factory.ai Gemini - gemini.google.com GitHub Copilot - github.com Goose - block.github.io Kilo - kilo.ai Kiro CLI - clawd.bot OpenCode - opencode.ai Roo - roocode.com Trae - trae.ai Windsurf - codeium.com Popular Skills on Skills.sh Theo leaderboard, những skill được install nhiều nhất:\nSkill Owner Installs vercel-react-best-practices vercel-labs/agent-skills 22.0K web-design-guidelines vercel-labs/agent-skills 16.8K building-ui expo/skills 1.2K upgrading-expo expo/skills 1.2K data-fetching expo/skills 1.1K dev-client expo/skills 1.0K deployment expo/skills 975 api-routes expo/skills 938 cicd-workflows expo/skills 903 tailwind-setup expo/skills 874 ","permalink":"https://freepeak.github.io/posts/cach-su-dung-ai-agent-coding-tool-hieu-qua-voi-skills-sh/","summary":"From installing dozens of tools to using the right tool at the right time - one practical framework","title":"How to Use AI Agent Coding Tools Effectively with Skills.sh"},{"content":"Có một thời gian tôi nhìn code signing như một bước “phụ” trong pipeline.\nBuild xong. Ký. Đẩy OTA. Nhưng càng làm với firmware, tôi càng thấy “ký” không phải là một bước kỹ thuật đơn lẻ. Nó là một quyết định quản trị rủi ro.\nVì nếu private key ký firmware bị lộ, thứ bạn mất không chỉ là một artifact. Bạn mất niềm tin của cả chuỗi cung ứng.\nThe original question: what am I signing to protect? Định nghĩa nghe rất đẹp: “ký số để đảm bảo tính toàn vẹn và xác thực”.\nĐọc xong vẫn có cảm giác… trừu tượng.\nĐến khi đặt lại câu hỏi cho đúng:\nMình đang bảo vệ binary hay bảo vệ quyền phát hành? Kẻ tấn công cần hack build server, hay chỉ cần lấy được private key? Nếu private key nằm trong một file .pem trên CI runner, thì đôi khi câu trả lời đáng buồn là: không cần hack gì phức tạp, chỉ cần “lỡ” có người copy key ra khỏi đúng chỗ.\nAn HSM is not for making crypto faster Tôi từng tưởng HSM là để tăng hiệu năng crypto. Nhưng trong bối cảnh code signing, giá trị chính của HSM là: (Nếu bạn muốn hiểu sâu hơn về lớp vỏ vật lý, FIPS 140-2 và tại sao nó an toàn đến mức \u0026lsquo;hoang tưởng\u0026rsquo;, hãy đọc bài HSM: Pháo Đài Vật Lý Cho Những Bí Mật Số)\nPrivate key được sinh ra và nằm trong phần cứng. Private key không được “export” theo nghĩa thực dụng (không có file để copy). Người ký phải cắm thiết bị + nhập PIN, tức là có sự hiện diện. Nói cách khác: HSM biến “ký” từ một hàm trong code thành một hành vi có ma sát.\nMa sát này chính là thứ cứu bạn trong những ngày xui.\nThe decision tree: why a portable HSM (instead of a huge one) Khi chọn HSM cho hệ thống ký firmware, tôi thấy có ít nhất 3 hướng (mỗi hướng có cái giá riêng):\nHSM dạng appliance (đặt trong data center) Ưu: tập trung, kiểm soát chặt. Nhược: vận hành nặng; dễ trở thành “single bottleneck” cho release. Cloud HSM Ưu: tích hợp tốt với cloud stack. Nhược: ràng buộc cloud; câu chuyện “ai giữ key” đôi khi khó nói. HSM dạng portable cho signer Ưu: chia quyền ký cho nhiều người; giảm điểm chết. Nhược: phải quản lý phân phối, quy trình, audit. Trong một triển khai điển hình, lựa chọn ở đây là một HSM dạng portable: nhỏ, có chứng nhận, hỗ trợ PKCS#11, đủ dùng cho ký Ed25519, và quan trọng là có thể phát cho từng signer.\nReality check: what do buyers (and the ops team) actually care about? Nếu bỏ hết jargon, tôi thấy yêu cầu thực tế hay rơi vào 3 nhóm (và nhóm nào cũng “đúng”, chỉ khác thứ tự ưu tiên):\nGiảm rủi ro lộ key (blast radius nhỏ) Không làm release bị kẹt (velocity) Có audit trail để giải trình (ai ký, ký cái gì, lúc nào) Nhiều hệ thống ký số thất bại vì cố tối ưu nhóm (1) nhưng làm nát nhóm (2): mọi thứ phải qua một “ông admin giữ key”, đến lúc ông đó đi họp là release đứng hình.\nThe 3-of-5 signing ritual: looks like paperwork, is actually a guardrail Thiết kế 3-trên-5 (3-of-5) nghe có vẻ “corporate”, nhưng nó giải được một bài toán rất đời thường:\nKhông muốn 1 người có thể tự ý phát hành firmware. Nhưng cũng không thể bắt đủ 5 người online cùng lúc. Một cách nhìn đơn giản:\nBuild Server -\u0026gt; Signing Server -\u0026gt; (signing request) -\u0026gt; 5 Signers |-\u0026gt; thu ít nhất 3 chữ ký hợp lệ v OTA/Release Điểm hay là bạn đang chia rủi ro theo cả hai trục:\nTrục kỹ thuật: key nằm trong HSM, không rời khỏi thiết bị. Trục tổ chức: quyền phát hành cần đồng thuận, không phụ thuộc một người. A 3-step framework for thinking about firmware signing (pragmatic) Nếu phải nén mọi thứ lại để triển khai được, tôi dùng 3 bước sau:\nKhóa (keys) Chỉ đưa private key vào nơi có kiểm soát mạnh nhất. Ưu tiên “không thể copy” thay vì “khó copy”. Nghi lễ (ceremony) Ai được ký? Ký cái gì? Cửa sổ ký mở bao lâu? Multi-party approval (ví dụ 3-trên-5) là luật tổ chức, không phải tính năng. Bằng chứng (audit) Log không phải để “đẹp”, mà để khi có sự cố bạn trả lời được: chuyện gì đã xảy ra. Một mảnh ghép thường bị xem nhẹ là chuẩn giao tiếp giữa tool và HSM: PKCS#11. Bạn không cần nhớ hết API, chỉ cần giữ một mental model kiểu “client trao đổi với HSM” như dưới đây:\nPKCS#11: signing client \u0026lt;-\u0026gt; HSM Signing Client (app/tool) HSM (PKCS#11 token) ----------------------------------------------------------------- 1) C_Initialize() -\u0026gt; token ready 2) C_OpenSession() -\u0026gt; session handle 3) C_Login(PIN) -\u0026gt; verify PIN / policy 4) C_FindObjects*() -\u0026gt; key handle (non-exportable) 5) C_Sign(digest) -\u0026gt; sign with private key digest -----------------------------\u0026gt; signature \u0026lt;-------------------------- 6) C_Logout() + C_CloseSession() -\u0026gt; clear state 7) C_Finalize() -\u0026gt; unload module Key idea: digest goes in, signature comes out; private key never leaves HSM. Anti-pattern: turning a private key into just another secret Một số phản xạ rất tự nhiên (và rất nguy hiểm):\nNhét private key vào CI secrets vì “tiện”. Cho build server quyền ký trực tiếp vì “tự động hóa”. Để một người vừa build vừa ký vì “ít người thì làm gì có separation-of-duties”. Những thứ này đều có chung một bệnh: nó làm giảm ma sát đến mức kẻ tấn công chỉ cần chiếm một hệ thống (CI/build) là có quyền phát hành.\nHSM không làm bạn miễn nhiễm. Nó chỉ giúp bạn buộc hệ thống phải trả lời câu hỏi: “Key đang ở đâu, ai đang chạm vào nó?”\nClosing Tôi thích nhìn HSM như một cách “định hình hành vi” hơn là một món đồ crypto.\nKhi bạn thiết kế ký firmware với HSM + 3-trên-5, bạn đang nói thẳng một nguyên tắc:\nQuyền phát hành không thuộc về một script. Nó thuộc về một quy trình có người chịu trách nhiệm, có đồng thuận, và có dấu vết. Và nếu phải nhớ một câu để mang sang dự án khác: đừng tối ưu sự tiện cho người ký đến mức quên mất sự an toàn cho người dùng.\nQuick definitions (so you do not get lost) Thuật ngữ Nghĩa trong bài này OTA Over-The-Air: kênh phát hành/cập nhật firmware từ server xuống thiết bị qua mạng. HSM (Hardware Security Module) Thiết bị phần cứng giữ private key và thực hiện thao tác ký; private key không rời khỏi thiết bị. Code signing Ký số lên firmware để thiết bị kiểm tra “ai phát hành” và “có bị sửa không”. Signer Người được cấp quyền ký (mỗi người giữ 1 HSM). Signing ceremony Quy trình/“nghi lễ” tổ chức việc ký: ai ký, ký cái gì, trong cửa sổ thời gian nào, audit ra sao. 3-of-5 Cần ít nhất 3 chữ ký hợp lệ trong 5 signer để phát hành. Digest Giá trị băm (ví dụ SHA-256) đại diện cho nội dung image; thường ký trên digest thay vì ký cả file. Ed25519 Thuật toán chữ ký (ECC) cho chữ ký 64 bytes, nhanh và gọn cho firmware. PKCS#11 Chuẩn API để app nói chuyện với HSM (open session, login, sign\u0026hellip;). mTLS Mutual TLS: client và server đều xác thực lẫn nhau bằng certificate khi kết nối. Signing request (trong bài này) Yêu cầu ký (payload + metadata). Tránh nhầm với Certificate Signing Request. ","permalink":"https://freepeak.github.io/posts/hsm-3-tren-5-ky-so-firmware-yubihsm2/","summary":"An HSM is not just a hardware crypto box. It is a way to force digital signing - so often reduced to a script in CI - back into a ritual with constraints: the key never leaves the device, the signer shows up in person, and the signature needs enough people to agree.","title":"HSM and the 3-of-5 Signing Ritual: Turning Code Signing From a Script Into a Process"},{"content":"About money Thông báo lương về lúc 9:03 sáng.\nMột cái rung nhẹ, một dòng số nhảy lên, và tự nhiên cả tuần tôi biết cười lịch sự hơn. Tôi hay gọi đó là “tiền an thần”: nó không làm mình thông minh hơn, nhưng làm mình dễ để yên những câu hỏi khó.\nTôi biết sai vẫn làm.\nCái sai ở đây không phải là “đi làm” (đi làm có gì sai). Cái sai là cái khoảnh khắc mình giả bộ như không có giao dịch nào đang xảy ra: mình đưa 8 tiếng mỗi ngày, đổi lại một phần an tâm.\nSpoiler: nếu ai đổi “an tâm” bằng tiền, thì an tâm đó thường là hàng có hạn sử dụng.\nWhy we work Nếu gọi thẳng ra, tôi thấy chúng ta đi làm vì ba thứ rất người, và rất dễ hiểu.\nThứ nhất: vì cần sống.\nThuê nhà, tiền sửa răng, tiền thuốc, tiền bị bệnh, tiền cho bố mẹ, tiền học của con. Con người không ăn lý tưởng được.\nThứ hai: vì cần được công nhận.\nĐi làm không chỉ là bán sức lao động. Nó là mua một cái nhãn: “tôi có ích”. Một cái badge, một cái title, một cái email domain, là tự nhiên mình thấy mình có chỗ đứng trên đời.\nThứ ba: vì sợ bị bỏ lại.\nỞ nhà quá lâu, mình sẽ nghe rõ tiếng đồng hồ và tiếng những câu hỏi. Đi làm là cách mình làm ồn, tưởng như đang tiến lên. Đường thẳng hay vòng không quan trọng. Quan trọng là mình vẫn đang “đi”.\nBa lý do đó đều hợp lý. Vì nó hợp lý nên nó càng khó thoát.\nSelling yourself to capital is selling what Nói “bán mình cho tư bản” nghe như một câu khẩu hiệu. Nhưng nếu bỏ chữ nghĩa đi, nó chỉ là một mô tả đời thường: ta đem một phần đời mình ra đổi lấy tiền.\nVấn đề là ta không bán một lần. Ta cho thuê theo tháng.\nCho thuê thời gian.\nCho thuê sự chú ý.\nCho thuê sự kiên nhẫn.\nCho thuê cái mặt “không sao đâu” để giữ hòa khí.\nVà vì là cho thuê nên nó có điều khoản. Điều khoản thường không viết ra, nhưng ai cũng ngầm hiểu:\nBạn sẽ có những ngày chán việc, nhưng vẫn phải tỏ ra “professional”. Bạn sẽ có những câu hỏi đạo đức, nhưng ưu tiên của bạn là hóa đơn cuối tháng. Bạn sẽ thấy hệ thống có vấn đề, nhưng hệ thống không trả lương cho “thấy”. Tư bản, ở mức đời thường nhất, không phải một ông chủ ngồi vuốt râu cười. Nó là một cơ chế có năng lực biến thời gian sống thành chi phí vận hành.\nCòn chúng ta cũng có một năng lực không kém: tự thuyết phục mình rằng “đây là bình thường”.\nTôi hay tự giễu rằng trong đầu mình có một startup vô hình, chuyên gọi vốn bằng câu: “Ráng thêm chút nữa là ổn.” Startup đó chưa bao giờ IPO, nhưng gọi vốn rất đều.\nWhy people split into camps and hate each other Nếu chỉ có chuyện “đi làm để sống”, ta đã có thể thương nhau hơn. Nhưng không. Ta chia phe.\nPhe người làm văn phòng cười người làm tay chân. Phe “ngành hot” cười phe “ngành lương thấp”. Phe có tài sản cười phe thuê nhà.\nRồi trong công ty: team này ghét team kia, nhân viên ghét sếp, sếp ghét nhân viên.\nTôi nghĩ có ba động cơ chính.\nThứ nhất: khan hiếm tạo ra cuộc chiến ngang.\nKhi tài nguyên khan hiếm (tiền, cơ hội, chỗ đứng), con người có xu hướng đánh nhau với người đứng cạnh mình.\nTrong một căn phòng thiếu oxy, người ta giành nhau cái bình thở, chứ không có thời gian đi hỏi vì sao cửa phòng bị khóa.\nKhi layoff rình rập, thăng tiến ít đi, thưởng giảm, người ta nhìn đồng nghiệp như một biến số. Không còn là “người cùng chịu”, mà là “đối thủ cùng phòng”.\nThứ hai: bản sắc là thuốc giảm đau.\nCon người chịu đau giỏi hơn khi nỗi đau có tên. “Tôi thuộc phe chăm chỉ”, “tôi thuộc phe tỉnh thức”, “tôi thuộc phe thực tế”, “tôi thuộc phe nạn nhân”, “tôi thuộc phe đạo đức”.\nPhe phái đôi khi không phải là lý trí. Nó là một loại thuốc giảm đau.\nVà thuốc nào cũng có tác dụng phụ: nó làm ta bớt nhìn người khác như người.\nThứ ba: hệ thống rất thích chúng ta ghét nhau.\nKhông cần âm mưu. Chỉ cần cơ chế. Khi người lao động bận đánh nhau với nhau, họ ít thời gian nhìn lên.\nCuộc chiến ngang giúp hệ thống rẻ đi. Rẻ chi phí thương lượng. Rẻ chi phí thay thế. Rẻ cả chi phí đạo đức, vì người ta tự đổ lỗi cho nhau thay vì hỏi: “Tại sao mình phải sống như vậy?”\nWhat the benefit actually is Lợi ích có hai phía.\nVới hệ thống: con người được nén thành những thứ dễ đo lường.\nBạn thành headcount. Bạn thành resource. Bạn thành KPI.\nKhi con người thành số, việc cắt giảm trở nên “hợp lý”. Khi con người thành KPI, việc kiệt sức trở nên “bình thường”.\nVới cá nhân: ta mua được một thứ rất khó mua: cảm giác kiểm soát.\nLương về, ta thấy mình kiểm soát được tháng này. Có phe, ta thấy mình kiểm soát được thế giới. Có kẻ để ghét, ta thấy mình kiểm soát được sự bất lực.\nTôi không nói vậy để mắng ai. Tôi cũng làm y như thế.\nThe escape hatch for an ordinary employee Tôi hay gặp một câu tự trách kiểu này:\n“Mình chỉ là nhân viên bình thường. Mình không ham tranh giành. Mình không muốn leo lên team lead. Vậy mình sống sao cho đỡ khổ?”\nTôi nghĩ đây là một lựa chọn tử tế, chứ không phải một sự thiếu tham vọng.\nNhưng để lựa chọn đó sống được, mình cần một thứ khác thay cho địa vị. Nếu không, mình sẽ rơi vào kiểu “không muốn leo” nhưng vẫn bị so sánh giết chết mỗi ngày.\nTôi tìm được một cái khung khá hợp khi đọc một bài về chuyện “reset cuộc đời trong 1 ngày” (link: https://tramdoc.vn/tin-tuc/cach-sua-lai-toan-bo-cuoc-doi-ban-chi-trong-1-ngay-nGGl6W.html).\nÝ tôi rút ra, nói ngắn gọn:\nThay vì cố sửa hành vi, hãy sửa cái “kiểu người” mình đang tin là mình phải trở thành. Mục tiêu không chỉ là đích đến, nó là một “lăng kính” khiến ta nhìn đời theo một kiểu. Và ở đây tôi mượn thêm một mẩu Phật pháp theo nghĩa thực dụng: giải thoát là bớt dính.\nKhông phải bớt dính để thành một người vô cảm. Là bớt dính để cái tâm mình không bị lôi đi bằng mỗi lời khen chê.\nBạn có thể làm một nhân viên “bình thường”, nhưng không tầm thường:\nLàm việc có tay nghề, đáng tin. Không gây drama. Về nhà vẫn còn phần đời cho mình. Lối thoát, với tôi, có ba hướng. Bạn có thể chọn cả ba, nhưng ưu tiên khác nhau.\n1) Keep the job, lose the anguish Đây là “giải thoát” cấp độ một: vẫn làm, vẫn nhận lương, nhưng bớt bị kéo bởi so sánh.\nViệc khó nhất không phải là công việc. Việc khó nhất là cái tâm lúc ngồi trong công việc.\nMột vài nguyên tắc tôi thấy hữu ích:\nPhân biệt rõ: “Tôi làm tốt” khác “Tôi có giá trị”. Tự nhắc: cảm xúc khó chịu là một hiện tượng, không phải một bản án. Khi bị kích hoạt (bị chê, bị coi thường, bị bỏ qua), hãy chậm lại 10 giây trước khi phản ứng. Nếu nghe giống thiền, thì đúng là thiền. Nhưng phiên bản công sở của thiền chỉ là: đừng để bản ngã lái máy.\n2) Optimise for more time (cut demand, gain freedom) Nếu bạn không muốn leo thang địa vị, bạn vẫn cần “đòn bẩy” khác. Đòn bẩy của người không leo là: giảm mức lệ thuộc.\nVài việc rất đời:\nBiết mình cần bao nhiêu tiền để “đủ” (không phải để khoe). Tạo một quỹ dự phòng để bạn có quyền nói “không” khi cần. Nâng tay nghề ở mức đủ để không bị lệ thuộc một nơi. Đây không phải “tự do tài chính”. Đây là “tự do tối thiểu”: đủ để thở.\nTrong Phật pháp, khổ nhiều khi đến từ tham. Nhưng “tham” không chỉ là tham tiền. Nó là tham cảm giác an toàn tuyệt đối.\nAn toàn tuyệt đối không có. Chỉ có bớt phụ thuộc.\n3) Dare to change workplace (without burning bridges) Không leo chức không có nghĩa là bạn phải chịu mọi môi trường.\nCó những nơi văn hóa độc hại, KPI dùng để đánh người, và “tận tâm” được hiểu là “không được quyền mệt”.\nTrong những nơi đó, thực hành tâm linh chỉ giúp bạn chịu đựng giỏi hơn. Chịu đựng giỏi hơn chưa chắc là giải thoát.\nĐổi môi trường làm việc là một hành động có thể rất tỉnh:\nBạn không bỏ đi trong tức giận. Bạn chuẩn bị đường lui. Bạn giữ quan hệ tử tế. Bạn rời đi vì hiểu giới hạn của mình. Giải thoát không phải là thắng. Giải thoát là biết lúc nào không cần đánh nữa.\nClosing Thông báo lương vẫn sẽ rung.\nTôi vẫn sẽ mừng, kiểu mừng của người vừa được thở thêm một tháng.\nNhưng nếu có thể, tôi muốn mình nhớ một điều: mình đi làm để sống, không phải sống để đi làm.\nVà nếu đã phải cho thuê thời gian, thì ít nhất đừng cho thuê luôn cái tâm.\nCuộc đời nhiều khi không tàn nhẫn. Nó chỉ rất giỏi bắt mình tàn nhẫn với nhau.\nTôi thì chắc vẫn đi làm. Nhưng cố gắng đừng biến mình thành một người chỉ biết cười khi tiền về.\n","permalink":"https://freepeak.github.io/posts/chung-ta-di-lam-vi-dieu-gi/","summary":"\u003ch2 id=\"about-money\"\u003eAbout money\u003c/h2\u003e\n\u003cp\u003eThông báo lương về lúc 9:03 sáng.\u003c/p\u003e\n\u003cp\u003eMột cái rung nhẹ, một dòng số nhảy lên, và tự nhiên cả tuần tôi biết cười lịch sự hơn.\nTôi hay gọi đó là “tiền an thần”: nó không làm mình thông minh hơn, nhưng làm mình dễ\nđể yên những câu hỏi khó.\u003c/p\u003e\n\u003cp\u003eTôi biết sai vẫn làm.\u003c/p\u003e\n\u003cp\u003eCái sai ở đây không phải là “đi làm” (đi làm có gì sai). Cái sai là cái khoảnh khắc mình\ngiả bộ như không có giao dịch nào đang xảy ra: mình đưa 8 tiếng mỗi ngày, đổi lại một\nphần an tâm.\u003c/p\u003e","title":"Why We Work"},{"content":"Tôi làm backend.\nNên tôi từng đi qua một giai đoạn rất “thẳng”: query chậm thì thêm index.\nNó cũng giống như lúc bạn thấy đường kẹt xe, bạn nghĩ “thêm một làn nữa là xong”. Thỉnh thoảng đúng. Nhưng đa phần là… bạn chỉ dời điểm kẹt từ chỗ này sang chỗ khác, và tự nhiên chi phí bảo trì đường tăng lên.\nMySQL index cũng vậy.\nNó làm query nhanh lên thật. Nhưng nó không miễn phí. Và quan trọng hơn: nó chỉ nhanh khi query đi đúng đường.\nThe original question: in what order does your query want to travel? Một query phổ biến trong backend thường làm 3 việc:\nLọc dữ liệu (WHERE) Sắp xếp (ORDER BY) Lấy ít thôi (LIMIT, phân trang) Nếu không có index phù hợp, MySQL sẽ phải:\nquét nhiều dòng hơn cần thiết hoặc lọc xong mới sort (Using filesort) hoặc vừa sort vừa đọc bảng quá nhiều (I/O tăng, CPU tăng) Nói cho gọn: vấn đề không nằm ở việc “có index hay không”, mà nằm ở việc “index hiện tại có đúng với đường lọc/sort của query hay không”.\nEvery choice has a cost (the backend constraint) Tôi hay gặp 3 kiểu “đời sống” khi đụng index:\nRead-heavy: API phải nhanh, p95 quan trọng. Write-heavy: insert/update nhiều, index thêm vào là write chậm thấy rõ. Bảng lớn dần: query đang ổn sẽ tự nhiên xuống cấp theo thời gian. Và vì mỗi lựa chọn đều có giá, nên index không nên được tạo theo cảm giác.\nBạn tạo index là bạn đang ký vào một “hợp đồng”:\nnhanh hơn cho một nhóm query đổi lại tốn dung lượng và tốn chi phí ghi cho mọi mutation In practice users only care about 3 things Nếu bỏ hết thuật ngữ đi thì cuối cùng vẫn là:\nAPI trả nhanh hơn Giảm chi phí vận hành Giảm nguy cơ incident Bạn có thể giải thích B-Tree với đồng nghiệp cả buổi. Nhưng nếu endpoint list đơn hàng vẫn 3–4 giây thì sản phẩm vẫn đau.\nDefinitions that sound good, reality that traps you MySQL 8+ sometimes skips an index on its own (low selectivity) Có một cảm giác rất dễ gây hoang mang: bạn đã tạo index rồi, nhưng EXPLAIN vẫn cho thấy MySQL quét bảng.\nVới MySQL 8 trở lên, chuyện này lại càng “bình thường” hơn, vì optimizer có nhiều thông tin/cost model tốt hơn để tự quyết.\nTrường hợp hay gặp nhất là độ chọn lọc thấp.\nVí dụ cột status chỉ có vài giá trị kiểu PAID | PENDING | CANCELLED. Nếu hệ thống của bạn có 30% đơn hàng ở trạng thái PAID, thì query:\nSELECT id, status, created_at FROM orders WHERE status = \u0026#39;PAID\u0026#39;; có thể phải đọc một phần rất lớn của bảng.\nTrong tình huống đó, đi theo index status đôi khi còn tệ hơn:\nMySQL đọc index để lấy rất nhiều row_id. Sau đó phải \u0026ldquo;quay lại\u0026rdquo; bảng để đọc dữ liệu thật (random I/O). Nên optimizer có thể chọn quét bảng (sequential I/O) và filter trong quá trình đọc.\nDấu hiệu nhận biết trong EXPLAIN:\npossible_keys có index (ví dụ idx_status) nhưng key = NULL type = ALL rows ước lượng lớn Định nghĩa kiểu sách giáo khoa: index giúp tăng tốc truy vấn.\nĐúng.\nNhưng đời thật là: index chỉ giúp nếu query có thể dùng index đó một cách “tử tế”.\nVà có vài thứ khiến MySQL “bó tay”, dù bạn có index:\nBọc cột bằng hàm trong WHERE Ép kiểu lộn xộn LIKE '%keyword%' OR tùy tiện OFFSET phân trang quá lớn Những thứ này làm bạn cảm giác “mình đã có index rồi mà vẫn chậm”.\nThực ra là query đang đi sai đường.\nOne lens that is easy to use: start from the important query, not the table Đừng hỏi: “Bảng orders cần index gì?”\nHãy hỏi:\nEndpoint nào gọi nhiều nhất? Query nào nóng nhất (top slow queries / APM)? Nó lọc theo cột nào? Nó sort theo cột nào? Nó phân trang kiểu gì? Khi bạn trả lời được 4–5 câu đó, index tự nhiên “hiện hình”.\nA 3-step framework (enough for backend developers) Step 1: Write the query the way MySQL can use an index Một anti-pattern kinh điển:\nWHERE DATE(created_at) = \u0026#39;2026-01-01\u0026#39; Bạn nhìn thấy “hợp lý” vì bạn đang nghĩ theo ngôn ngữ người. Nhưng MySQL thì thấy: “bạn bắt tôi tính DATE() cho mọi dòng, rồi mới so sánh”.\nCách viết tốt hơn:\nWHERE created_at \u0026gt;= \u0026#39;2026-01-01\u0026#39; AND created_at \u0026lt; \u0026#39;2026-01-02\u0026#39; Ngắn gọn: muốn dùng index thì đừng bọc cột bằng hàm trong điều kiện lọc.\nStep 2: Design the index in the real filter/sort order Giả sử backend có endpoint list đơn hàng của user:\nSELECT id, status, created_at, total_amount FROM orders WHERE user_id = ? AND status = ? ORDER BY created_at DESC LIMIT 20; Index hợp lý thường là index ghép:\nCREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at DESC); Lý do:\nuser_id và status là điều kiện = giúp thu hẹp tập dữ liệu nhanh created_at phục vụ ORDER BY ... DESC để tránh sort và trả nhanh LIMIT 20 Một lỗi hay gặp là tạo index theo kiểu “mình thấy cột created_at quan trọng” rồi đặt nó lên đầu. Nhưng nếu query luôn bắt đầu bằng user_id, bạn đặt user_id lên trước thường đúng hơn.\nBạn không cần nhớ quá nhiều lý thuyết. Chỉ cần nhớ quy tắc “leftmost prefix” của index ghép:\n(user_id, status, created_at) dùng tốt khi query có user_id trước thiếu user_id thì các cột sau khó phát huy Step 3: Use EXPLAIN to confirm, not faith Sau khi tạo index, chạy:\nEXPLAIN SELECT ... Những dấu hiệu bạn muốn thấy:\nkey là index bạn vừa tạo (không phải NULL) type không phải ALL rows ước lượng giảm mạnh hạn chế Using filesort khi bạn đã thiết kế index để ăn ORDER BY Thói quen tốt: “tạo index xong là phải kiểm chứng”, không phải “cảm giác thấy ổn”.\nAnti-pattern: adding an index out of fear, and the price behind it Nhiều team đi vào vòng lặp:\nQuery chậm → thêm index Lại có query khác chậm → thêm index Một thời gian sau: write chậm, replication lag, disk phình, buffer pool căng Index kiểu này giống mua thêm đồ nghề vì sợ thiếu. Lúc đầu có tác dụng. Nhưng về sau nó thành gánh nặng vận hành.\nMột câu hỏi nên tự hỏi trước khi thêm index:\n“Index này cứu endpoint nào, tần suất bao nhiêu, và đánh đổi gì cho write?”\nPagination: large OFFSET is a performance killer Pagination kiểu:\nLIMIT 20 OFFSET 20000 OFFSET càng lớn thì MySQL càng phải đọc/đi qua nhiều dòng rồi mới trả 20 dòng bạn cần.\nNếu endpoint quan trọng và dữ liệu lớn, cân nhắc keyset pagination:\nSELECT id, status, created_at, total_amount FROM orders WHERE user_id = ? AND status = ? AND created_at \u0026lt; ? ORDER BY created_at DESC LIMIT 20; Khi đó index (user_id, status, created_at) gần như “đúng sách”.\nA quick checklist (for backend developers) Bắt đầu từ query thật (hot path), không bắt đầu từ “bảng này nên có gì”. Tránh điều kiện phá index: hàm trên cột, ép kiểu, LIKE '%...%'. Index ghép: ưu tiên cột lọc = trước, rồi đến cột phục vụ sort/range. ORDER BY + LIMIT là cơ hội tối ưu rất lớn nếu index đúng. OFFSET lớn: cân nhắc keyset pagination. Mỗi index là chi phí ghi + dung lượng + bảo trì. Đừng tạo cho yên tâm. Closing on one principle Index không phải bùa.\nIndex là đường đi.\nMuốn query nhanh, bạn không cần nhiều đường. Bạn cần đúng đường, và viết query để đi đúng đường đó.\nNếu bạn muốn, gửi mình 1–2 query đang chậm (kèm schema các cột liên quan và ước lượng số bản ghi). Tôi sẽ gợi ý index cụ thể và chỉ ra điểm nào khiến optimizer đang chọn plan không như bạn kỳ vọng.\n","permalink":"https://freepeak.github.io/posts/mysql-index-duong-di-cho-query/","summary":"An index is only fast when the query takes the right path. A practical lens for backend developers: start from the queries that matter, write sargable conditions, design composite indexes in the real filter/sort order, and verify with EXPLAIN.","title":"MySQL Index: The Path a Query Takes (Written for Backend Developers)"},{"content":"Trong hệ sinh thái Cloud Native ngày nay, việc quản lý traffic đi vào (Ingress) và giao tiếp giữa các services (East-West) là bài toán cốt lõi. Đối với các hệ thống chạy trên Google Kubernetes Engine (GKE), mặc dù GCP cung cấp Ingress Controller mặc định (GCE Ingress), nhưng nhiều đội ngũ kỹ thuật vẫn lựa chọn Kong Open Source.\nBài viết này sẽ đi sâu vào lý do tại sao Kong lại là lựa chọn ưu việt về mặt kiến trúc, đồng thời chia sẻ các Best Practices đúc kết từ thực tế để triển khai Kong hiệu quả trên GKE.\n1. Introduction: Kong Gateway in the K8s-on-GCP picture Kong Open Source là một API Gateway phổ biến nhất thế giới, được xây dựng dựa trên NGINX và mở rộng bằng Lua (hoặc Go/Rust qua WASM). Trong môi trường Kubernetes, Kong đóng vai trò kép:\nIngress Controller: Điều hướng traffic từ bên ngoài vào cluster tuân theo chuẩn Kubernetes Ingress resource. API Gateway: Thực hiện các chức năng nâng cao như Authentication, Rate Limiting, Logging, Transformation, v.v. Khác với các Load Balancer layer 4 đơn thuần, Kong hoạt động ở Layer 7, cho phép xử lý logic nghiệp vụ phức tạp ngay tại \u0026ldquo;cửa ngõ\u0026rdquo; của hệ thống.\nIts role in the GKE architecture Trên GKE, Kong thường đứng sau Google Cloud Load Balancer. Mô hình phổ biến là: Client -\u0026gt; GCP L4 Load Balancer -\u0026gt; Kong Proxy Service -\u0026gt; K8s Services\nMô hình này tận dụng được sức mạnh hạ tầng mạng toàn cầu của Google (Global Load Balancing) đồng thời giữ được sự linh hoạt trong xử lý logic của Kong.\n2. Why choose Kong over the default GCP Ingress? GCP cung cấp GCE Ingress Controller tích hợp sẵn rất tốt, nhưng nó thường chỉ giải quyết được bài toán routing cơ bản. Khi hệ thống lớn dần, các hạn chế bắt đầu lộ diện. Dưới đây là lý do các kiến trúc sư phần mềm chuyển sang Kong:\nThe weaknesses of GCP Ingress (GCE) Hạn chế về tính năng: Chủ yếu tập trung vào L7 Load Balancing cơ bản (Path-based routing). Các tính năng như rate limiting, authentication, request transformation gần như không có hoặc rất khó cấu hình. Độ trễ (Latency): Với các rule phức tạp, độ trễ có thể tăng lên. Vendor Lock-in: Cấu hình phụ thuộc chặt chẽ vào Annotation của Google Cloud. Khó mang sang AWS hay Azure nếu cần chiến lược Multi-cloud. Kong\u0026rsquo;s standout advantages Hệ sinh thái Plugin khổng lồ: Đây là \u0026ldquo;vũ khí bí mật\u0026rdquo; của Kong. Bạn cần Rate Limiting? Có plugin. Cần OAuth2/JWT Auth? Có plugin. Cần log request ra Kafka/ELK? Có plugin. Tất cả chỉ cần bật/tắt qua config. Hiệu năng cao: Kong được build trên NGINX và sử dụng OpenResty (Lua JIT), cho phép xử lý hàng chục nghìn request/giây với độ trễ cực thấp (\u0026lt; 10ms). Unified Gateway: Kong có thể xử lý cả traffic từ ngoài vào (North-South) và traffic giữa các service (East-West) trong mô hình Service Mesh (Kong Mesh). Vendor Agnostic: Cấu hình Kong là chuẩn Kubernetes CRD hoặc YAML. Bạn có thể bê nguyên bộ config này chạy trên AWS EKS, Azure AKS hay On-premise mà không cần sửa đổi nhiều. When should you use Kong? Khi bạn cần các tính năng API Management nâng cao (Auth, Quota, Monetization). Khi hệ thống có lượng traffic lớn và yêu cầu độ trễ thấp. Khi bạn muốn đồng nhất công nghệ Gateway trên nhiều môi trường (Hybrid Cloud). 3. Best Practices: Running Kong on GKE Để vận hành Kong ổn định trên môi trường Production, đừng chỉ dừng lại ở helm install. Dưới đây là các thực tiễn tốt nhất bạn nên áp dụng.\na. Preparation and overall architecture 1. Luôn sử dụng Helm Charts Không nên cài đặt Kong bằng các file YAML rời rạc (manifests). Hãy sử dụng Official Kong Helm Chart. Helm giúp bạn dễ dàng quản lý version, rollback khi có sự cố và tùy biến values.yaml một cách có cấu trúc.\n2. Tách biệt Namespace Đừng cài Kong vào default namespace. Hãy tạo một namespace riêng biệt, ví dụ kong-gateway hoặc ingress-controller. Điều này giúp quản lý Resource Quota và RBAC (phân quyền) tốt hơn, tránh việc một developer vô tình xóa nhầm Ingress Controller.\n3. Kiến trúc Load Balancing Trên GKE, hãy cấu hình Service của Kong Proxy là LoadBalancer. Google sẽ tự động cấp phát một External IP và một L4 Load Balancer trỏ vào các Pod Kong.\nTip: Sử dụng Annotation cloud.google.com/load-balancer-type: \u0026quot;External\u0026quot; để chỉ định rõ loại LB. b. Config and plugin management done right 1. \u0026ldquo;Configuration as Code\u0026rdquo; với CRDs Kong cung cấp các Custom Resource Definitions (CRDs) như KongPlugin, KongIngress, KongConsumer. Hãy sử dụng chúng thay vì gọi Admin API bằng tay.\nLợi ích: Toàn bộ cấu hình được lưu trong Git, có thể review và audit (GitOps). 2. Scope của Plugin Hạn chế dùng Global Plugin (áp dụng cho toàn bộ cluster) trừ khi thực sự cần thiết (ví dụ: Log correlation ID). Hãy apply plugin ở mức Ingress hoặc Service cụ thể. Ví dụ: Service Payment cần Rate Limit khắt khe hơn Service Product Catalog.\n3. Centralized Logging Đừng để log \u0026ldquo;chết\u0026rdquo; trong pod. Hãy sử dụng plugin tcp-log, udp-log hoặc file-log để đẩy log về hệ thống giám sát tập trung. Trên GCP, bạn có thể đẩy thẳng về Google Cloud Logging (Stackdriver).\nc. Optimisation and security 1. Bảo vệ Kong Admin API Đây là nguyên tắc sống còn. Kong Admin API (mặc định port 8001/8444) cho phép thay đổi toàn bộ cấu hình Gateway.\nAction: Chỉ expose Admin API qua ClusterIP (nội bộ cluster). Nếu cần truy cập từ ngoài, hãy dùng kubectl port-forward hoặc bảo vệ nó bằng một Ingress riêng có Authentication cực mạnh. 2. Quản lý Secrets Không bao giờ hardcode API Key hay Certificate trong file cấu hình.\nDùng Kubernetes Secrets để lưu SSL Certificate. Tích hợp GCP Secret Manager với External Secrets Operator để sync secret vào K8s an toàn. 3. HTTPS Everywhere Sử dụng Cert-Manager kết hợp với Let\u0026rsquo;s Encrypt để tự động hóa việc cấp phát và gia hạn SSL Certificate cho tất cả các domain. Cấu hình Kong để force redirect HTTP sang HTTPS.\nd. CI/CD and DevOps automation Để đạt được sự linh hoạt tối đa, hãy áp dụng mô hình GitOps:\nSource Code: Chứa code ứng dụng và file values.yaml cho Helm Chart của ứng dụng. Infrastructure Repo: Chứa cấu hình Kong (Plugins, Consumers, Global Configs). CD Tool (ArgoCD/Flux): Tự động đồng bộ trạng thái từ Git vào Cluster. Khi bạn commit một file KongPlugin mới lên Git, ArgoCD sẽ tự động apply nó vào K8s. 4. Real production lessons Trong quá trình vận hành Kong trên GKE cho các hệ thống lớn, đây là những \u0026ldquo;chiến trường\u0026rdquo; mà chúng tôi đã đi qua:\nDB-less Mode không phải lúc nào cũng là “best practice”: DB-less có thể rất phù hợp với GitOps và triển khai bất biến (immutable deployments), nhưng nếu cấu hình không đúng (không có fallback declarative_config, không persist LMDB cache, chỉ chạy 1 replica), bạn có thể gặp rủi ro mất cấu hình sau sự cố hoặc khi pod restart. Hãy chọn mode theo nhu cầu (GitOps vs runtime changes vs enterprise scale), không nên mặc định “DB-less là chân ái” cho mọi bài toán.\nCẩn thận với Health Checks: Cấu hình Liveness và Readiness Probe cho Kong rất quan trọng. Nếu cấu hình sai, K8s sẽ restart Kong liên tục khi traffic tăng đột biến (do Kong xử lý chậm lại), gây ra thảm họa dây chuyền.\nPlugin Ordering: Thứ tự chạy của các Plugin rất quan trọng (ví dụ: Rate Limit phải chạy trước hay sau Auth?). Hãy nắm vững \u0026ldquo;Priority\u0026rdquo; của từng plugin để tránh các lỗ hổng logic.\nBest practices by use case (DB-less / PostgreSQL / Hybrid) Dưới đây là khung tham chiếu để chọn và vận hành Kong an toàn trong Production. Mục tiêu là: nêu rõ trade-off, giảm rủi ro mất cấu hình, và phù hợp cách deploy của team.\n1) DB-less Mode (suits GitOps / simple setups) kong.conf essentials:\ndatabase = off declarative_config = /kong-config/kong.yaml # ALWAYS set a fallback lmdb_map_size = 2048m # tune based on config size Gợi ý vận hành:\nLưu declarative config trong Git: version control cho kong.yaml (review/audit/rollback). Luôn set declarative_config: làm “phao cứu sinh” nếu LMDB cache bị mất. Persist LMDB bằng PersistentVolume: giúp survive pod restarts (đặc biệt khi bạn muốn khởi động ổn định, hạn chế phụ thuộc cache tạm). Validate trước khi deploy: kong config parse kong.yaml hoặc deck gateway validate. Chạy nhiều replica: không phụ thuộc 1 node duy nhất. Ví dụ Kubernetes (persist LMDB + mount declarative config):\nvolumes: - name: kong-cache persistentVolumeClaim: claimName: kong-lmdb-pvc # Persist dbless.lmdb - name: kong-config configMap: name: kong-declarative-config # Fallback config 2) PostgreSQL Mode (suits enterprise / runtime changes via API or UI) HA PostgreSQL: Patroni, CrunchyData, hoặc managed service. Read replicas (nếu cần): dùng read-only endpoint/host (tùy theo version/feature set) để offload reads. Connection pooler: PgBouncer khi có nhiều Kong nodes. Backup định kỳ + PITR (point-in-time recovery). Tune cache: db_update_frequency, db_cache_ttl (cần test theo traffic thực tế). 3) Hybrid Mode (best of both) Control Plane (có DB) + Data Planes (DB-less): CP quản lý config tập trung (PostgreSQL). DPs nhẹ, cache config từ CP. DPs có thể tiếp tục chạy khi CP outage nhờ LMDB cache local (tùy cách deploy). Decision framework Nếu bạn cần\u0026hellip; Nên dùng GitOps, immutable deployments DB-less Thay đổi config runtime qua API/UI PostgreSQL Large scale, enterprise features PostgreSQL hoac Hybrid Minimal ops overhead DB-less Cluster-wide rate limiting (phụ thuộc DB) PostgreSQL Vừa muốn central control vừa muốn nodes nhẹ Hybrid TL;DR DB-less an toàn nếu cấu hình đúng: persist LMDB + set declarative_config fallback + nhiều replicas. Nếu thiếu các safeguard này: đúng, cấu hình có thể bị mất sau crash/restart. Production lớn: ưu tiên Hybrid, hoặc PostgreSQL nếu cần runtime changes và các nhu cầu enterprise. 5. Conclusion Sử dụng Kong Open Source trên GKE mang lại sức mạnh kiểm soát traffic tuyệt vời mà Ingress mặc định khó so sánh được. Tuy nhiên, \u0026ldquo;sức mạnh lớn đi kèm trách nhiệm lớn\u0026rdquo;. Việc làm chủ Kong đòi hỏi team của bạn phải hiểu sâu về cách Kong hoạt động cũng như các mô hình quản lý cấu hình hiện đại như GitOps.\nTóm lại:\nChọn Kong nếu bạn cần Flexibility và Plugins. Chọn GCP Ingress nếu bạn cần sự Đơn giản và Ít vận hành. Hy vọng bài viết này giúp bạn có cái nhìn rõ ràng hơn cho kiến trúc hệ thống sắp tới của mình. Happy Coding!\nTham khảo:\nKong Official Documentation Kong Ingress Controller for Kubernetes ","permalink":"https://freepeak.github.io/posts/kong-opensource-k8s-gcp/","summary":"Why choose Kong over the default GCP Ingress? Architecture, the reasoning behind the choice, and real-world best practices for running Kong Open Source on GKE efficiently, securely and cost-effectively.","title":"Architecture and Best Practices: Running Kong Open Source as an API Gateway on GKE"},{"content":"Nếu muốn làm một hệ thống sập, đôi khi bạn không cần DDoS.\nChỉ cần một chỗ đứng đủ gần \u0026ldquo;cửa\u0026rdquo;.\nVà một cấu hình đủ ngây thơ.\nTrên AWS EKS, cái \u0026ldquo;cửa\u0026rdquo; đó thường là Ingress.\nCái ngây thơ thường là: \u0026ldquo;ALB Ingress là đủ rồi\u0026rdquo;.\nTôi cũng từng tin như vậy.\nCho đến khi bài toán không còn là routing nữa, mà là kiểm soát hành vi.\nCụ thể: rate limiting theo tenant.\nBạn có 1 API. Nhưng bạn có nhiều khách hàng. Một số khách hàng trả tiền nhiều. Một số trả tiền ít. Một số trả tiền rất ít, nhưng request rất nhiều.\nNếu bạn giới hạn theo IP, bạn đang phạt nhầm người vô tội (NAT gateway làm điều đó giúp bạn).\nNếu bạn giới hạn theo path, bạn đang giả vờ như tenant không tồn tại.\nBạn phải giới hạn theo identity: API key, JWT claim, consumer id… nói chung là theo \u0026ldquo;người đang gọi\u0026rdquo;, không phải theo \u0026ldquo;địa chỉ nhà họ\u0026rdquo;.\nĐến đây, ALB bắt đầu lộ vẻ mặt: tôi là load balancer, đừng bắt tôi làm gateway.\n0) Quick contents The real problem: rate limiting by identity Decision tree: ALB/WAF/Lambda vs Kong Why DB-less fits EKS The proposed EKS architecture Operational guardrails (best practices) A minimal configuration example Closing 1) The real problem: rate limiting by identity (not by IP) Nghe thì đơn giản: \u0026ldquo;giới hạn request\u0026rdquo;.\nNhưng trong các hệ thống SaaS nghiêm túc, giới hạn ở đâu và theo cái gì mới là phần làm bạn mất ngủ.\nVì gateway là một thứ giống như bệnh viện trong bài của vnhacker: ai cũng đi qua, dữ liệu/traffic cực nhạy cảm cứ thế chảy vào, còn bên trong thì không ai muốn nghĩ sâu.\nCho đến khi có sự cố.\nGiới hạn theo IP: dễ làm, nhưng sai mô hình. Một NAT gateway có thể làm bạn phạt nhầm cả công ty. Giới hạn theo path: cũng dễ làm, nhưng không phân biệt được tenant. Giới hạn theo identity: đúng bài. Nhưng identity lại đến từ nhiều nguồn: API key, JWT claim (sub/tenant_id), header do upstream inject, mTLS client cert\u0026hellip; Lúc này, thứ bạn cần không phải là “một con ALB đứng trước Ingress”. Bạn cần một điểm quyết định ở Layer 7, nơi request được hiểu theo ngữ nghĩa: ai gọi, gói nào, quota nào, và nếu vượt thì trả về 429 một cách nhất quán.\nNôm na: bạn cần API Gateway hơn là Ingress.\n2) An engineer decision tree: ALB/WAF/Lambda or a real gateway? Tôi từng đi qua đủ các nhánh, nên xin kể theo kiểu “đường nào cũng có giá”.\nBranch A: Squeeze the logic into ALB Ingress ALB Ingress mạnh ở chỗ: routing, TLS termination, integration với AWS ecosystem.\nNhưng khi bạn đòi rate limiting theo tenant, bạn sẽ bắt đầu thấy mình viết thêm thứ mà bạn không định vận hành:\nMột lớp “tự nhận diện tenant” (parse JWT, map API key -\u0026gt; tenant) Một nơi lưu state/quota (counter, window, redis/elasticache?) Một chỗ enforce + trả lỗi chuẩn Tức là bạn đang lắp một cái gateway bằng… các mảnh khác nhau.\nBranch B: Use AWS WAF WAF làm tốt một số thứ: IP reputation, bot control, rule matching.\nNhưng với rate limiting theo identity, WAF thường khiến bạn rơi vào 2 tình huống:\nQuy tắc trở nên khó đọc/khó quản: đội app không hiểu, đội platform ôm hết. Chi phí và vận hành bắt đầu tăng theo traffic, và mỗi thay đổi lại giống “xin phép một bộ phận khác”. WAF không tệ. Chỉ là nó không sinh ra để trở thành “quota engine theo tenant”.\nBranch C: Write a Lambda/custom authorizer Đây là con đường hấp dẫn nhất, vì nó cho bạn cảm giác “mình kiểm soát được hết”.\nNhưng nó cũng là con đường dễ biến thành nợ dài hạn:\nBạn tự chịu trách nhiệm latency (cold start, retry, error handling) Bạn tự xây observability (metrics, logs, correlation) Bạn tự định nghĩa chuẩn hành vi (429, headers, backoff hints) Tới một ngày, bạn nhận ra: bạn vừa dựng một API Gateway mini, nhưng không có đội ngũ của Kong.\nBranch D: Run Kong DB-less on EKS Kong giải đúng cái bạn đang thiếu: một gateway layer 7 với model “consumer + credential + plugin”.\nRate limiting là plugin, không phải dự án. Nhận diện tenant là cấu hình (theo key-auth/jwt/oauth2), không phải mỗi service tự chế. Hành vi thống nhất: 429, headers, policy, metrics. Và quan trọng: DB-less cho bạn một style vận hành phù hợp Kubernetes.\n3) DB-less on EKS: why this is usually the best practice Tôi không nói DB-less là “chân lý cho mọi trường hợp”. Nhưng trên EKS, với mục tiêu tối giản vận hành, DB-less gần như là lựa chọn mặc định tốt.\nDB-less nghĩa là: Kong không cần Postgres để lưu config runtime. Thay vào đó, config được cung cấp theo kiểu declarative. Trong Kubernetes, bạn kéo nó về “configuration as code”.\nLợi ích thực dụng:\nKhông phải vận hành Postgres HA chỉ để chứa config gateway. Kong proxy gần như stateless: scale ngang dễ, rollout rõ ràng. Hợp GitOps: config nằm trong Git, review được, audit được. Đổi lại, bạn chấp nhận:\nMuốn thay đổi config “nhanh bằng UI” thì không còn vui nữa; bạn commit Git rồi deploy. Nói thẳng: nếu bạn đã chạy EKS nghiêm túc, bạn nên thích cách làm này.\n4) The proposed AWS EKS architecture (minimal but not naive) Một kiến trúc “đủ tốt” và dễ mở rộng thường trông như sau:\nClient -\u0026gt; AWS ALB (L7) -\u0026gt; Kong Proxy (K8s Service) -\u0026gt; Services trong cluster\nMột số điểm chốt:\nALB vẫn có chỗ đứng: nó lo TLS/ACM, internet-facing LB, integration với AWS networking. Kong là nơi ra quyết định: auth, rate limit, request/response transform, logging. Services phía sau sạch sẽ hơn: code tập trung vào business logic. Nếu bạn muốn đi xa hơn (multi-tenant mạnh, internal traffic, east-west), bạn có thể tách:\nMột Kong “edge” cho traffic ngoài vào Một Kong “internal” cho traffic nội bộ (tùy mức độ phức tạp) Nhưng YAGNI: đừng làm hai con gateway chỉ vì… nhìn cho giống big tech.\n5) Guardrails for running Kong DB-less on EKS (best practices) Tôi gọi đây là guardrails, không phải best practices.\nVì best practices nghe như khuyến nghị.\nCòn guardrails là thứ bạn dựng lên để giảm blast radius khi có người (hoặc một script) đẩy nhầm config.\n5.1) Treat gateway config like product: GitOps and review before apply Nếu gateway là cửa ngõ, thì config của nó là “policy của công ty”.\nLưu toàn bộ Kong config trong Git. Mọi thay đổi đi qua PR review. Dùng ArgoCD/Flux để sync vào cluster. Đừng để gateway trở thành nơi “ai cũng có thể bấm” nhưng “không ai chịu trách nhiệm”.\n5.2) Lock the Admin API down (do not let it live as a public endpoint) Nguyên tắc sống còn:\nAdmin API để ClusterIP. Nếu cần thao tác khẩn: dùng kubectl port-forward + RBAC. Bạn có thể có 100 lớp auth cho traffic client. Nhưng nếu Admin API hở, thì coi như xong.\n5.3) Per-tenant rate limiting: pick a clear identity source Trước khi bật plugin, hãy trả lời một câu:\n“Tenant identity đến từ đâu?”\nAPI key -\u0026gt; map thẳng consumer JWT -\u0026gt; tiêu chuẩn hóa claim (ví dụ tenant_id) và enforce ở gateway Đừng để mỗi service tự parse theo một kiểu. Bạn sẽ không debug nổi khi có incident.\n5.4) Avoid global plugins unless you really need them Global plugin nhìn rất tiện. Nhưng nó cũng là cách nhanh nhất để:\nPhạt nhầm những route không nên bị phạt Tạo coupling vô hình giữa các team Best practice thường là:\nApply plugin theo Ingress/Service/Route cụ thể Chỉ globalize những thứ mang tính “hạ tầng” (correlation id, baseline logging) 5.5) Observability is not decoration Một gateway không có metrics/logs thì chỉ là một hộp đen biết trả 502.\nBật access logs có cấu trúc (JSON) và đẩy về hệ thống tập trung. Xuất metrics (Prometheus) và đặt alert cho 4 thứ: latency, 429 rate, 5xx rate, upstream timeouts. Rate limiting theo tenant mà không đo tenant nào bị 429 nhiều nhất thì… bạn chỉ đang đoán.\n6) A minimal configuration example, for shape Dưới đây là ví dụ dạng “minh họa ý tưởng” (tùy bạn đang dùng CRDs hay decK).\nMột consumer đại diện cho tenant Một credential (API key hoặc JWT) Một rate limiting plugin gắn vào route/service # Pseudo-example: illustrative, not a full manifest apiVersion: configuration.konghq.com/v1 kind: KongPlugin metadata: name: rl-tenant-basic config: minute: 120 policy: local plugin: rate-limiting Ghi chú thực dụng:\npolicy: local đơn giản nhất (đếm theo từng pod). Nếu bạn cần quota “toàn cụm” chính xác hơn, bạn sẽ cân nhắc Redis. Đừng tối ưu quá sớm. Hãy bắt đầu với cái bạn vận hành được. 7) Closing ALB Ingress là một default hợp lý. Nhưng nó không phải là API Gateway.\nKhi bài toán của bạn là rate limiting theo tenant, bạn đang chơi một trò khác: trò của identity, policy, và vận hành nhất quán.\nKong DB-less trên EKS là một lựa chọn thực dụng vì nó biến “logic ở cửa ngõ” thành cấu hình có kiểm soát: review được, audit được, rollout được.\nNếu phải chốt một câu để nhớ:\nGateway là nơi bạn quyết định ai được làm gì. Đừng biến quyết định đó thành một đống băng keo.\n","permalink":"https://freepeak.github.io/posts/kong-eks-aws-rate-limit-tenant/","summary":"If you need to rate limit by identity (tenant, API key, JWT claim) on EKS, ALB Ingress pushes you into a spiral of WAF, Lambda and custom code. Kong DB-less gives you one decision point at layer 7: plugins, consumers, config-as-code, GitOps. This post locks down the architecture and the deployment best practices.","title":"Kong DB-less on AWS EKS: Per-Tenant Rate Limiting, and Why ALB Ingress Is Not Enough"},{"content":"Ngày bắt đầu dựng AWS cho IaC platform, tôi tưởng network là phần “nhẹ đô”: tạo VPC, chia subnet, apply là xong.\nNhưng network có một đặc điểm rất kỳ: lúc mới làm thì nó trông như vài dòng config. Đến lúc bạn muốn mở rộng (VPN, peering, multi-account, kết nối on-prem, tách môi trường), tự nhiên mọi thứ quay lại cắn bạn.\nVà thứ hay cắn nhất lại là một dòng nhìn vô hại: CIDR của VPC.\nChanging a CIDR is not like changing a variable CIDR nhìn như một lựa chọn kỹ thuật thuần túy:\n10.0.0.0/16 Nhưng thực tế nó là ranh giới địa chỉ IP của cả “thành phố” hạ tầng bạn đang xây:\nsubnet public/private sinh ra từ đó route table, NAT gateway, VPC endpoints bám theo đó EKS nodes, pods, services… đều phải sống trong không gian đó peering / VPN / Transit Gateway phải thương lượng với CIDR đó Lúc mới dựng dev, bạn ít khi thấy vấn đề. Vẫn ping được, vẫn deploy được, vẫn lên cụm.\nRắc rối chỉ lộ ra khi bạn bắt đầu nối mạng thật: VPC-to-VPC, VPC-to-onprem, hoặc gom nhiều môi trường/tài khoản vào một hệ thống.\nTới đoạn đó, bạn mới bật ra câu hỏi: “Nếu CIDR đang dùng bị chồng lấp thì sao?”\nCâu trả lời thường là: thì mệt.\nBạn có thể thêm CIDR thứ hai, rồi dọn dẹp dần. Hoặc bạn tạo VPC mới và migrate. Dù chọn hướng nào, đây cũng không phải loại việc bạn muốn làm vào tuần mà team đang bận release.\nWhy CIDRs overlap Tôi thấy có hai nguyên nhân phổ biến:\nCopy-paste kinh điển AWS tutorial nào cũng có ví dụ 10.0.0.0/16. Bạn copy một lần thì không sao. Nhưng đến lúc bạn có 3 cái VPC, 5 môi trường, thêm một mạng office cũng dùng 10.0.0.0/16… thế là bắt đầu “đụng hàng”.\nKhông có “bảng địa chỉ” cấp tổ chức Mỗi team tự chọn một dải. Khi chưa kết nối liên mạng thì vẫn ổn. Khi cần nối thì phát hiện dải của nhau chồng lấp và không route được.\nCIDR chồng lấp không phải lỗi kỹ thuật phức tạp. Nó giống kiểu “lúc xây nhà quên chừa đường ống”. Quên xong rồi, sửa mới đau.\nLock the CIDR early, and lift it to project-level config Tôi không nghĩ CIDR là thứ cần “chọn cho đúng một lần là xong mãi mãi”. Nhưng tôi tin nó là thứ nên được quản trị như một quyết định nền tảng, thay vì nằm rải rác trong code.\nCách tôi làm (đơn giản, nhưng hiệu quả) là:\nKhai báo vpc_cidr ở project.hcl theo từng môi trường. Truyền nó xuống network/terragrunt.hcl bằng inputs. Vì sao quan trọng?\nCIDR trở thành “tham số cấp dự án”, review một phát là thấy. Mỗi môi trường có dải rõ ràng, ít biến thành “mystery meat config”. Về sau muốn thêm staging/prod/multi-account, bạn chỉ việc mở rộng bảng CIDR, tránh chồng lấp từ đầu. Tóm lại: thay vì để CIDR là một chi tiết implementation, tôi coi nó là một phần của hợp đồng kiến trúc.\nA small strategy for a big problem Nếu áp dụng cách nghĩ “chiến lược bắt đầu từ vấn đề”, thì vấn đề ở đây không phải là “chọn CIDR nào cho đẹp”.\nVấn đề là: về sau bạn sẽ phải kết nối nhiều thứ lại với nhau, và lúc đó CIDR sai sẽ làm bạn trả giá cao.\nChốt CIDR từ sớm không làm bạn deploy nhanh hơn hôm nay.\nNhưng nó giúp bạn khỏi phải chạy ngược lại vào ngày mai.\nIllustrations Sơ đồ 1: CIDR là nền cho mọi lớp hạ tầng Sơ đồ 2: Khi CIDR chồng lấp, đường nối bị “kẹt” ","permalink":"https://freepeak.github.io/posts/chot-vpc-cidr-tu-som/","summary":"A VPC CIDR looks like a small technical choice, but it is the foundation of the whole network. Here is why locking it in early and treating it as a project-level decision saves you from rebuilding when you scale.","title":"Lock In VPC CIDR Early: A Small Decision That Prevents Tearing It All Down Later"},{"content":" Lưu ý: Hình ảnh trong bài viết đang được cập nhật\nChào anh em, mình viết bài này để kể lại hành trình \u0026ldquo;chơi hệ AI agents\u0026rdquo; của một đứa làm dev đúng nghĩa: thử đủ kiểu tool, trả phí đủ nơi, bị limit đủ lần, rồi cuối cùng rút ra được một kết luận khá thực dụng: mỗi model/tool có điểm mạnh – yếu riêng, dùng đúng bài mới thấy đáng tiền.\nBài này mình kể theo trải nghiệm thật, và có kèm một bảng tổng hợp cuối bài để anh em dễ chọn.\n1) From ChatGPT to the era of unpaid labour, and the first paid seat Mình bắt đầu với ChatGPT ngay từ lúc nó vừa ra mắt. Lúc đó quá đã: hỏi gì cũng trả lời, viết email, tóm tắt, brainstorm… đủ thứ. Sau này nhu cầu tăng (và giới hạn cũng tăng), mình bắt đầu bước qua các tool chuyên code.\nCó giai đoạn mình \u0026ldquo;cày\u0026rdquo; bản free của Cursor bằng cách dùng… 5 email. Anh em nào từng trải chắc hiểu cảm giác hit limit rồi phải đổi account nó \u0026ldquo;hề\u0026rdquo; thế nào. Cuối cùng mình cũng xuống tiền $20/tháng cho Cursor.\n2) As projects grow, I need AI to do real work Mình không chỉ code mấy repo CRUD nhỏ. Công việc/side project của mình đi qua đủ loại mức độ phức tạp:\nCRUD/Monolith: API + DB + auth/roles + migration. EDA (Event-Driven Architecture): Kafka/NATS/RabbitMQ, retry, idempotency, message schema. MCP/MPC/Crypto: luồng ký/verify, chia sẻ bí mật, xử lý key material. PKI: certificate chain, CSR, rotation, trust store, policy. Càng lên mấy tầng này, mình càng quan tâm 3 thứ:\nQuality: code ra có đúng không, có \u0026ldquo;hợp não\u0026rdquo; không. Cycle prompt: bao nhiêu vòng hỏi-đáp để hoàn thành task. Stability/Quota: đang chạy mà bị limit thì mất flow cực kỳ. 3) Cursor + Opus 4.5: the most stable on quality and prompt cycle (but rate limited) Nếu nói thuần về \u0026ldquo;độ chắc tay\u0026rdquo; khi làm project lớn (đọc codebase rộng, bám ngữ cảnh dài, đụng nhiều ngôn ngữ), thì Cursor + Opus 4.5 là combo mình đánh giá ổn định nhất.\nVí dụ kiểu task mình hay giao:\n\u0026ldquo;Quét hết repo, tìm luồng auth hiện tại, thêm mTLS cho service-to-service\u0026rdquo; \u0026ldquo;Refactor module payment thành event-driven, đảm bảo idempotency và retry\u0026rdquo; \u0026ldquo;Đọc code crypto đang dùng, viết thêm verify path + test vectors\u0026rdquo; Với mấy task này, Opus thường giúp mình giảm số vòng prompt rất rõ: hỏi ít hơn, sửa ít hơn, và ít bị \u0026ldquo;lạc ngữ cảnh\u0026rdquo;.\nNhưng… điểm đau duy nhất: limit quá nhiều. Dùng kiểu dev cày 4–6 tiếng/ngày là sẽ dính cap, và mình không muốn mỗi lần vào guồng lại bị ngắt.\n4) Because of limits, I moved to GitHub Copilot + Antigravity (Google) Hiện tại mình dùng song song:\nGitHub Copilot: với cách tính credit cho Opus \u0026ldquo;dễ thở hơn\u0026rdquo; (mình đang dùng ở mức Opus chỉ tầm 3x credit) nên tổng chi phí dễ kiểm soát hơn. Antigravity (Google): mình dùng như một agent phụ trợ mạnh, nhưng kiểu limit theo chu kỳ 5h nên cần \u0026ldquo;xếp lịch\u0026rdquo; lúc nào dùng cho đáng. Thói quen của mình là:\nTask cần hoàn thành nhanh, có deadline, repo lớn: ưu tiên Cursor/Opus (nếu quota đủ). Khi quota gắt: đẩy qua Copilot để giữ nhịp code. Những đoạn cần agent đọc/điều phối nhiều bước (setup, thay config, chạy chuỗi thay đổi): dùng Antigravity (Google). 5) Claude Code: many plugins and features, but hard limits push team cost up Mình vẫn công nhận hệ Claude có nhiều điểm hay: ecosystem plugin/support khá rộng, trải nghiệm \u0026ldquo;nói chuyện để code\u0026rdquo; mượt. Nhưng vấn đề lớn nhất khiến mình không gắn bó lâu là limit usage mạnh, dẫn đến khi quy ra chi phí/khả năng cày, thì cảm giác đắt hơn các nhà khác (vì mình bị buộc phải tiết kiệm lượt).\n6) Small repos, simple tasks: GLM 4.7 is what I use most Với những tác vụ kiểu:\nrepo ít file thay đổi nhỏ thêm endpoint CRUD viết script migration đơn giản …thì mình luôn ưu tiên GLM 4.7. Lý do là nó nhanh, ra kết quả gọn, và mình không cần \u0026ldquo;đốt\u0026rdquo; Opus cho những việc không đòi hỏi chất lượng quá cao.\n7) Writing and research: give the right job to the right agent Mình chia use-case khá rõ:\nViết email, viết blog, soạn nội dung: mình dùng GPT vì văn phong linh hoạt, dễ chỉnh tone. Research/nghiên cứu: mình thường dùng Gemini và Grok vì nhanh, hợp kiểu quét thông tin nhiều hướng. 8) Summary table: which tool for which job Use case Tool/model mình hay dùng Vì sao hợp Điểm cần lưu ý Project lớn, đa ngôn ngữ, độ phức tạp cao (CRUD → EDA → MCP/MPC → PKI) Cursor + Opus 4.5 Chất lượng ổn định, ít vòng prompt, bám ngữ cảnh dài Limit dễ đứt mạch Giữ nhịp code hằng ngày, teamwork, IDE integration GitHub Copilot (Opus ~3x credit) Ổn định, tích hợp tốt, cost dễ kiểm soát hơn Vẫn cần prompt rõ khi làm kiến trúc Agent chạy đa bước, hỗ trợ điều phối task Antigravity (Google) Mạnh về agent flow, phù hợp việc dài hơi Limit theo chu kỳ 5h Repo nhỏ, task đơn giản GLM 4.7 Nhanh, gọn, đủ tốt cho việc nhẹ Không phải lúc nào cũng \u0026ldquo;deep\u0026rdquo; được Viết lách (mail/blog) GPT Dễ điều chỉnh giọng văn, bố cục tốt Cần fact-check kỹ Research đa chiều Gemini, Grok Quét nhanh, gợi ý hướng tìm Cần kiểm tra nguồn 9) Conclusion: to judge fairly, benchmark the same task Cuối cùng, cách mình thấy \u0026ldquo;công bằng\u0026rdquo; nhất để chọn tool/model là:\nlấy đúng một tác vụ, ví dụ \u0026ldquo;thêm auth + RBAC + audit log cho API\u0026rdquo; chạy lần lượt trên các model so sánh đầu ra: độ đúng, độ sạch, tốc độ, số vòng prompt Nói chung, mình không tin vào \u0026ldquo;model nào bá nhất\u0026rdquo; cho mọi thứ. Mình tin vào việc chọn đúng model cho đúng bài, và sẵn sàng đổi tool nếu bài toán đổi.\nNếu bạn muốn, bạn đưa mình 2–3 task bạn hay làm nhất, mình giúp bạn thiết kế một checklist benchmark để test các model một cách khách quan.\n","permalink":"https://freepeak.github.io/posts/hanh-trinh-su-dung-ai-agents/","summary":"A tour and comparison of the popular AI coding agents: from ChatGPT, Cursor and GitHub Copilot to Claude Code and GLM 4.7, judged on real work from CRUD apps through EDA, MCP/MPC and PKI.","title":"Using AI Coding Agents as a Developer: A 2026 Field Report"},{"content":"","permalink":"https://freepeak.github.io/tags/","summary":"tags","title":"Tags"}]