Rewrite both AI drafts in Neil's voice (still draft)

De-AI'd both posts per editorial review: cut negative parallelisms,
manufactured section-enders, twin sign-offs, and excess em-dashes; added
hedges and rough edges. Sign-offs now plain and distinct.

- Swamp: real Baby Buddy numbers (96 feedings / 65 sleeps / 4h46m); cut the
  fabricated vitamin-D-three-spellings anecdote, replaced with the true
  midnight/2-3am dosing cluster; honest about not yet having the Pebble
  watch/ring hardware; added the real fork surface (FCM push, audit fields,
  soft-delete timers, new reports)
- WiFi cert: added "what the write-kernel actually is" section; kept the
  parallel-agents-in-worktrees detail; framed the three-way write rule as his

Still draft: true — pending Neil's technical accuracy pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 22:11:20 -04:00
co-authored by Claude Opus 4.8
parent 6c338ada35
commit 736a10eb32
2 changed files with 194 additions and 191 deletions
+98 -100
View File
@@ -1,6 +1,6 @@
--- ---
title: "I Put My Son in a Swamp" title: "I Put My Son in a Swamp"
description: "I modeled my newborn as a typed cloud resource with a sync method, fifteen reports, and an idempotent delete. It is the fifth interface I have built for tracking one baby, and somewhere in the absurdity there is a real reason two sleep-deprived parents instrument everything." description: "I modeled my newborn as a typed resource with a sync method and a stack of reports. It's the newest of five ways I track one baby, and somewhere in the absurdity there's a real reason two exhausted people instrument everything."
date: 2026-07-11T00:45:00-04:00 date: 2026-07-11T00:45:00-04:00
draft: true draft: true
categories: ['automation', 'homelab'] categories: ['automation', 'homelab']
@@ -8,135 +8,133 @@ tags: ['swamp', 'babybuddy', 'parenting', 'home-assistant', 'mcp', 'pebble', 'de
--- ---
My son is a typed resource now. He has a `sync` method that pulls the last seven My son is a typed resource now. He has a `sync` method that pulls the last seven
days of his life into a versioned snapshot, fifteen reports that summarize how he days of his life into a versioned snapshot, a stack of reports that summarize how
slept and ate and — I want to be clear that I did think about this — a he slept and ate, and — I did think about this — a delete that's careful to be
`delete-entry` method that is careful to be idempotent, because deleting the same idempotent, because deleting the same diaper twice shouldn't be an error.
diaper twice should not be an error.
I did not set out to put a baby in a swamp. It just followed, the way these I did not set out to put a baby in a swamp. It followed, the way these things do,
things do, from already having put everything else in one. from having already put everything else in one.
## What "in a swamp" means ## What "in a swamp" means
[swamp](https://github.com/swamp-club/swamp) models real resources as typed [swamp](https://github.com/swamp-club/swamp) models real resources as typed
objects with methods — read them, mutate them — and records every run as objects with methods, and records every run as versioned data other models can
versioned data other models can reference. You write extensions in TypeScript. I read. You write extensions in TypeScript. I used it [last week to automate my home
used it [last week to automate my home certificate certificate authority]({{< ref "the-wifi-cert-that-didnt-exist-an-hour-ago" >}}),
authority]({{< ref "the-wifi-cert-that-didnt-exist-an-hour-ago" >}}), and the and the whole time I kept thinking about the thing I actually spend all night
whole time I kept thinking about the thing I actually spend all night touching, dealing with, which is not a certificate.
which is not a certificate.
We run [Baby Buddy](https://github.com/babybuddy/babybuddy) at home — a small, We run [Baby Buddy](https://github.com/babybuddy/babybuddy) at home — a small,
self-hosted app for tracking a newborn's feedings, diapers, sleep, pumping, self-hosted app for tracking a newborn's feedings, diapers, sleep, pumping, meds.
temperature, meds. It has a clean REST API. And a REST API, to a certain kind of It has a clean REST API. A clean REST API, at 2am, to a certain kind of brain, is
tired brain at 2 a.m., is an invitation. a dare.
So I wrote `@kneel/babybuddy`: one model that reads the whole instance into a So I wrote `@kneel/babybuddy`: one model that reads the whole instance into a
snapshot and writes new entries back, plus a pile of reports that run over that snapshot and writes new entries back, with a stack of reports that run over the
snapshot. Log a feeding, delete a mis-logged one, patch a bad timestamp. Then snapshot. Log a feeding, fix a mis-logged one, patch a bad timestamp, then ask it
ask it questions. questions.
## The reports tell on everyone ## The reports tell on you
The nice thing about summarizing a week of a baby is that the numbers are, on Summarizing a week of a newborn produces numbers that are, on their own, a little
their own, quietly insane. Ninety-nine feedings. Sixty-five separate sleep unhinged. In the last seven days: ninety-six feedings. Sixty-five separate sleep
sessions. Forty-four doses of medication. His longest unbroken stretch of sleep sessions. His longest unbroken stretch was four hours and forty-six minutes,
was four hours and forty-eight minutes, which I know to the minute because there which I know to the minute because there's a report called `sleep-longest-stretch`
is a report called `sleep-longest-stretch` and I run it the way other people and I run it the way other people check the weather.
check the weather.
But the report I did not expect to matter was the medication one. I built it to The one that got me was the medication report. I built it to answer "are we giving
answer "are we dosing the vitamin D on schedule," and instead it answered a the vitamin D on schedule," and it answered a question I hadn't asked, which is
question I had not asked, which is "how many different ways has Neil spelled "when does Neil actually remember the vitamin D." Not on schedule, it turns out.
vitamin D at midnight." The answer is three. `Vitamin D`, `Vitamin D Drops`, and It's a little cluster of timestamps at midnight, 2am, 3am — whenever I surfaced
`Vitamin D drops` are, as far as the database is concerned, three unrelated enough to think of it. The schedule in my head and the schedule in the database
substances, dosed on three overlapping schedules, by one man who was very tired. were not the same schedule, and only one of them keeps records.
That is the honest value of instrumenting anything. Not the dashboard. The moment That's the actual value of instrumenting anything: some night the dashboard hands
the dashboard shows you your own sloppiness back. you back a picture of yourself you weren't planning to volunteer.
## This is not the first interface. It is the fifth. ## This isn't the first interface. It's the fifth.
Here is the part that I think is genuinely unhinged, and I say that with Here's the genuinely stupid part, and I say that with affection for the man who
affection for the person who did it: the swamp extension is the *newest* way I did it: the swamp extension is the *newest* way I have to track this kid, not the
have to track this child, not the only one, and not by a wide margin. only one, and not by a wide margin.
There is a **mobile app** — a Capacitor wrapper around Baby Buddy with the sharp There's a **mobile app** — a Capacitor wrapper around Baby Buddy with the sharp
edges filed off. Biometric lock so a phone left on the couch doesn't show the edges filed down. Biometric lock, so a phone left on the couch doesn't show the
whole night's log. Siri intents, so "log a diaper" is a sentence you say into the whole night's log. Log-a-diaper you can say out loud, because your hands are full,
air with both hands full, which is the only state your hands are ever in. Live because your hands are always full. A running feed timer on the lock screen next
Activities, so a running feeding timer ticks on the lock screen next to the to the elapsed time you're trying not to look at. iOS and Android, because of
elapsed time you are trying not to look at. course iOS and Android.
There is a **Pebble watchapp**, because I have a Pebble again and it turns out the There's a **Pebble watchapp**, because the best possible interface for logging a
best interface for logging a feed at 3 a.m. is a physical button on your wrist feed at 3am is a physical button on your wrist that emits about four photons.
that does not emit a single photon more than necessary. Start a timer, stop it, Start a timer, stop it, pick the amount, done, without the wake-your-whole-nervous-
pick the amount, done — without the wake-up-your-whole-nervous-system brightness system glare of a phone. I'll admit the watchapp is currently ahead of my
of a phone. There is a Pebble smart *ring* on order for the same reason. I am not hardware — I don't have the watch yet. There's a ring interface too. I don't have
going to defend the ring. The ring defends itself. the ring yet either. I wrote software for two devices I'm still waiting on, which
tells you roughly everything about the state of mind here.
There is a **conversational server** — an MCP that lets me (or Claude, or the There's a **conversational server** — an MCP that lets me, or Claude, or the voice
voice assistant it hangs off of) just *say* what happened. "He ate ninety at assistant it hangs off of, just *say* what happened. "He ate ninety at eleven." It
eleven." It writes the same feeding record the app would, the same one the watch writes the same feeding record the app would, the same one the watch would, the
would, the same one the swamp model syncs back out. same one the swamp model syncs back out.
And there is **Home Assistant**, which is the layer that nags. At 10:30 it And there's **Home Assistant**, the layer that nags. At 10:30 it reminds whoever's
reminds whoever is still vertical to prep the overnight bottle. At 11 it reminds still upright to make the overnight bottle. At 11 it reminds us the vitamin D goes
us the vitamin D goes *in* that bottle — the vitamin D of at least three names. in it. If nobody acknowledges, it asks again. It's a very polite robot with a
If nobody acknowledges it, it asks again. It is, functionally, a very polite clipboard, and right now it has a better memory than either of us.
robot co-parent with a clipboard.
Five interfaces. One baby. Every single one of them a different door into the None of these sit on a stock Baby Buddy, either — the one underneath isn't stock
same little REST API. anymore. Somewhere in here I ended up bolting push notifications onto it via FCM,
adding per-record audit fields so I can see who logged what, soft-deleting timers
so a fat-fingered stop doesn't just vanish, and writing a handful of new reports.
"Track the baby" quietly became "re-platform the baby tracker," which is a
sentence I'd be embarrassed by if I'd slept.
## Why any of this Five interfaces. One baby. Every one of them a different door into the same little
REST API.
You could read all of that as a man building toys instead of sleeping, and you ## Why though
would not be entirely wrong. But there is a real thing underneath it, and it is
the least technical thing in this whole post.
A newborn is a 24-hour operation run by two people who are each getting about You could read all of this as a man building toys instead of sleeping and you
five hours of sleep in shifts. The single hardest part is not any one feeding. wouldn't be all the way wrong. But there's a real thing underneath it, and it's
It's the *handoff* — the 1 a.m. moment where one of us goes horizontal and the the least technical part of the post.
other picks up the watch, and the only way that works is if we are both looking
at the same set of facts. When did he last eat. How much. Is he due. Did the
vitamin D happen or did I dream that.
All of this tracking — the app, the watch, the voice, the nags, and now the A newborn is a 24-hour operation run by two people getting maybe five hours each,
swamp — is not quantified-self vanity. It is a shared source of truth for two in shifts. The hardest part isn't any single feeding. It's the handoff — the 1am
exhausted adults trying to hold one continuous story between them across a moment where one of us goes horizontal and the other picks up the watch, and that
shift change, in the dark, without waking each other up to ask. The only works if we're both looking at the same set of facts. When did he last eat.
instrumentation is the coordination layer. The baby is just where the data How much. Is he due. Did the vitamin D happen or did I dream it.
happens to come from.
All the tracking — app, watch, voice, nags, swamp — isn't quantified-self stuff.
It's a shared source of truth for two very tired adults trying to hold one
continuous story between them across a shift change, in the dark, without waking
each other up to ask. The baby is just where the data comes from.
## The bit worth keeping ## The bit worth keeping
If there's one engineering lesson that survived contact with a real newborn, it's If one engineering lesson survived contact with a real newborn, it's the same one
the same one from the certificate post, wearing a smaller hat: **data that claims from the [certificate post]({{< ref "the-wifi-cert-that-didnt-exist-an-hour-ago" >}})
to be complete when it isn't is worse than no data.** wearing a smaller hat: data that claims to be complete when it isn't is worse than
no data.
When I built the `sync` method, it capped each query at some number of records. A When I wrote the `sync` method it capped each query at some number of records. A
sensible default, until you imagine a parent syncing a long window, quietly sensible default until you picture a parent syncing a long window, quietly
losing the oldest entries, and getting a report that looks whole. So the snapshot dropping the oldest entries, and getting a summary that looks whole. So the
carries a `truncated` flag now an honest "there was more than this" and the snapshot carries a `truncated` flag now, an honest "there was more than this," and
summary says so out loud instead of lying with a clean-looking table. A review the summary says so out loud instead of lying with a tidy little table.
pass caught it before it shipped, which is the entire reason to have a review
pass.
The same instinct is why `delete-entry` swallows a 404 and calls it done. Two Same instinct is why the timers soft-delete instead of actually deleting, and why
parents, two devices, one baby — someone *will* delete the same thing twice. The stop-timer won't silently throw away a running one. Two parents, two devices, one
tool's job is to make the second delete boring, not to punish you for the racing baby: someone is going to stop the wrong thing or delete the same thing twice. The
condition of being a family. tool's job is to make that boring, not to punish you for the race condition of
being a family.
He's asleep right now. Twenty-two minutes in, per the timer on my wrist, which I He's asleep right now — nineteen minutes in, per the timer I am absolutely going
will not be checking again, because that would wake the report, which would wake to stop checking, because checking it wakes the report, which wakes me. The
me. The extension is [open source on the swamp extension's open source on the swamp registry if you also run Baby Buddy and also
registry](https://github.com/NeilHanlon/swamp-babybuddy) if you also run Baby cannot leave a working API alone.
Buddy and also cannot leave a working REST API alone.
--- ---
I do infrastructure and automation for a living through [my consulting I point these same instincts at other people's infrastructure through
practice](https://shrugpw.com) — the kind of instrumentation that is nominally [Shrug PW](https://shrugpw.com). The stakes are higher and the sleep is better.
for adults. The methodology, it turns out, is identical. There is just less
spit-up.
@@ -1,132 +1,137 @@
--- ---
title: "The WiFi Certificate That Didn't Exist an Hour Ago" title: "The WiFi Certificate That Didn't Exist an Hour Ago"
description: "I built a family of FreeIPA automation extensions for swamp, then used them to mint my laptop an 802.1X client certificate and walk onto the network — end to end, through code I'd just finished writing. A note on the write-safety pattern that made it safe." description: "I taught swamp to drive FreeIPA, then used it to issue my laptop an 802.1X client cert and get onto my own network. Mostly this is about knowing when to write things down."
date: 2026-07-10T18:30:00-04:00 date: 2026-07-10T18:30:00-04:00
draft: true draft: true
categories: ['infrastructure', 'automation'] categories: ['infrastructure', 'automation']
tags: ['freeipa', 'swamp', 'pki', '802.1x', 'eap-tls', 'radius', 'homelab', 'devops'] tags: ['freeipa', 'swamp', 'pki', '802.1x', 'eap-tls', 'radius', 'homelab', 'devops']
--- ---
My laptop is on WiFi right now with a client certificate that did not exist an My laptop is on WiFi right now on a certificate that didn't exist an hour ago. I
hour ago. I generated the keypair, my own certificate authority signed it, the made the keypair, my own CA signed it, the private key went into my secret store,
private key went straight into encrypted storage, and the machine walked onto and the machine authenticated onto the 802.1X network and pulled a DHCP lease —
the 802.1X network and pulled a DHCP lease — all through a tool I had just all through code I'd finished writing about twenty minutes earlier. I want to
finished writing. This is a short note on how that came together, and on the one talk about the boring part that made it safe, because the boring part is the only
piece of engineering I think is actually worth sharing. part I'd defend.
## The itch ## The itch
I run [FreeIPA](https://www.freeipa.org/) at home — my own Kerberos realm, LDAP I run [FreeIPA](https://www.freeipa.org/) at home. Kerberos realm, LDAP, a CA —
directory, and certificate authority, the same identity stack a lot of companies the same identity stack plenty of companies run, except mine is operated entirely
run in the large. It is a genuinely great piece of software. It is also, in my by hand. `kinit`, the `ipa` CLI, a shell script when I'm feeling fancy. Every
setup, operated entirely by hand: `kinit`, the `ipa` CLI, a shell script when I cert, every group, every bit of it is me typing commands and hoping I remembered
am feeling fancy. Every user certificate, every group, every bit of it is a the flags. It works, and it's also exactly the sort of thing I'll fat-finger at
human typing commands and hoping they remembered the flags. 11pm.
I wanted it declarative. I wanted to describe an identity object and have I wanted it declarative: describe an identity object, have something reconcile
something reconcile it, keep a versioned record of what happened, and let me it, keep a record of what it did, and let me wire one piece of the homelab into
wire one piece of infrastructure into the next. the next.
## What swamp is, briefly ## swamp, quickly
[swamp](https://github.com/swamp-club/swamp) models real resources as typed [swamp](https://github.com/swamp-club/swamp) models real resources as typed
objects with methods — create, read, mutate — and captures every run as objects with methods — create, read, mutate — and keeps every run as versioned
versioned data you can reference from other models. You extend it in TypeScript. data other models can read. You write extensions in TypeScript. It's the good
Think of it as the good parts of infrastructure-as-code without pretending the parts of infrastructure-as-code without the pretense that the world holds still.
world is a static graph. FreeIPA had no coverage in the registry, so this was FreeIPA had nothing in the registry, so I got to start from scratch, which is my
greenfield. favorite way to start.
## A family, not a monolith ## Not one model, a family
FreeIPA is a platform, not a service, so I split the work into a family of small FreeIPA is a platform, not a service, so I split it into a few small packages
packages rather than one god-model. The first, `@shrug/freeipa/domain`, is instead of one enormous one. `@shrug/freeipa/domain` is read-only: it logs into
read-only: it logs into the JSON-RPC API and snapshots the realm, the server the JSON-RPC API and snapshots the realm, the server inventory, the replication
inventory, and the replication topology. "Understand the domain" before you topology. Understand the domain before you touch it.
touch it.
Then the interesting ones, the packages that *write*: `user`, `group`, and Then the ones that write: `user`, `group`, and `cert`. Certs got their own
`cert`. I split certificates into their own package on purpose — in FreeIPA a package on purpose — in FreeIPA a certificate can belong to a user, a host, or a
certificate can be issued to a user, a host, or a service, so binding cert service, so hanging cert issuance off the "user" model would have been wrong the
issuance to the "user" model would have been wrong the moment I wanted a device second I wanted a device cert. (I wanted a device cert.)
cert.
I built the three write packages in parallel three agents, three repositories, I built the three write packages at the same time, in parallel: three agents,
each mirroring the structure of the read-only one and each carrying a each in its own git worktree, each cloning the shape of the read-only package and
byte-for-byte identical "write-kernel." That shared kernel is where the real each carrying an identical copy of the same "write-kernel."
thinking went.
## The part worth sharing: how do you safely persist a mutation? ### What the write-kernel actually is
Here is a question that sounds trivial and is not: when a mutating operation That last bit needs unpacking, because "byte-for-byte identical write-kernel" is
runs, *when* do you write down what happened? the kind of phrase that sounds like it means something and might not. It's not a
shared library — each package vendors its own copy. It's a small module that
every write goes through: it takes the mutation you want, runs it, and decides
what gets persisted and when. Same code, same rules, in all three packages, so
"how do we write things down" gets answered once and copied, not reinvented three
times slightly differently. Why it's worth copying is the next section.
The default guidance — swamp's, and honestly most people's instinct — is **throw ## The actual point: when do you write it down?
before you write**. If the operation fails, persist nothing, because a stored
record that claims success the world did not deliver is a lie that downstream
consumers will read and act on. That rule is correct, and I kept it. Mostly.
Because two cases quietly break its assumption. Here's a question that sounds trivial and isn't: when a mutating operation runs,
*when* do you record what happened?
**Irreplaceable material.** When I issue a certificate, the model generates a swamp's default guidance — and honestly most people's reflex — is throw before
fresh private key, sends the signing request, and the CA signs it. The you write. If the op fails, persist nothing, because a stored record claiming a
certificate now *exists*. If any step after that throws and I had followed "throw success the world never delivered is a lie, and downstream models will read that
before writing," I would lose the only private key that matches a certificate lie and act on it. That's correct, and I kept it. But it quietly assumes every
that is already real. Here the safe-sounding rule is actively destructive. The kind of "what happened" wants to be written at the same instant, and two of them
right principle is the opposite: persist irreplaceable state the instant it don't.
becomes real, before anything else can fail.
**Partial failure.** One of the operations creates a steering group as *both* a The first is **irreplaceable material.** When I issue a cert, the model generates
user-group and a host-group — two independent calls. If the first lands and the a fresh private key, sends the signing request, and the CA signs it. The cert
second errors, "it failed" is true but useless; what you need to know is which exists now. If something after that throws and I'd followed "throw before
half exists so you can reconcile it. writing," I'd have thrown away the only private key that matches a certificate
that's already, really, out in the world. So that one flips: write the
irreplaceable thing the moment it's real, before anything else gets a chance to
fail.
So the kernel follows a three-way rule instead of a slogan: The second is **partial failure.** One operation creates a steering group as both
a user-group and a host-group — two separate calls. If the first lands and the
second blows up, "it failed" is technically true and completely useless. What I
need to know is which half exists, so I can go fix the other one.
So the kernel follows three rules instead of a slogan:
1. **State** — the object itself — is written only on success. A failed mutation 1. **State** — the object itself — is written only on success. A failed mutation
never publishes misleading state. (The original rule, intact.) never publishes state that says otherwise. (The original rule, untouched.)
2. **Irreplaceable material** — a generated key, a signed cert — is persisted the 2. **Irreplaceable material** — a generated key, a signed cert — is written the
moment it is real, before any later throw can eat it. moment it's real, before any later throw can eat it.
3. An **audit record** is written on *both* paths, success and failure. It is 3. An **audit record** is written on *both* paths, success and failure. It's
telemetry, not state; when it says `success: false` it is telling the truth, telemetry, not state; when it says `success: false` it's telling the truth, so
so persisting it on failure violates nothing. writing it on failure isn't lying about anything.
The slogan is right until the domain proves it wrong in a specific, nameable That's the whole idea. "Only write on success" is a fine default right up until
way. Then you deviate on purpose, and you write down why. the domain hands you a case where it's actively wrong — and then you break it on
purpose and leave a comment saying why.
## The payoff ## The payoff, which is deliberately boring
With that in place the actual automation is almost boring, which is the goal. With that in place the rest is unglamorous, which was the goal. One method makes
One method idempotently ensures the VLAN-steering group exists as both a sure the VLAN-steering group exists as both group types and shrugs off "already
user-group and a host-group, swallowing "already exists" so re-runs are safe. exists" so I can re-run it. Another generates an RSA keypair and a PKCS#10 request
Another generates an RSA keypair and a PKCS#10 request in-process, submits it to in process, hands it to the CA, and stashes the resulting private key encrypted in
the CA, and stores the resulting private key **encrypted in my secret store** my secret store — never in plaintext, never in the model's own data.
never in plaintext, never in the model's own data.
Then the last mile, by hand because it should be: pull the signed certificate Then the last mile, by hand, because it should be: pull the signed cert and key
and the key out to files, point NetworkManager's EAP-TLS profile at them, and out to files, point NetworkManager's EAP-TLS profile at them, bring the interface
bring the interface up. The RADIUS server validated the certificate against my up. RADIUS checked the cert against my CA, said yes, dropped me on a VLAN, and
CA, said yes, dropped me on a VLAN, and DHCP did the rest. I was online — on an DHCP did the rest. Online — on an SSID with a potato in the name, because it's my
SSID with a potato in the name, because this is *my* network and I make the network and I get to name the SSID.
rules.
A certificate that did not exist when I started the afternoon, issued by A certificate that didn't exist when I sat down, issued by infrastructure I
infrastructure I automated the same afternoon, carrying a laptop onto the automated the same afternoon, carrying my laptop onto the network. Loop closed.
network. That is the whole loop, closed.
## Why bother ## Why bother
I could have typed the `ipa` commands. I have, many times. But automating a thing I could have typed the `ipa` commands. I have, a hundred times. But automating
is how you are forced to actually understand it — every flag you were cargo-culting, something is how you're forced to actually understand it — every flag you'd been
every failure mode you were getting away with ignoring. The write-safety pattern copy-pasting on faith, every failure mode you'd been quietly getting away with.
above did not come from wanting clever code; it came from asking "what happens if The write-safety stuff up top didn't come from wanting clever code. It came from
this throws *here*" until the answer stopped being "you lose your private key." asking "what happens if this throws *here*" over and over until the answer
stopped being "you lose the private key."
The packages are open source on the swamp registry. If you run FreeIPA and want The packages are open source on the swamp registry. If you run FreeIPA and want
to stop operating it by hand — or if you just like arguing about when to persist to stop hand-driving it — or you just like arguing about when to persist a
a failure — come find me. failure — come find me.
--- ---
I do this kind of infrastructure and automation work through This is what I do for money too, through [Shrug PW](https://shrugpw.com): bigger
[my consulting practice](https://shrugpw.com), and I am around the usual places networks, fewer potatoes.
on the internet.