The Wrong Front Door: Split DNS, DHCP Leases, and Verifiable Local Testing

๐Ÿค– Read Raw Markdown โ€ข ๐Ÿ“„ Google Doc (Try: Tools/Audio/Listen to document summary)

Setting the Stage: Context for the Curious Book Reader

Context for the Curious Book Reader: In our ongoing journey to build reliable, reproducible software systems, we often encounter the friction point where public internet tooling collides with home-lab reality. Having hardened deployment pipelines and automated verification in previous chapters, this entry tackles the classic hairpin NAT problem: trying to run a strangerโ€™s public install command against a self-hosted domain from inside the local network. Rather than relying on brute-force firewall overrides or insecure bypasses, this methodology walks through a clean bisection of the network stackโ€”from curlโ€™s connection flags to pfSense split DNS and DHCP lease mechanicsโ€”turning an irritating local networking glitch into a checkable, replayable receipt.

TL;DR: A one-line installer served from a home-hosted domain, curl -fsSL https://npvg.org | bash, failed with TLS errors when run from inside the same home network, on Linux and on macOS. The certificate was not the problem. From inside the network the domain resolved to the homeโ€™s public address, and the pfSense router answered that address itself, with its admin page and self-signed certificate, instead of forwarding to the web server behind it. A temporary workaround (curl --connect-to) reached the server by its LAN address while keeping certificate verification on. The lasting fix was a split-DNS entry on the router, which at first appeared to do nothing, because the routerโ€™s DHCP server was handing every device Google and Cloudflare as its resolvers, so no device ever asked the router. Clearing those two DHCP fields and renewing each deviceโ€™s lease made the unmodified command work on Linux, macOS, and an iPhone on Wi-Fi. Each step was checked with the same commands run before and after, including querying every resolver on the list directly by address.


Technical Journal Entry Begins

๐Ÿ”— Verified Pipulate Commits:

MikeLev.in: Alright, letโ€™s get this installer working. Now the high friction part now is that https://npvg.org is home-hosted behind a pfSense router and it has this default setting that causes the security alert warnings if you try to visit a home-hosted secure site from behind that same LANโ€ฆ well, this on Mac:

michaellevin@MichaelMacBook-Pro ~ % curl -fsSL https://npvg.org | bash
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
michaellevin@MichaelMacBook-Pro ~ %

And this on Linux:

(nix) ~ $ curl -fsSL https://npvg.org | bash
curl: (60) SSL: no alternative certificate subject name matches target hostname 'npvg.org'
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.
(nix) ~ $

Now I want the 80/20-rule solution. I know I can change the router setting but I donโ€™t want to go messing around with that. Iโ€™ve got an open mind to it though if it is really easy and does turn out to be the light-touch path. But I realistically do need to test this from inside my LAN.

Two Errors, One Wrong Door: Diagnosing the Perimeter

Fable 5.1 Extra: Context verified. No live receipts this compile; the only readings are the two terminal pastes in the prompt, and the Codebase shows the installer as it stands after 7118cb5d, brace group and all. Nothing below is a measurement until the probes run.

Two error messages, one wrong door. Both machines did exactly what a strangerโ€™s machine does: asked DNS where npvg.org lives, got the public address, and knocked. The difference is where the knock came from. A stranger knocks from the street, and the router forwards the visitor down the hall to honeybot. You knocked from inside the house, on your own front door, and the router does not forward that. It answers it. What answered is pfSenseโ€™s own admin page, the same thing that returned yesterdayโ€™s 301 with X-Frame-Options: SAMEORIGIN on port 80. On port 443 that page presents the routerโ€™s own certificate, signed by the routerโ€™s own private CA, with the routerโ€™s own name on it.

The two messages are that one certificate failing two different checks. curl asks its questions in order: first, do I trust whoever signed this? Second, is the name on it the name I asked for? The Mac stopped at the first question. The Linux box passed the first, which says its trust store accepts the routerโ€™s issuer (the likeliest reading: the routerโ€™s CA was installed there at some point to quiet the admin page), and stopped at the second, because the name on the certificate is the routerโ€™s and not npvg.org. Different last words, same wrong door. Probe 1 settles this in about five lines by printing the subject and issuer of whatever certificate npvg.org hands your desk right now. Router names mean the diagnosis holds. npvg.org and Letโ€™s Encrypt with โ€œverify okโ€ mean it does not, and the next turn starts over.

The 80/20, and it is zero router edits: tell curl the room number and keep the ID check. curl has a flag for exactly this: --connect-to npvg.org:443:<honeybot's LAN address>:443. It means โ€œwhen I ask for npvg.org on 443, walk to this address instead of asking the phone book, but keep asking for npvg.org by name.โ€ The name on the request, the name in the TLS handshake, and the name checked against the certificate all stay npvg.org, so honeybot serves the real Letโ€™s Encrypt certificate and curl verifies it normally. You already use the same trick every compile: the loopback probeโ€™s --resolve npvg.org:443:127.0.0.1 is this move aimed at honeybotโ€™s own loopback. This is the same move aimed across the LAN.

For the New-B, the line that must not blur: this is not -k. The -k (--insecure) flag turns the ID check off. --connect-to leaves it on and only changes the walk. Never pipe curl -k into bash, ever, not even once to โ€œjust see.โ€ Yesterdayโ€™s cellular tests proved that a strangerโ€™s curl verifies this certificate; todayโ€™s test should be held to the same standard from inside the house, and the flag lets it.

What the LAN test proves and what it skips. It proves the exact bytes strangers get, over real TLS, against the real certificate, and it exercises every fetch the installer itself makes, because those go to GitHub, pipulate.com, and Determinate, which are outside the house and unaffected by the door problem. The one hop it skips is the routerโ€™s port forward, and that hop was witnessed from the phone twice yesterday: HTTP 200 at 06:32 and HTTPS 200 at 09:09, Wi-Fi off. Inside-with-address plus phone-from-outside cover the whole path between them. The one thing neither can do is a complete install from a network that is not yours; that needs a second location and is a different day.

The Light-Touch Bypass: Curl Connect-To Over NAT Reflection

If you do want the router change, this is the light-touch one, and it is not NAT. Split DNS: teach the routerโ€™s DNS resolver that npvg.org lives at honeybotโ€™s LAN address. One screen, Services โ†’ DNS Resolver โ†’ Host Overrides, one entry plus www as an additional name, Save, Apply. Every device that asks the router for DNS then walks straight to honeybot, gets the real certificate, and needs no flag: the Mac, the phone on Wi-Fi, a browser on your desk. Nothing outside the house changes, and certificate renewal is untouched because Letโ€™s Encrypt validates from outside. pfSenseโ€™s own documentation recommends this over NAT reflection. The gate is probe 2: if your workstation asks some other resolver, the override is inert for that machine, still useful for the others, and the flag stays your deskโ€™s answer.

The heavier option, named so it is not mistaken for the light one. NAT reflection (System โ†’ Advanced โ†’ Firewall & NAT โ†’ โ€œPure NATโ€) makes the router forward inside knocks the way it forwards outside ones. It is global for every port forward, and it carries a trap for exactly your layout: with the workstation and honeybot on the same LAN, honeybot answers you directly with its own address as the sender, your machine was expecting a reply from the public address, and the handshake dies as a silent timeout rather than an error. The second checkbox, โ€œEnable automatic outbound NAT for Reflection,โ€ exists to prevent that. Two boxes, global scope, a failure mode that prints nothing. Split DNS does the same job for you with none of that.

One hazard before you run the real thing. Run the installer from a terminal that has not entered nix develop. The workshop shell front-loads its own openssl and glibc into LD_LIBRARY_PATH, the wrapper that protects nix is a shell function children never inherit, and on 2026-08-01 that killed the nix binary with โ€œversion not foundโ€ inside a workshop while the identical command would have succeeded for a stranger. A stranger has a clean shell; the test should too. Give it a throwaway name so the result is deletable: bash -s npvgtest lands in ~/npvgtest, which must not already exist. Once the banner reads โ€œWelcome to the Npvgtest Installer,โ€ the download completes, and the key fetch reports saved, the question this ride asked is answered; the nix develop handoff after that is yesterdayโ€™s proven lane. Small debt noticed while reading, not for this ride: the installerโ€™s own re-run instructions name pipulate.com, so a stranger arriving by npvg.org who hits the Nix-not-found branch is pointed at the other door. Same bytes behind both, so harmless.


1. PROBES

curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
getent hosts npvg.org
sha256sum assets/installer/install.sh
HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum

Probe 1 is the diagnosis and the only place -k appears: it reads the certificate to /dev/null and executes nothing. Router names in subject and issuer confirm the wrong door; npvg.org and Letโ€™s Encrypt refute it. Probe 2 is the split-DNS straddle: the public address before, honeybotโ€™s LAN address after, if Car 2 is ridden and this machine asks the router. Probes 3 and 4 are a gate, not a straddle: one digest in both worlds, because the --connect-to path needs no change to work. If probe 4 prints e3b0c442โ€ฆ that is the digest of empty input, meaning curl failed and the error is on the line above it; the likely cause is ssh -G returning an alias with no HostName set, which curl then cannot resolve.

2. NEXT CONTEXT

/home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
assets/installer/install.sh
nixops.sh
remotes/honeybot/nixos/configuration.nix
! curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
! getent hosts npvg.org
! sha256sum assets/installer/install.sh
! HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum

3. PATCHES

No repo patches required. Two cars, both actuators, both yours to ride or veto.

Car 1, the strangerโ€™s run from inside the house. Fresh terminal, not the workshop shell, home directory, no ~/npvgtest present. Paste the whole terminal output into the next prompt; the last line the installer printed is the reading.

HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --connect-to "npvg.org:443:${HB}:443" https://npvg.org | bash -s npvgtest

Cleanup when done: rm -rf ~/npvgtest. No ignition beyond the run itself; the run is the witness.

Car 2, optional, the router override. A setting outside the repo, so its steps ride in section 5. Its ignition is pfSenseโ€™s Apply Changes, which restarts the resolver; if the workstation runs systemd-resolved, resolvectl flush-caches before re-reading probe 2, or the old answer rides out its TTL and the AFTER reads like the BEFORE.

4. PROMPT

Rule from the LIVE RECEIPTS. Probe 1 is the diagnosis: subject and issuer naming the router and its private CA means the two-errors-one-door reading holds; subject npvg.org, issuer Let's Encrypt, and "verify ok" means the workstation already reaches honeybot by name, and probe 2 says whether that is Car 2 landing or the diagnosis being wrong. Probe 2: the public address (redacted in the receipt) means DNS still points at the front door; honeybot's LAN address means split DNS is live for this machine. Probes 3 and 4 are a gate, not a straddle: one digest in both worlds, or probe 4 prints the empty-input digest e3b0c442 with curl's error above it, and the fault is the address derivation, not the server. Then read the operator's pasted Car 1 transcript as the CVR it is: name the last line the installer printed, whether the banner said Npvgtest, whether the download and the key fetch completed, and whether the handoff into nix develop happened; a stop anywhere is a finding with a line, not a failed ride. Do not call the installer proven for strangers: this run tested the bytes, the certificate, and the script's own outbound fetches from inside the LAN, and the router's forward was witnessed only by the phone. No repo patches unless a receipt convicts one. Continue the article for the New-B and close with the five-stage response contract.

5. EXTERNAL DELIVERABLES

A. The Mac form of the test (manual paste). The Mac may not carry the honeybot ssh alias, so the address is filled by hand: it is the host part of nixops.shโ€™s TARGET line, or whatever probe 4โ€™s ssh -G prints on the workstation. Appleโ€™s curl supports --connect-to.

HB=<honeybot LAN address>; curl -fsSL --connect-to "npvg.org:443:${HB}:443" https://npvg.org | bash

B. pfSense split DNS (Car 2, optional, manual clicks).

  1. Services โ†’ DNS Resolver โ†’ General Settings, scroll to Host Overrides, click Add. (If the router runs the DNS Forwarder instead, the same Host Overrides table lives under Services โ†’ DNS Forwarder.)
  2. Host: leave blank. Domain: npvg.org. IP Address: honeybotโ€™s LAN address, the same value as HB above.
  3. Under Additional Names for this Host, add Host www, Domain npvg.org.
  4. Save, then Apply Changes.

It affects only devices that ask the router for DNS. It does not touch the port forward, the DDNS unit, or certificate renewal. Do not enable NAT reflection as part of this; if you ever do, it is the two-checkbox form with โ€œautomatic outbound NAT for Reflectionโ€ included, for the same-subnet reason named above.

MikeLev.in:

Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.

1: Probe: (BEFORE: hand-run, nothing changed yet)

Probe 1 on Linux in a non-nix developed shell:

(sys) pipulate $ curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
getent hosts npvg.org
sha256sum assets/installer/install.sh
HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
*  subject: O=pfSense webConfigurator Self-Signed Certificate; CN=pfSense-6808ceb59b08f
*  issuer: O=pfSense webConfigurator Self-Signed Certificate; CN=pfSense-6808ceb59b08f
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
< HTTP/2 200 
< x-frame-options: SAMEORIGIN
[REDACTED_IP]   npvg.org
59244b08613cd834d089f3bffa88b7c2cfcf8b40a8f3bbb80fde37fdc04feffc  assets/installer/install.sh
59244b08613cd834d089f3bffa88b7c2cfcf8b40a8f3bbb80fde37fdc04feffc  -
(sys) pipulate $ 

And probe 1 again from Mac:

michaellevin@MichaelMacBook-Pro ~ % curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
getent hosts npvg.org
sha256sum assets/installer/install.sh
HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum
* (304) (IN), TLS handshake, CERT verify (15):
*  subject: O=pfSense webConfigurator Self-Signed Certificate; CN=pfSense-6808ceb59b08f
*  issuer: O=pfSense webConfigurator Self-Signed Certificate; CN=pfSense-6808ceb59b08f
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
< HTTP/2 200 
< x-frame-options: SAMEORIGIN
zsh: command not found: getent
sha256sum: assets/installer/install.sh: No such file or directory
curl: (6) Could not resolve host: honeybot
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  -
michaellevin@MichaelMacBook-Pro ~ %

2: Context: (AFTER: the same probes re-run by the compiler as ! lines)

# AD HOC CHOP! The Not-Managed-by-Git Safe-for-Client-Data place. Insert Simpson Couch Gag in white space  below (explain anything to the audience you feel needs it explained)G
# adhoc.txt    _   _   _               ____ _   _  ___  ____  _   
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |  Trying to get the installer to work inside my LAN.
# ahe/ _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |  
# ahc ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|  
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)  

# To Human: WELCOME TO VIM! It's really NeoVim but I say vim anyway.
# To AI: This is an alternate 40K view to the big book-ore rolling pin spine.

# 1. **Probe**: Baseline Reading
# 2. **Context**: Post-experiment *planned* reading instructions
# 3. **Patch**: The experiment and how to make it happen
# 4. **Prompt**: Post-experiment instructions and how to read results
# 5. **Deliverable**: How the world is forever different moving forward

# The first thing you need to know here is that everything that comes after the
# hash symbol (#) is commented out โ€” and that's EVERYTHING in this file's default
# state. Begin editing-in lines for inclusion as part of the context or adding
# chunks of new context at the bottom. `Ctrl`+`v`, `j` (repeatedly), `l` (to move
# right), `d` (to delete). Reverse that with `Ctrl`+`v`, `j` (repeatedly),
# `Shift`+`i`, `# `, `Esc` to put the hashes back. You can just arrow-key around
# here with `h`, `j`, `k`, `l`. Save-and-quit is a bit tricky because another
# file is also loaded: `Esc`, `:`, `q`, `w`, `!`

# If this is stressing you out and you're a quitter and want to quit, just type:
# `Esc`, `:`, `q`, `!`, `Enter`. That will exit without saving any changes. If
# you want to get over this hump, type: `Esc`, `:`, `T`, `u`, `t`, `o`, `r`, `Enter`.

# This file is just to make it easy having options of what to edit into context.
# You can use whatever text-file you want to stack file-names and commands to
# build an output text-file with the identically stacked output of each file or
# command. In this way we vertically append or "stack" a bunch of text; simple as
# that. If you understand this concept, you're on your way to future-proofing
# yourself in the Age of AI. Congratulations! Here is how to include web pages:

#    !URL  --------------------------------------------------------------------
#      when    Public page; what a stranger or crawler sees; the BEFORE of a
#              login-wall diagnosis
#      switch  It shows a login page -> `warm URL` once, then `?URL`
#    
#    ?URL  --------------------------------------------------------------------
#      when    Anything behind a login, on the site's persistent profile;
#              `check URL` first
#      switch  The lenses show a shell (nav, an `[Iframe]` leaf, no content) ->
#              read the wire truth for the XHR the frame makes, then call that
#              API with a connector
#    
#    @URL  --------------------------------------------------------------------
#      when    Every re-read of a page already scraped; no browser, no network
#      switch  The cached page is stale or was a login wall -> fresh `!` or `?`
#    
#    $URL  --------------------------------------------------------------------
#      when    Exact markup: meta tags, a JSON blob in a `<script>`
#      note    Token-heavy; needs a prior scrape
#    
#    %URL  --------------------------------------------------------------------
#      when    The network log distilled; SPA endpoint discovery
#      switch  It re-serves the wire truth you already have -> the API
#    
#    ! cmd  -------------------------------------------------------------------
#      when    Any bounded, non-interactive command as a live receipt
#      note    Cap it with `-n`; no aliases, no prompts
#    
#    Connector  ---------------------------------------------------------------
#      when    The number you want is one GET away
#      switch  LIST until the thing isn't in the list -> FETCH by id -> DRILL
#              the path the app's own frame called -> `--grep` to narrow a list
#              or find a leaf

# Every step is one argument longer than the last; the moment a lens shows less than the wire does is the moment to stop scraping.

# STORYTELLING: OPTIONAL, BIG 40K-FOOT VIEW
# ! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs  # <-- ROLLING PIN that gives the 40K foot book-spine view of book-ore (only works for me because of local-only git repo)
# GLOSSARY.md                 # <-- Terms defined like back of J.R.R. Tolkien book, grows as `prompt_foo.py` gets scars and shrinks.
# scripts/articles/lsa.py     # <-- 2ND BRAIN: Search external memory with `rgx`, `rgxc` & `posts` Blogging for Hackers Jekyll-compatible.
# ~/repos/nixos/autognome.py  # <-- Letting the AIs really understand my environment (The Brave Little Tailor punches above Their Weight Class proving the dunning-kruger effect the gate-keeper's (lower-case) lament.)
 
# STILL BIG BUT LESS OPTIONAL (especially flake.nix)
# init.lua                    # <-- Daily driver hot-keys that overlap with aliases in flake.nix. `<leader>m` makes it Science (this process)!
# flake.nix                   # <-- THE ONE BIG THING TO INCLUDE Infrastructure as Code (IaC) tells LLM about your system down to the metal
# prompt_foo.py               # <-- THIS SYSTEM
# foo_files.py                # <-- MAIN ROUTER FILE (alternative to this one)

# TINY BUT EXCELLENT FOR "THIS SYSTEM" CONTEXT (OK to include every time / automatically = `apply.py`, `.gitignore`, `.gitattributes`)
# requirements.in             # <-- All known dependencies and (necessary) version pinning. WORA gotcha's exposed.
# __init__.py                 # <-- Master versioning
# pyproject.toml              # <-- The PyPI Packaging details

# OPTIONAL ACTUATORS (cheap and good to include to expand the AI's capabilities)
# cli.py                      # <-- Catch-all actuator for PyPI envs, Python anchoring, MCP tool-call (plus alternatives) and **kwargs like wrapping for CLI
# scripts/xp.py               # <-- Transforms host OS copy-paste buffer player-piano music into context-payload.
# scripts/ai.py               # <-- How I constantly use local AI to write git commit messages with `m` alias.
# scripts/crawl.py            # <-- Feel free to ask for something to be crawled and included in the next turn.
# scripts/weblogin.py         # <-- Lets the user "warm up" the cache for their web logins at their leisure on a profile that persists.
# scripts/webclip_2_markdown.py  # <-- Surprisingly important program.
 
# MISCELLANEOUS (rare to include but sometimes critical)
# scripts/foo_cartridge.py    # Needs description
# scripts/foo_replay.py       # Needs description
# release.py                  # <-- How everything ends up where it does (GitHub, PyPI, etc.)
# imports/voice_synthesis.py  # <-- The wand can talk to you
# imports/ascii_displays.py   # <-- Where all the ASCII Art lives
# scripts/release/version_sync.py  # <-- Needs to be wrapped into release.py and eliminated, I think.

#                         --- Under this line is were you paste what the AI gives you ---
#                         --- We call it context but it's really just the right-hand  ---
#                         --- blast-radius of the "probes" to make this all science.  ---

# Carry-over as the important work-in-progress parts of the project here just
# like above but not as long-standing overarching to the framework but rather
# for the current hot spots actively being worked on.

# STICKBUG & MOTHER CAT KATA (WORKING ON THE CHAPTER)

# assets/trails/first_context.yaml
# assets/trails/practice.yaml
# assets/trails/public_walk.yaml
# # assets/trails/botify_pageworkers.yaml

# Always include these with whatever connector
# scripts/sources_menu.py
# scripts/connectors/README.md
# scripts/connectors/wallet.py

# scripts/connectors/botify.py
# scripts/connectors/confluence.py
# scripts/connectors/gmail.py
# scripts/connectors/gsc.py
# scripts/connectors/jira.py
# scripts/connectors/mcp.py
# scripts/connectors/mcp_warm.py
# scripts/connectors/sheets.py
# scripts/connectors/slack.py

# The "walk" system is different from the Unix-like tiny connector commands.
# assets/installer/mck.sh
# assets/installer/replay.sh
# scripts/bookmark_import.py
# scripts/walk.py
# scripts/walk_cartridge.py
# scripts/walk_compile.py
# scripts/weblogin.py
# tools/scraper_tools.py
# 
# scripts/mcp_dummy_server.py  
# scripts/boot_menu.py
# scripts/mother_cat.py
# scripts/sources_menu.py

# --- START THIS DISCUSSION ---

# Get things started here! Guess at what context should be included.
# If you get it wrong, you're just wasting 1-turn because the AI will help.
# Un-comment lines, add lines with absolute-path filenames or `! ` commands. 

# Context 1 (Edit-in selections from above and add new files immediately below)
# /home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
# assets/installer/install.sh

# Context 2
/home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
assets/installer/install.sh
nixops.sh
remotes/honeybot/nixos/configuration.nix
! curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
! getent hosts npvg.org
! sha256sum assets/installer/install.sh
! HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum

# --- END `adhoc.txt` TEMPLATE ---

3: Patches: (the one change between the readings)

[Paste the diffs: patch, app, d, m per car]

Ignition (what makes the patched code run before the AFTER reading โ€“ <F2>, nix develop, a re-ride โ€“ or none required):

Well Ignition seems like the whole install test here. Letโ€™s check on Linux:

(sys) pipulate $ HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --connect-to "npvg.org:443:${HB}:443" https://npvg.org | bash -s npvgtest

--------------------------------------------------------------
   ๐Ÿš€ Welcome to the Npvgtest Installer ๐Ÿš€
   Local-first, Nix-reproducible, and yours to delete.
   A context compiler. Prompt well.
--------------------------------------------------------------

๐Ÿ” Checking prerequisites...
โœ… All required tools found.

๐Ÿ“ Checking target directory: /home/mike/npvgtest
โœ… Target directory is available.
๐Ÿ“ Creating directory '/home/mike/npvgtest'
๐Ÿ“ฅ Downloading Pipulate source code...
  #-#O=-#   #                                                                  
โœ… Download complete.

๐Ÿ“ฆ Extracting source code...
โœ… Extraction complete. Source code installed to '/home/mike/npvgtest'.

๐Ÿ“ Now in directory: /home/mike/npvgtest

๐Ÿ”‘ Fetching the shared deploy key from https://pipulate.com/key.rot...
   (Public, ROT13-encoded, pull-only: it exists so this folder can fetch
    updates without a GitHub account. nix develop decodes it into
    ~/.ssh/id_rsa only if no key is there already.)
โœ… Deploy key downloaded.
๐Ÿ”’ Deploy key saved as .ssh/rot (mode 600).

๐Ÿš€ Starting the Npvgtest environment...
--------------------------------------------------------------
  Source is in place at: /home/mike/npvgtest  
  To come back later, run:  
  cd /home/mike/npvgtest && nix develop -L  
--------------------------------------------------------------

Setting up app identity as 'npvgtest'...
โœ… Application identity set.

Creating ./run -- a one-file shortcut for the cd-and-nix-develop line above.

Next, nix develop builds the environment and turns this folder into a
git repository (the 'magic cookie' step) so it can auto-update from now on.
๐Ÿš€ Booting the Forever Machine...
Please wait while the Nix environment hydrates...
๐Ÿ”„ Transforming installation into git repository...
Creating temporary clone in /tmp/nix-shell.v1r28c/tmp.7c782SBAOv...
Cloning into '/tmp/nix-shell.v1r28c/tmp.7c782SBAOv'...
remote: Enumerating objects: 381, done.
remote: Counting objects: 100% (381/381), done.
remote: Compressing objects: 100% (333/333), done.
remote: Total 381 (delta 25), reused 239 (delta 21), pack-reused 0 (from 0)
Receiving objects: 100% (381/381), 3.20 MiB | 15.74 MiB/s, done.
Resolving deltas: 100% (25/25), done.
Preserving app identity and credentials...
Creating backup of current directory in /tmp/nix-shell.v1r28c/tmp.zkt29EuCwR...
Moving git repository into place...
โœ… Successfully transformed into git repository!
Original files backed up to: /tmp/nix-shell.v1r28c/tmp.zkt29EuCwR
Already up to date.
Found ~/.ssh/id_rsa -- switching the git remote to SSH so auto-updates use it.
INFO: Setting up your personal Playground...
(Nix) 2.25.0pre20240910_b9d3cdfb ยท Python 3.12.13 ยท v2.55 ยท ~/npvgtest
๐Ÿ”ง Fresh install detected โ€” packages downloading...
โœ… 290 packages ready.
๐Ÿ““ 12 starter file(s) copied into Notebooks/ -- yours to edit.
๐Ÿ›‘ Stopped a Pipulate server that was already running (ports 5001/8888 are shared -- one workshop at a time).
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ nix develop -- a reproducible *nix shell :: pick a door โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚                                                                                                                                                                                   โ”‚
โ”‚  [1]  JupyterLab tab     both servers start; JupyterLab opens in the browser                                                                                                      โ”‚
โ”‚  [2]  Text Commands      nothing starts -- five words wait at the prompt                                                                                                          โ”‚
โ”‚  [3]  Pipulate tab       both servers start; the app opens in the browser                                                                                                         โ”‚
โ”‚                                                                                                                                                                                   โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ waiting for your choice -- Ctrl+C also drops to the shell โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

Okay, thatโ€™s good. And now on Mac:

michaellevin@MichaelMacBook-Pro ~ % HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --connect-to "npvg.org:443:${HB}:443" https://npvg.org | bash -s npvgtest
curl: (6) Could not resolve host: honeybot
michaellevin@MichaelMacBook-Pro ~ % HB=$(ssh -G [REDACTED_IP] | awk '/^hostname /{print $2}'); curl -fsSL --connect-to "npvg.org:443:${HB}:443" https://npvg.org | bash -s npvgtest

--------------------------------------------------------------
   ๐Ÿš€ Welcome to the Npvgtest Installer ๐Ÿš€
   Local-first, Nix-reproducible, and yours to delete.
   A context compiler. Prompt well.
--------------------------------------------------------------

๐Ÿ” Checking prerequisites...
โœ… All required tools found.

๐Ÿ“ Checking target directory: /Users/michaellevin/npvgtest
โœ… Target directory is available.
๐Ÿ“ Creating directory '/Users/michaellevin/npvgtest'
๐Ÿ“ฅ Downloading Pipulate source code...
  #-#O=-#   #                                                                  
โœ… Download complete.

๐Ÿ“ฆ Extracting source code...
โœ… Extraction complete. Source code installed to '/Users/michaellevin/npvgtest'.

๐Ÿ“ Now in directory: /Users/michaellevin/npvgtest

๐Ÿ”‘ Fetching the shared deploy key from https://pipulate.com/key.rot...
   (Public, ROT13-encoded, pull-only: it exists so this folder can fetch
    updates without a GitHub account. nix develop decodes it into
    ~/.ssh/id_rsa only if no key is there already.)
โœ… Deploy key downloaded.
๐Ÿ”’ Deploy key saved as .ssh/rot (mode 600).

๐Ÿš€ Starting the Npvgtest environment...
--------------------------------------------------------------
  Source is in place at: /Users/michaellevin/npvgtest  
  To come back later, run:  
  cd /Users/michaellevin/npvgtest && nix develop -L  
--------------------------------------------------------------

Setting up app identity as 'npvgtest'...
โœ… Application identity set.

Creating ./run -- a one-file shortcut for the cd-and-nix-develop line above.

Next, nix develop builds the environment and turns this folder into a
git repository (the 'magic cookie' step) so it can auto-update from now on.
๐Ÿš€ Booting the Forever Machine...
Please wait while the Nix environment hydrates...
๐Ÿ”„ Transforming installation into git repository...
Creating temporary clone in /tmp/nix-shell.KTKrzk/tmp.AbetyfOreV...
Cloning into '/tmp/nix-shell.KTKrzk/tmp.AbetyfOreV'...
remote: Enumerating objects: 381, done.
remote: Counting objects: 100% (381/381), done.
remote: Compressing objects: 100% (333/333), done.
remote: Total 381 (delta 25), reused 239 (delta 21), pack-reused 0 (from 0)
Receiving objects: 100% (381/381), 3.20 MiB | 11.41 MiB/s, done.
Resolving deltas: 100% (25/25), done.
Preserving app identity and credentials...
Creating backup of current directory in /tmp/nix-shell.KTKrzk/tmp.a46cDishk6...
Moving git repository into place...
โœ… Successfully transformed into git repository!
Original files backed up to: /tmp/nix-shell.KTKrzk/tmp.a46cDishk6
Already up to date.
Found ~/.ssh/id_rsa -- switching the git remote to SSH so auto-updates use it.
INFO: Setting up your personal Playground...
(Determinate Nix 3.19.1) 2.34.6 ยท Python 3.12.13 ยท v2.55 ยท ~/npvgtest
๐Ÿ”ง Fresh install detected โ€” packages downloading...
โœ… 291 packages ready.
๐Ÿ““ 12 starter file(s) copied into Notebooks/ -- yours to edit.
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ nix develop -- a reproducible *nix shell :: pick a door โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚                                                                                                                     โ”‚
โ”‚  [1]  JupyterLab tab     both servers start; JupyterLab opens in the browser                                        โ”‚
โ”‚  [2]  Text Commands      nothing starts -- five words wait at the prompt                                            โ”‚
โ”‚  [3]  Pipulate tab       both servers start; the app opens in the browser                                           โ”‚
โ”‚                                                                                                                     โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ waiting for your choice -- Ctrl+C also drops to the shell โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

Okay, thatโ€™s good. I had to put the IP address in for honeybot on the Mac. But still this does give a minimum viable route. I will look at the actual changes to the pfSense router itself because when Iโ€™m screen sharing with coworkers showing them what to do, I want to use the exact same commands as they do but that was lower priority than testing it at all which is what we just did on 2 platforms!

4: Prompt: Rule from the LIVE RECEIPTS. Probe 1 is the diagnosis: subject and issuer naming the router and its private CA means the two-errors-one-door reading holds; subject npvg.org, issuer Letโ€™s Encrypt, and โ€œverify okโ€ means the workstation already reaches honeybot by name, and probe 2 says whether that is Car 2 landing or the diagnosis being wrong. Probe 2: the public address (redacted in the receipt) means DNS still points at the front door; honeybotโ€™s LAN address means split DNS is live for this machine. Probes 3 and 4 are a gate, not a straddle: one digest in both worlds, or probe 4 prints the empty-input digest e3b0c442 with curlโ€™s error above it, and the fault is the address derivation, not the server. Then read the operatorโ€™s pasted Car 1 transcript as the CVR it is: name the last line the installer printed, whether the banner said Npvgtest, whether the download and the key fetch completed, and whether the handoff into nix develop happened; a stop anywhere is a finding with a line, not a failed ride. Do not call the installer proven for strangers: this run tested the bytes, the certificate, and the scriptโ€™s own outbound fetches from inside the LAN, and the routerโ€™s forward was witnessed only by the phone. No repo patches unless a receipt convicts one. Continue the article for the New-B and close with the five-stage response contract.

5: Deliverables: [external artifacts, updates to this system or โ€˜None this turn outside the repoโ€™]

I did confirm that I can get into the router web-based interface but I donโ€™t want to make that change yet until I get the feedback from the AI on this turn.

I did confirm that I can get into the router web-based interface but I donโ€™t want to make that change yet until I get the feedback from the AI on this turn.

Fable 5.1 Extra: Fable 5.1 Extra: Context verified. Four live receipts this compile, all four reading as the last caboose predicted, plus two cockpit transcripts pasted by hand. No patches rode, so nothing here is a straddle; Car 1 was an ignition, and the transcripts are its recording.

The receipts, in one breath. Probe 1: the certificate npvg.org hands this desk names the router in both subject and issuer, O=pfSense webConfigurator Self-Signed Certificate, so the two-errors-one-door reading holds. Three curl builds looked at that one certificate this turn and reported three different verify codes: 18 from the system curl (self-signed), 20 from Appleโ€™s (no local issuer), and a bare 12 from the compileโ€™s nix-shell curl, which I cannot place in OpenSSLโ€™s table with confidence and will not build on. That also corrects last turnโ€™s guess that โ€œthe Linux box trusts the routerโ€™s issuerโ€: the hand-runโ€™s code 18 says the system curl does not, and whichever check fails first, the name on the certificate is never npvg.org. Probe 2: DNS still answers with the front-door address (the receipt carries it in the clear; it is a public DNS record, and I will call it the front-door address rather than repeat the digits), not honeybotโ€™s LAN address, so no split DNS is live and the diagnosis stands. Probes 3 and 4: 59244b08โ€ฆ and 59244b08โ€ฆ, one digest in both worlds, the gate. The ssh warning under probe 4 (โ€œPseudo-terminal will not be allocatedโ€) is noise from ssh -G inheriting a tty request; the digest under it proves the address came through anyway.

The Macโ€™s failed probe 4 was the predicted failure, and the fix is a plain number. Last turn named it in advance: if ssh -G returns an alias with no HostName, curl cannot resolve it and prints the empty-input digest e3b0c442โ€ฆ under a curl error. That is exactly what the Mac printed. For the New-B: ssh -G somename prints the settings ssh would use for that name. On the Linux box, ~/.ssh/config says honeybot means [REDACTED_IP], so the address comes out. The Mac has no such entry, so ssh echoes the word back unchanged, and curl went looking for a machine called honeybot that exists only in one file on another computer. ssh -G [REDACTED_IP] works because an address echoes as itself; it is a roundabout way of writing HB=[REDACTED_IP], and that is the honest Mac form. The Macโ€™s other two failures are boring: macOS has no getent, and there is no repo checkout at that path. Neither is a finding.

The Linux recording, stage by stage. The banner read โ€œWelcome to the Npvgtest Installer,โ€ so the custom name rode through the pipe, through main "$@", and through the brace group. Prerequisites found. ~/npvgtest created. The GitHub zip downloaded and extracted. The deploy key fetched from pipulate.com and saved at mode 600. whitelabel.txt written, ./run created. The installerโ€™s own last line was โ€œPlease wait while the Nix environment hydratesโ€ฆโ€; everything after it is the flake speaking: 381 objects cloned, the folder backed up and swapped for a real git repository, โ€œSuccessfully transformed into git repository!โ€, 290 packages hydrated, 12 starter notebooks copied, and the three-door menu waiting for a choice. The last line on the terminal is the menuโ€™s bottom border. Every stage the caboose asked about happened, in order.

The Mac recording, same script, different vantage. Appleโ€™s curl (which reports TLS 1.3 as โ€œ(304)โ€, the protocolโ€™s wire number in hex) fetched the same bytes through --connect-to. Banner Npvgtest, download, extract, key, identity, ./run, git transformation, then Determinate Nix 3.19.1 hydrated 291 packages and the menu came up. Two operating systems, two Nix installations of different lineage, two curl builds, one script, one digest. The one-package difference between 290 and 291 is a platform-specific wheel and not this rideโ€™s question.

Two lines in the recordings deserve a second look. First, Linux printed โ€œ๐Ÿ›‘ Stopped a Pipulate server that was already running (ports 5001/8888 are shared โ€“ one workshop at a time).โ€ That server was your main workshop in ~/repos/pipulate. Designed behavior, but a side effect of rehearsing on the machine you work on; restart it after cleanup. The Mac had nothing to stop. Second, both boxes printed โ€œFound ~/.ssh/id_rsa โ€“ switching the git remote to SSH so auto-updates use it.โ€ Both machines already carry your own key, so the flake took the branch that uses it and never decoded .ssh/rot. The magic-cookie branch, the one a stranger with an empty ~/.ssh takes, is unwitnessed by this ride. So is the Nix-not-found branch, the โ€œInventing the universeโ€ one, because both boxes had Nix already. Those are the two branches a stranger is most likely to take, and the two a rehearsal on your own machines cannot take.

What this proved and what it did not. Proved: the exact bytes strangers get, over real TLS against the real certificate, from inside the house, on two platforms, all the way through the handoff into the flake and out the other side to the menu. Not proved: the routerโ€™s forward from the street (witnessed only by the phone yesterday), the WSL lane, and the two stranger branches above. โ€œThe installer works from inside the LAN on two platformsโ€ is the true sentence. โ€œThe installer is proven for strangersโ€ is not, and the next cheap witness for it is a machine with no Nix and no key, which is a different day.

The router: go, and here is why it is the light touch. For the New-B, the router runs a small phone book for the house. Every device that asks โ€œwhere is npvg.org?โ€ gets its answer from that book, which today just copies the public answer: the front door. A host override adds one line to the houseโ€™s book only, saying npvg.org lives in the back room. The public book is untouched, so strangers still arrive at the front door and get forwarded as before. Nothing else moves: not the port forward, not NAT, not the DDNS heartbeat, not certificate renewal (Letโ€™s Encrypt checks from outside, where the answer has not changed). Reversal is deleting the one line and clicking Apply. The one gate is whether your desk asks the router at all; probe A prints which resolver it asks. If it names the router, the override reaches this machine and the AFTER you want is the strangerโ€™s exact command, curl -fsSL https://npvg.org | bash -s npvgtest, working with no flag; probe D measures precisely that minus the bash. If probe A names an outside resolver, the override still helps the Mac and the phone on Wi-Fi, and the desk keeps --connect-to until its DNS is pointed home. Two cache notes: pfSense restarts its resolver on Apply, which clears the routerโ€™s cache, but the workstation may hold the old answer, so flush it or the AFTER reads like the BEFORE; and honeybotโ€™s own hosts file pins mikelev.in to loopback but says nothing about npvg.org, so after the override honeybot asking the router gets its own LAN address, which its nginx answers, and no rebuild is needed. If mikelev.in has the same trouble from the desk, the same table fixes it with one more entry.


1. PROBES

grep -E '^nameserver' /etc/resolv.conf; resolvectl dns 2>/dev/null
getent hosts npvg.org
curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
curl -fsSL --max-time 10 https://npvg.org | sha256sum
HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum
sha256sum assets/installer/install.sh

Probe A is the gate: the routerโ€™s address means Car 2 will reach this desk; an outside resolver means it will not, and the override still serves the other devices. Probe B is Car 2โ€™s straddle: the front-door address before, honeybotโ€™s LAN address after. Probe C is the same straddle read at the certificate: pfSense names and x-frame-options before, CN=npvg.org with a Letโ€™s Encrypt issuer and no x-frame-options after. Probe D is the coworkerโ€™s command with the bash removed: the empty-input digest e3b0c442โ€ฆ under curl: (60) before, 59244b08โ€ฆ after; that flip is the whole point of the ride. Probes E and F are gates, not straddles: 59244b08โ€ฆ in both worlds.

2. NEXT CONTEXT

/home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
assets/installer/install.sh
! grep -E '^nameserver' /etc/resolv.conf; resolvectl dns 2>/dev/null
! getent hosts npvg.org
! curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
! curl -fsSL --max-time 10 https://npvg.org | sha256sum
! HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum
! sha256sum assets/installer/install.sh

The 27k-token prior article is context for continuity only; prune it with ahe if the budget needs the room.

3. PATCHES

No repo patches required. Three cars, all actuators, all yours to ride or veto.

Car 1, cleanup, both machines. Removes the rehearsal folders so Car 3 can run fresh, then restarts the workshop the Linux rehearsal stopped.

rm -rf ~/npvgtest
cd ~/repos/pipulate && nix develop

Car 2, the router override. Outside the repo, so its clicks ride in section 5A. Ignition is pfSenseโ€™s Apply Changes (which restarts the resolver), followed on the workstation by resolvectl flush-caches if probe A showed systemd-resolved; if it showed a plain /etc/resolv.conf, there is no local cache to flush. Probes B, C, and D read DNS at call time, so no further ignition.

Car 3, optional, the coworkerโ€™s exact command from inside the house. Only after probe D reads 59244b08โ€ฆ. Fresh terminal, not the workshop shell, ~/npvgtest absent. Probe D already proves the bytes and the certificate; this car proves the whole handoff under the identical line you will type on a screen share.

curl -fsSL https://npvg.org | bash -s npvgtest

4. PROMPT

Rule from the LIVE RECEIPTS. Probe A is the gate: name the resolver this desk asks; the router's address means Car 2 can reach it, anything else means the override is inert here and the rest of the readings are expected to match the BEFORE. Probe B is Car 2's straddle: the front-door address (do not repeat its digits) before, honeybot's LAN address after. Probe C reads the same flip at the certificate: pfSense subject and issuer with x-frame-options before; CN=npvg.org, a Let's Encrypt issuer, and no x-frame-options after. Probe D is the coworker's command minus the bash: e3b0c442 under curl (60) before, 59244b08 after, and that flip is the destination of this ride; if B flipped but D did not, name the cache, the resolver, or the nginx door as the suspect and say which reading would separate them. Probes E and F are gates and must read 59244b08 in both worlds. If the operator pastes a Car 3 transcript or the Mac commands from section 5B, read them as the CVR they are: banner, download, key, identity, handoff, menu, and any stopped-server line. Do not call the installer proven for strangers: the magic-cookie key branch and the Nix-not-found branch are unwitnessed, WSL is untouched, and the router's forward was witnessed only by the phone. No repo patches unless a receipt convicts one. Continue the article for the New-B and close with the five-stage response contract.

5. EXTERNAL DELIVERABLES

A. pfSense split DNS (Car 2, manual clicks).

  1. Services โ†’ DNS Resolver โ†’ General Settings, scroll to Host Overrides, click Add. (If this router runs the DNS Forwarder instead, the same table lives under Services โ†’ DNS Forwarder.)
  2. Host: leave blank. Domain: npvg.org. IP Address: [REDACTED_IP].
  3. Under Additional Names for this Host: Host www, Domain npvg.org.
  4. Save, then Apply Changes.

Reversal is deleting that entry and applying again. Do not enable NAT reflection as part of this.

B. The Macโ€™s readings (manual paste; macOS has no getent, and the digest tool is shasum). Run before and after Car 2; the flush line only after Apply.

scutil --dns | grep -m3 nameserver
dscacheutil -q host -a name npvg.org
curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
curl -fsSL --max-time 10 https://npvg.org | shasum -a 256
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder

Until Car 2 lands, the Macโ€™s working form of the installer stays HB=[REDACTED_IP]; curl -fsSL --connect-to "npvg.org:443:${HB}:443" https://npvg.org | bash -s npvgtest, with rm -rf ~/npvgtest first.

MikeLev.in:

Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.

1: Probe: (BEFORE: hand-run, nothing changed yet)

(sys) pipulate $ grep -E '^nameserver' /etc/resolv.conf; resolvectl dns 2>/dev/null
getent hosts npvg.org
curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
curl -fsSL --max-time 10 https://npvg.org | sha256sum
HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum
sha256sum assets/installer/install.sh
nameserver 8.8.8.8
nameserver 1.1.1.1
nameserver 2600:4041:50e2:5b00:2d0:b4ff:fe04:3685
[REDACTED_IP]   npvg.org
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
*  subject: O=pfSense webConfigurator Self-Signed Certificate; CN=pfSense-6808ceb59b08f
*  issuer: O=pfSense webConfigurator Self-Signed Certificate; CN=pfSense-6808ceb59b08f
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
< HTTP/2 200 
< x-frame-options: SAMEORIGIN
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  -
59244b08613cd834d089f3bffa88b7c2cfcf8b40a8f3bbb80fde37fdc04feffc  -
59244b08613cd834d089f3bffa88b7c2cfcf8b40a8f3bbb80fde37fdc04feffc  assets/installer/install.sh
(sys) pipulate $ 

And on Mac:

michaellevin@MichaelMacBook-Pro ~ % grep -E '^nameserver' /etc/resolv.conf; resolvectl dns 2>/dev/null
getent hosts npvg.org
curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
curl -fsSL --max-time 10 https://npvg.org | sha256sum
HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum
sha256sum assets/installer/install.sh
nameserver 2600:4041:50e2:5b00:2d0:b4ff:fe04:3685
nameserver 8.8.8.8
nameserver 1.1.1.1
zsh: command not found: getent
* (304) (IN), TLS handshake, CERT verify (15):
*  subject: O=pfSense webConfigurator Self-Signed Certificate; CN=pfSense-6808ceb59b08f
*  issuer: O=pfSense webConfigurator Self-Signed Certificate; CN=pfSense-6808ceb59b08f
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
< HTTP/2 200 
< x-frame-options: SAMEORIGIN
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  -
curl: (6) Could not resolve host: honeybot
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  -
sha256sum: assets/installer/install.sh: No such file or directory
michaellevin@MichaelMacBook-Pro ~ % 

Okay, thatโ€™s the before. Iโ€™m pretty sure I make the pfSense changes before I do the ahc experiment which gets the 2nd reading. Letโ€™s set that up.

2: Context: (AFTER: the same probes re-run by the compiler as ! lines)

# AD HOC CHOP! The Not-Managed-by-Git Safe-for-Client-Data place. Insert Simpson Couch Gag in white space  below (explain anything to the audience you feel needs it explained)G
# adhoc.txt    _   _   _               ____ _   _  ___  ____  _   
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |  Trying to get the installer to work inside my LAN.
# ahe/ _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |  Yeah, yeah, I'll have to uninstall Nix for the full test soon but not right now.
# ahc ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|  
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)  

# To Human: WELCOME TO VIM! It's really NeoVim but I say vim anyway.
# To AI: This is an alternate 40K view to the big book-ore rolling pin spine.

# 1. **Probe**: Baseline Reading
# 2. **Context**: Post-experiment *planned* reading instructions
# 3. **Patch**: The experiment and how to make it happen
# 4. **Prompt**: Post-experiment instructions and how to read results
# 5. **Deliverable**: How the world is forever different moving forward

# The first thing you need to know here is that everything that comes after the
# hash symbol (#) is commented out โ€” and that's EVERYTHING in this file's default
# state. Begin editing-in lines for inclusion as part of the context or adding
# chunks of new context at the bottom. `Ctrl`+`v`, `j` (repeatedly), `l` (to move
# right), `d` (to delete). Reverse that with `Ctrl`+`v`, `j` (repeatedly),
# `Shift`+`i`, `# `, `Esc` to put the hashes back. You can just arrow-key around
# here with `h`, `j`, `k`, `l`. Save-and-quit is a bit tricky because another
# file is also loaded: `Esc`, `:`, `q`, `w`, `!`

# If this is stressing you out and you're a quitter and want to quit, just type:
# `Esc`, `:`, `q`, `!`, `Enter`. That will exit without saving any changes. If
# you want to get over this hump, type: `Esc`, `:`, `T`, `u`, `t`, `o`, `r`, `Enter`.

# This file is just to make it easy having options of what to edit into context.
# You can use whatever text-file you want to stack file-names and commands to
# build an output text-file with the identically stacked output of each file or
# command. In this way we vertically append or "stack" a bunch of text; simple as
# that. If you understand this concept, you're on your way to future-proofing
# yourself in the Age of AI. Congratulations! Here is how to include web pages:

#    !URL  --------------------------------------------------------------------
#      when    Public page; what a stranger or crawler sees; the BEFORE of a
#              login-wall diagnosis
#      switch  It shows a login page -> `warm URL` once, then `?URL`
#    
#    ?URL  --------------------------------------------------------------------
#      when    Anything behind a login, on the site's persistent profile;
#              `check URL` first
#      switch  The lenses show a shell (nav, an `[Iframe]` leaf, no content) ->
#              read the wire truth for the XHR the frame makes, then call that
#              API with a connector
#    
#    @URL  --------------------------------------------------------------------
#      when    Every re-read of a page already scraped; no browser, no network
#      switch  The cached page is stale or was a login wall -> fresh `!` or `?`
#    
#    $URL  --------------------------------------------------------------------
#      when    Exact markup: meta tags, a JSON blob in a `<script>`
#      note    Token-heavy; needs a prior scrape
#    
#    %URL  --------------------------------------------------------------------
#      when    The network log distilled; SPA endpoint discovery
#      switch  It re-serves the wire truth you already have -> the API
#    
#    ! cmd  -------------------------------------------------------------------
#      when    Any bounded, non-interactive command as a live receipt
#      note    Cap it with `-n`; no aliases, no prompts
#    
#    Connector  ---------------------------------------------------------------
#      when    The number you want is one GET away
#      switch  LIST until the thing isn't in the list -> FETCH by id -> DRILL
#              the path the app's own frame called -> `--grep` to narrow a list
#              or find a leaf

# Every step is one argument longer than the last; the moment a lens shows less than the wire does is the moment to stop scraping.

# STORYTELLING: OPTIONAL, BIG 40K-FOOT VIEW
# ! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs  # <-- ROLLING PIN that gives the 40K foot book-spine view of book-ore (only works for me because of local-only git repo)
# GLOSSARY.md                 # <-- Terms defined like back of J.R.R. Tolkien book, grows as `prompt_foo.py` gets scars and shrinks.
# scripts/articles/lsa.py     # <-- 2ND BRAIN: Search external memory with `rgx`, `rgxc` & `posts` Blogging for Hackers Jekyll-compatible.
# ~/repos/nixos/autognome.py  # <-- Letting the AIs really understand my environment (The Brave Little Tailor punches above Their Weight Class proving the dunning-kruger effect the gate-keeper's (lower-case) lament.)
 
# STILL BIG BUT LESS OPTIONAL (especially flake.nix)
# init.lua                    # <-- Daily driver hot-keys that overlap with aliases in flake.nix. `<leader>m` makes it Science (this process)!
# flake.nix                   # <-- THE ONE BIG THING TO INCLUDE Infrastructure as Code (IaC) tells LLM about your system down to the metal
# prompt_foo.py               # <-- THIS SYSTEM
# foo_files.py                # <-- MAIN ROUTER FILE (alternative to this one)

# TINY BUT EXCELLENT FOR "THIS SYSTEM" CONTEXT (OK to include every time / automatically = `apply.py`, `.gitignore`, `.gitattributes`)
# requirements.in             # <-- All known dependencies and (necessary) version pinning. WORA gotcha's exposed.
# __init__.py                 # <-- Master versioning
# pyproject.toml              # <-- The PyPI Packaging details

# OPTIONAL ACTUATORS (cheap and good to include to expand the AI's capabilities)
# cli.py                      # <-- Catch-all actuator for PyPI envs, Python anchoring, MCP tool-call (plus alternatives) and **kwargs like wrapping for CLI
# scripts/xp.py               # <-- Transforms host OS copy-paste buffer player-piano music into context-payload.
# scripts/ai.py               # <-- How I constantly use local AI to write git commit messages with `m` alias.
# scripts/crawl.py            # <-- Feel free to ask for something to be crawled and included in the next turn.
# scripts/weblogin.py         # <-- Lets the user "warm up" the cache for their web logins at their leisure on a profile that persists.
# scripts/webclip_2_markdown.py  # <-- Surprisingly important program.
 
# MISCELLANEOUS (rare to include but sometimes critical)
# scripts/foo_cartridge.py    # Needs description
# scripts/foo_replay.py       # Needs description
# release.py                  # <-- How everything ends up where it does (GitHub, PyPI, etc.)
# imports/voice_synthesis.py  # <-- The wand can talk to you
# imports/ascii_displays.py   # <-- Where all the ASCII Art lives
# scripts/release/version_sync.py  # <-- Needs to be wrapped into release.py and eliminated, I think.

#                         --- Under this line is were you paste what the AI gives you ---
#                         --- We call it context but it's really just the right-hand  ---
#                         --- blast-radius of the "probes" to make this all science.  ---

# Carry-over as the important work-in-progress parts of the project here just
# like above but not as long-standing overarching to the framework but rather
# for the current hot spots actively being worked on.

# STICKBUG & MOTHER CAT KATA (WORKING ON THE CHAPTER)

# assets/trails/first_context.yaml
# assets/trails/practice.yaml
# assets/trails/public_walk.yaml
# # assets/trails/botify_pageworkers.yaml

# Always include these with whatever connector
# scripts/sources_menu.py
# scripts/connectors/README.md
# scripts/connectors/wallet.py

# scripts/connectors/botify.py
# scripts/connectors/confluence.py
# scripts/connectors/gmail.py
# scripts/connectors/gsc.py
# scripts/connectors/jira.py
# scripts/connectors/mcp.py
# scripts/connectors/mcp_warm.py
# scripts/connectors/sheets.py
# scripts/connectors/slack.py

# The "walk" system is different from the Unix-like tiny connector commands.
# assets/installer/mck.sh
# assets/installer/replay.sh
# scripts/bookmark_import.py
# scripts/walk.py
# scripts/walk_cartridge.py
# scripts/walk_compile.py
# scripts/weblogin.py
# tools/scraper_tools.py
# 
# scripts/mcp_dummy_server.py  
# scripts/boot_menu.py
# scripts/mother_cat.py
# scripts/sources_menu.py

# --- START THIS DISCUSSION ---

# Get things started here! Guess at what context should be included.
# If you get it wrong, you're just wasting 1-turn because the AI will help.
# Un-comment lines, add lines with absolute-path filenames or `! ` commands. 

# Context 1 (Edit-in selections from above and add new files immediately below)
# /home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
# assets/installer/install.sh

# Context 2
# /home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
# assets/installer/install.sh
# nixops.sh
# remotes/honeybot/nixos/configuration.nix
# ! curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
# ! getent hosts npvg.org
# ! sha256sum assets/installer/install.sh
# ! HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum

# Context 3
/home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
assets/installer/install.sh
! grep -E '^nameserver' /etc/resolv.conf; resolvectl dns 2>/dev/null
! getent hosts npvg.org
! curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
! curl -fsSL --max-time 10 https://npvg.org | sha256sum
! HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum
! sha256sum assets/installer/install.sh

# --- END `adhoc.txt` TEMPLATE ---

3: Patches: (the one change between the readings)

I clean the slate:

(nix) pipulate $ rm -rf ~/npvgtest
(nix) pipulate $ 

And on the Mac.

michaellevin@MichaelMacBook-Pro ~ % rm -rf ~/npvgtest
michaellevin@MichaelMacBook-Pro ~ %

Ignition (what makes the patched code run before the AFTER reading โ€“ <F2>, nix develop, a re-ride โ€“ or none required):

Alright now we need those pfSense entries. Okay, done. And I did the Apply Resolver button too. But itโ€™s still producing the error in the browser:

Potential DNS Rebind attack detected, see https://en.wikipedia.org/wiki/DNS_rebinding Try accessing the router by IP address instead of by hostname.

โ€ฆthough I think this may still be expected because thereโ€™s DNS caches all over the place. Letโ€™s check from the terminal again:

(sys) pipulate $ curl -fsSL https://npvg.org | bash -s npvgtest
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.
(sys) pipulate $ 

Nope. We try the Mac.

Last login: Mon Sep 14 04:23:18 on ttys000
michaellevin@MichaelMacBook-Pro ~ % rm -rf pipulate
michaellevin@MichaelMacBook-Pro ~ % curl -fsSL https://npvg.org | bash
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
michaellevin@MichaelMacBook-Pro ~ %

Nope. We double check that we did the right thing in the router. Yep, there it is:

Host Overrides

ย  npvg.org [REDACTED_IP] ย  ย 
ย  mikelev.in [REDACTED_IP] ย  ย 

The router offers โ€œRestart Serviceโ€ so Iโ€™ll try that firstโ€ฆ

No luck. I check on my phone with and without WiFi. With WiFi itโ€™s unreachable with the security warning still and without WiFi (on 5G) npvg.org is reachable without the warnings.

The Phone Book Trap: When Nobody Asks the Router

4: Prompt: (the next question, written before the AFTER reading is in)

Rule from the LIVE RECEIPTS. Probe A is the gate: name the resolver this desk asks; the routerโ€™s address means Car 2 can reach it, anything else means the override is inert here and the rest of the readings are expected to match the BEFORE. Probe B is Car 2โ€™s straddle: the front-door address (do not repeat its digits) before, honeybotโ€™s LAN address after. Probe C reads the same flip at the certificate: pfSense subject and issuer with x-frame-options before; CN=npvg.org, a Letโ€™s Encrypt issuer, and no x-frame-options after. Probe D is the coworkerโ€™s command minus the bash: e3b0c442 under curl (60) before, 59244b08 after, and that flip is the destination of this ride; if B flipped but D did not, name the cache, the resolver, or the nginx door as the suspect and say which reading would separate them. Probes E and F are gates and must read 59244b08 in both worlds. If the operator pastes a Car 3 transcript or the Mac commands from section 5B, read them as the CVR they are: banner, download, key, identity, handoff, menu, and any stopped-server line. Do not call the installer proven for strangers: the magic-cookie key branch and the Nix-not-found branch are unwitnessed, WSL is untouched, and the routerโ€™s forward was witnessed only by the phone. No repo patches unless a receipt convicts one. Continue the article for the New-B and close with the five-stage response contract.

5: Deliverables: [external artifacts, updates to this system or โ€˜None this turn outside the repoโ€™]

I double-check the router instructions. Yeah, I did it all correctly. It may be some other setting. Iโ€™m going to try to show you the entire Web UI. Oh man I canโ€™t because the warm command doesnโ€™t make the login for warm https://redacted-ip.invalid/ stick. In other words, following pop-ups arenโ€™t pre-logged in like normal websites and I donโ€™t want to go down that rabbit hole, so letโ€™s try to think it through. Oh I can just save the hydrated DOM! Okay, thatโ€™s included as deleteme.html.

Fable 5.1 Extra: Fable 5.1 Extra: Context verified. Six live receipts this compile, and the very first one is the gate the caboose named, and it fired: the desk does not ask the router. Every reading after it matches the BEFORE exactly as the caboose said it would, so nothing below is a failed ride. It is a correct override that nobody in the house has asked yet.

The receipts, in one breath. Probe A: /etc/resolv.conf lists 8.8.8.8 first, 1.1.1.1 second, then two IPv6 addresses (which I will not transcribe; they are your home prefix) that share one interface identifier under two different prefixes, which is what a pfSense LAN address looks like after Verizon rotates the delegated prefix. resolvectl printed nothing and exited nonzero, so there is no systemd-resolved cache to flush. Probe B: still the front-door address. Probe C: still the pfSense self-signed certificate with x-frame-options. Probe D: e3b0c442โ€ฆ under curl: (60), the nix-shell curl failing at the name check where the system curl fails at the chain; same door. Probes E and F: 59244b08โ€ฆ twice, the gate holding. Your two hand-runs, the phone on Wi-Fi, and the browser all agree with the machine.

For the New-B: you corrected the phone book, but nobody uses that phone book. The routerโ€™s resolver now says npvg.org lives in the back room; the saved page proves it, both entries present, โ€œchanges applied successfullyโ€ at the top. But every device keeps its own short list of which phone books to consult, and on the workstation that list reads Google, then Cloudflare, then the router. Linux asks the first book and stops when it answers. Googleโ€™s book has only the public answer. The routerโ€™s book was never opened. The Macโ€™s list has the routerโ€™s IPv6 entry first, yet it failed too, which says either that entry is the stale prefix of the two or macOS orders its resolvers differently than /etc/resolv.conf shows; the Mac commands in 5B separate those, and neither changes the fix.

The browser message is the same fingerprint curl saw, in different clothes. โ€œPotential DNS Rebind attack detectedโ€ is pfSenseโ€™s own admin page refusing to be addressed by a name that is not its own. You only see it if your browser reached the routerโ€™s admin server, and you reach the routerโ€™s admin server by knocking on the public address from inside. It is not rebinding protection blocking your override; host overrides are local data and are served before that filter runs. It is the router saying โ€œyou are still at my front door,โ€ which is exactly what x-frame-options: SAMEORIGIN told curl.

The phone is the tiebreaker, and it points at DHCP. A phone has no hand-typed DNS. If it failed on Wi-Fi, it was handed the same list your Mac and workstation carry, and the thing that hands lists to every device that joins the network is the routerโ€™s DHCP server. The resolver page you saved says the mechanism out loud in its own footnote: if the Resolver is enabled, DHCP automatically serves the routerโ€™s LAN address as the DNS server, which means that only happens when the DHCP serverโ€™s DNS fields are blank. The likeliest reading is that they hold 8.8.8.8 and 1.1.1.1, typed in once long ago. Probe 4 below reads the lease the router actually handed this machine, so that stays a reading and not a guess. The alternative, if the lease turns out to already name the router, is that the workstation pins those two in its own NixOS config, and probe 5 greps for that line.

One detail from the saved page, unrelated to the ride. The routerโ€™s admin certificate expired on 2026-05-27; that is the 139 notices. It is not why the ride failed, and fixing it does nothing for npvg.org, but it is why the admin page throws its own warnings. A chore for another day, under System โ†’ Certificates.

And one hazard from the tree. deleteme.html is untracked in the repo root, and .gitignore only covers deleteme*.txt. It carries a CSRF token from your router session and the routerโ€™s internal hostname. A blast that stages untracked files would publish it. Car 0 widens the pattern before anything else commits.

On the couch gag. Do not uninstall Nix from a machine you work on. The stranger test wants a throwaway: a VM, a spare laptop, a friendโ€™s machine. That is a different day.


1. PROBES

git status --short | head -5
GW=$(ip route show default | awk '{print $3; exit}'); .venv/bin/python -c 'import socket,struct,sys; gw=sys.argv[1]; q=b"\xbe\xef\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"+b"".join(bytes([len(p)])+p.encode() for p in "npvg.org".split("."))+b"\x00\x00\x01\x00\x01"; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(3); s.sendto(q,(gw,53)); r=s.recv(512); an=struct.unpack(">H",r[6:8])[0]; print("ROUTER="+gw, "RCODE=%d"%(r[3]&15), "ANCOUNT=%d"%an, "LAST_A="+(socket.inet_ntoa(r[-4:]) if an else "none"))' "$GW"
nmcli -g DHCP4.OPTION device show 2>/dev/null | grep -iE 'domain_name_servers|routers'
grep -rn -E 'nameservers' /etc/nixos ~/repos/nixos --include='*.nix' 2>/dev/null | head -8
grep -E '^nameserver' /etc/resolv.conf
getent hosts npvg.org
curl -fsSL --max-time 10 https://npvg.org | sha256sum
sha256sum assets/installer/install.sh

Probe 1 convicts the ignore gap: ?? deleteme.html before Car 0, absent after. Probe 2 asks the routerโ€™s own book directly, bypassing every list on this machine: a hand-rolled DNS question to the default gateway on port 53, printing the routerโ€™s address, the response code, the answer count, and the last four bytes as an address. RCODE=0 ANCOUNT=1 LAST_A=[REDACTED_IP] in both worlds acquits the override; a timeout, RCODE=5, or the front-door address means the override itself is the fault and Car 1 would change nothing. Probe 3 is Car 1โ€™s gate, the lease the router handed this machine: domain_name_servers = 8.8.8.8 1.1.1.1 before convicts the routerโ€™s DHCP; after Car 1 and a renewed lease it names the router. If it already names the router before, the 8.8.8.8 is local and probe 4 names the file. Probe 5 is Car 1โ€™s straddle: 8.8.8.8 first before, the routerโ€™s address first after. Probe 6: front-door before, [REDACTED_IP] after. Probe 7 is the destination, the coworkerโ€™s command minus the bash: e3b0c442โ€ฆ under curl: (60) before, 59244b08โ€ฆ after. Probe 8 is the gate, 59244b08โ€ฆ in both worlds.

2. NEXT CONTEXT

assets/installer/install.sh
! git status --short | head -5
! GW=$(ip route show default | awk '{print $3; exit}'); .venv/bin/python -c 'import socket,struct,sys; gw=sys.argv[1]; q=b"\xbe\xef\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"+b"".join(bytes([len(p)])+p.encode() for p in "npvg.org".split("."))+b"\x00\x00\x01\x00\x01"; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(3); s.sendto(q,(gw,53)); r=s.recv(512); an=struct.unpack(">H",r[6:8])[0]; print("ROUTER="+gw, "RCODE=%d"%(r[3]&15), "ANCOUNT=%d"%an, "LAST_A="+(socket.inet_ntoa(r[-4:]) if an else "none"))' "$GW"
! nmcli -g DHCP4.OPTION device show 2>/dev/null | grep -iE 'domain_name_servers|routers'
! grep -rn -E 'nameservers' /etc/nixos ~/repos/nixos --include='*.nix' 2>/dev/null | head -8
! grep -E '^nameserver' /etc/resolv.conf
! getent hosts npvg.org
! curl -fsSL --max-time 10 https://npvg.org | sha256sum
! sha256sum assets/installer/install.sh

Drop deleteme.html and the prior article from the next compile; the resolver page has been read. If probe 3 comes back empty (no NetworkManager on this box), save the DHCP Server pageโ€™s DOM the same way and add it back as deleteme.html.

3. PATCHES

Car 0, the ignore gap. One block, one line. patch, app, d, m. Probe 1 reads the tree at call time; no ignition.

Target: .gitignore
[[[SEARCH]]]
deleteme*.txt
[[[DIVIDER]]]
deleteme*
[[[REPLACE]]]

Car 1, the routerโ€™s DHCP. Outside the repo, so its clicks ride in 5A. Ignition is in two parts: Save on the router, then a renewed lease on each client, because the old lease keeps the old list until it expires. On the workstation:

nmcli networking off; sleep 2; nmcli networking on

If probe 4 shows a networking.nameservers line in your NixOS config, that line outranks any lease, and the ignition for this desk is removing it and rebuilding, which needs that file in context and is not this car.

Car 2, optional, the coworkerโ€™s exact command from inside the house. Only after probe 7 reads 59244b08โ€ฆ. Fresh terminal, not the workshop shell, ~/npvgtest absent; then rm -rf ~/npvgtest when the menu appears.

curl -fsSL https://npvg.org | bash -s npvgtest

4. PROMPT

Rule from the LIVE RECEIPTS. Probe 1: ?? deleteme.html before Car 0, gone after; if it still shows, Car 0 did not land. Probe 2 must read RCODE=0 ANCOUNT=1 LAST_A=[REDACTED_IP] in both worlds; that acquits the router's override, and any other reading (timeout, RCODE=5, the front-door address) means the override itself is at fault and Car 1 was the wrong lever, so say which and stop. Probe 3 is Car 1's gate: domain_name_servers naming 8.8.8.8 before convicts the router's DHCP; after Car 1 and a renewed lease it must name the router; if it named the router BEFORE, the 8.8.8.8 came from this machine and probe 4's grep names the line. Probe 5 is the straddle: 8.8.8.8 first before, the router's address first after; if it still reads 8.8.8.8 first after Car 1, the lease was not renewed or a local nameservers line outranks it, and probe 4 says which. Probe 6: front-door before (do not repeat its digits), [REDACTED_IP] after. Probe 7 is the destination: e3b0c442 under curl (60) before, 59244b08 after; if 5 and 6 flipped but 7 did not, the suspect is the nginx door, not DNS. Probe 8 must read 59244b08 in both worlds. If a Car 2 transcript or the Mac lines from 5B are pasted, read them as the CVR they are: dig's answer from the router, the DHCP packet's servers, banner, download, key, handoff, menu. Do not call the installer proven for strangers: the magic-cookie key branch and the Nix-not-found branch are unwitnessed, WSL is untouched, and the router's forward was witnessed only by the phone. No repo patches unless a receipt convicts one. Continue the article for the New-B and close with the five-stage response contract.

5. EXTERNAL DELIVERABLES

A. pfSense DHCP (Car 1, manual clicks).

  1. Services โ†’ DHCP Server, then the tab for the interface your desk sits on (probe 2โ€™s ROUTER= address names its subnet; the address you typed earlier, [REDACTED_IP], is not on honeybotโ€™s subnet, so read the receipt before choosing the tab).
  2. Scroll to the DNS Servers fields. If they hold 8.8.8.8 and 1.1.1.1, clear all of them. Blank means the router hands out its own LAN address, per the resolver pageโ€™s footnote.
  3. Save, and Apply Changes if the page offers it.
  4. Reversal is typing the two addresses back.

Side effect, named so it is not a surprise: every device in the house now resolves through the routerโ€™s unbound in resolver mode, recursing from the roots with DNSSEC on, instead of through Google and Cloudflare directly. If you want those two kept as the upstream, the companion switch is Services โ†’ DNS Resolver โ†’ โ€œEnable Forwarding Mode,โ€ with the two addresses under System โ†’ General Setup. Not required for this ride. Do not touch NAT reflection, do not install the routerโ€™s CA anywhere, and do not use -k; the fix is routing, not trust.

B. The Macโ€™s readings (manual paste; macOS ships dig, has no getent, and hashes with shasum). Run before and after Car 1; the last line only after the router is saved, and it renews the lease and flushes the cache in one go.

dig +short npvg.org @$(netstat -rn -f inet | awk '/^default/{print $2; exit}')
ipconfig getpacket en0 | grep -iE 'domain_name_server|router'
scutil --dns | grep -m4 nameserver
dscacheutil -q host -a name npvg.org
curl -fsSL --max-time 10 https://npvg.org | shasum -a 256
sudo ipconfig set en0 DHCP; sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder

The first line is the Macโ€™s version of probe 2 and should already read [REDACTED_IP]. The second shows whether the DHCP packet itself carries 8.8.8.8, the same conviction as probe 3. If ipconfig getpacket en0 prints nothing, the Mac is on a different interface; en1 is the usual second guess.

MikeLev.in:

Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.

1: Probe: (BEFORE: hand-run, nothing changed yet)

(sys) pipulate $ git status --short | head -5
GW=$(ip route show default | awk '{print $3; exit}'); .venv/bin/python -c 'import socket,struct,sys; gw=sys.argv[1]; q=b"\xbe\xef\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"+b"".join(bytes([len(p)])+p.encode() for p in "npvg.org".split("."))+b"\x00\x00\x01\x00\x01"; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(3); s.sendto(q,(gw,53)); r=s.recv(512); an=struct.unpack(">H",r[6:8])[0]; print("ROUTER="+gw, "RCODE=%d"%(r[3]&15), "ANCOUNT=%d"%an, "LAST_A="+(socket.inet_ntoa(r[-4:]) if an else "none"))' "$GW"
nmcli -g DHCP4.OPTION device show 2>/dev/null | grep -iE 'domain_name_servers|routers'
grep -rn -E 'nameservers' /etc/nixos ~/repos/nixos --include='*.nix' 2>/dev/null | head -8
grep -E '^nameserver' /etc/resolv.conf
getent hosts npvg.org
curl -fsSL --max-time 10 https://npvg.org | sha256sum
sha256sum assets/installer/install.sh
?? deleteme.html
ROUTER=[REDACTED_IP] RCODE=0 ANCOUNT=1 LAST_A=[REDACTED_IP]
dhcp_client_identifier = 01\:ec\:b1\:d7\:3e\:4b\:b2 | dhcp_lease_time = 7200 | dhcp_server_identifier = [REDACTED_IP] | domain_name = wonder.lan | domain_name_servers = 8.8.8.8 1.1.1.1 | expiry = 1789385878 | ip_address = [REDACTED_IP] | requested_broadcast_address = 1 | requested_domain_name = 1 | requested_domain_name_servers = 1 | requested_domain_search = 1 | requested_host_name = 1 | requested_interface_mtu = 1 | requested_ms_classless_static_routes = 1 | requested_nis_domain = 1 | requested_nis_servers = 1 | requested_ntp_servers = 1 | requested_rfc3442_classless_static_routes = 1 | requested_root_path = 1 | requested_routers = 1 | requested_static_routes = 1 | requested_subnet_mask = 1 | requested_time_offset = 1 | requested_wpad = 1 | routers = [REDACTED_IP] | subnet_mask = [REDACTED_IP]
nameserver 8.8.8.8
nameserver 1.1.1.1
nameserver 2600:4041:50e2:5b00:2d0:b4ff:fe04:3685
nameserver 2600:4041:50e6:2b00:2d0:b4ff:fe04:3685
[REDACTED_IP]   npvg.org
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  -
59244b08613cd834d089f3bffa88b7c2cfcf8b40a8f3bbb80fde37fdc04feffc  assets/installer/install.sh
(sys) pipulate $ 

2: Context: (AFTER: the same probes re-run by the compiler as ! lines)

# AD HOC CHOP! The Not-Managed-by-Git Safe-for-Client-Data place. Insert Simpson Couch Gag in white space  below (explain anything to the audience you feel needs it explained)G
# adhoc.txt    _   _   _               ____ _   _  ___  ____  _   
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |  Trying to get the installer to work inside my LAN.
# ahe/ _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |  Yeah, yeah, I'll have to uninstall Nix for the full test soon but not right now.
# ahc ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|  Something about phonebooks.
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)  

# To Human: WELCOME TO VIM! It's really NeoVim but I say vim anyway.
# To AI: This is an alternate 40K view to the big book-ore rolling pin spine.

# 1. **Probe**: Baseline Reading
# 2. **Context**: Post-experiment *planned* reading instructions
# 3. **Patch**: The experiment and how to make it happen
# 4. **Prompt**: Post-experiment instructions and how to read results
# 5. **Deliverable**: How the world is forever different moving forward

# The first thing you need to know here is that everything that comes after the
# hash symbol (#) is commented out โ€” and that's EVERYTHING in this file's default
# state. Begin editing-in lines for inclusion as part of the context or adding
# chunks of new context at the bottom. `Ctrl`+`v`, `j` (repeatedly), `l` (to move
# right), `d` (to delete). Reverse that with `Ctrl`+`v`, `j` (repeatedly),
# `Shift`+`i`, `# `, `Esc` to put the hashes back. You can just arrow-key around
# here with `h`, `j`, `k`, `l`. Save-and-quit is a bit tricky because another
# file is also loaded: `Esc`, `:`, `q`, `w`, `!`

# If this is stressing you out and you're a quitter and want to quit, just type:
# `Esc`, `:`, `q`, `!`, `Enter`. That will exit without saving any changes. If
# you want to get over this hump, type: `Esc`, `:`, `T`, `u`, `t`, `o`, `r`, `Enter`.

# This file is just to make it easy having options of what to edit into context.
# You can use whatever text-file you want to stack file-names and commands to
# build an output text-file with the identically stacked output of each file or
# command. In this way we vertically append or "stack" a bunch of text; simple as
# that. If you understand this concept, you're on your way to future-proofing
# yourself in the Age of AI. Congratulations! Here is how to include web pages:

#    !URL  --------------------------------------------------------------------
#      when    Public page; what a stranger or crawler sees; the BEFORE of a
#              login-wall diagnosis
#      switch  It shows a login page -> `warm URL` once, then `?URL`
#    
#    ?URL  --------------------------------------------------------------------
#      when    Anything behind a login, on the site's persistent profile;
#              `check URL` first
#      switch  The lenses show a shell (nav, an `[Iframe]` leaf, no content) ->
#              read the wire truth for the XHR the frame makes, then call that
#              API with a connector
#    
#    @URL  --------------------------------------------------------------------
#      when    Every re-read of a page already scraped; no browser, no network
#      switch  The cached page is stale or was a login wall -> fresh `!` or `?`
#    
#    $URL  --------------------------------------------------------------------
#      when    Exact markup: meta tags, a JSON blob in a `<script>`
#      note    Token-heavy; needs a prior scrape
#    
#    %URL  --------------------------------------------------------------------
#      when    The network log distilled; SPA endpoint discovery
#      switch  It re-serves the wire truth you already have -> the API
#    
#    ! cmd  -------------------------------------------------------------------
#      when    Any bounded, non-interactive command as a live receipt
#      note    Cap it with `-n`; no aliases, no prompts
#    
#    Connector  ---------------------------------------------------------------
#      when    The number you want is one GET away
#      switch  LIST until the thing isn't in the list -> FETCH by id -> DRILL
#              the path the app's own frame called -> `--grep` to narrow a list
#              or find a leaf

# Every step is one argument longer than the last; the moment a lens shows less than the wire does is the moment to stop scraping.

# STORYTELLING: OPTIONAL, BIG 40K-FOOT VIEW
# ! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs  # <-- ROLLING PIN that gives the 40K foot book-spine view of book-ore (only works for me because of local-only git repo)
# GLOSSARY.md                 # <-- Terms defined like back of J.R.R. Tolkien book, grows as `prompt_foo.py` gets scars and shrinks.
# scripts/articles/lsa.py     # <-- 2ND BRAIN: Search external memory with `rgx`, `rgxc` & `posts` Blogging for Hackers Jekyll-compatible.
# ~/repos/nixos/autognome.py  # <-- Letting the AIs really understand my environment (The Brave Little Tailor punches above Their Weight Class proving the dunning-kruger effect the gate-keeper's (lower-case) lament.)
 
# STILL BIG BUT LESS OPTIONAL (especially flake.nix)
# init.lua                    # <-- Daily driver hot-keys that overlap with aliases in flake.nix. `<leader>m` makes it Science (this process)!
# flake.nix                   # <-- THE ONE BIG THING TO INCLUDE Infrastructure as Code (IaC) tells LLM about your system down to the metal
# prompt_foo.py               # <-- THIS SYSTEM
# foo_files.py                # <-- MAIN ROUTER FILE (alternative to this one)

# TINY BUT EXCELLENT FOR "THIS SYSTEM" CONTEXT (OK to include every time / automatically = `apply.py`, `.gitignore`, `.gitattributes`)
# requirements.in             # <-- All known dependencies and (necessary) version pinning. WORA gotcha's exposed.
# __init__.py                 # <-- Master versioning
# pyproject.toml              # <-- The PyPI Packaging details

# OPTIONAL ACTUATORS (cheap and good to include to expand the AI's capabilities)
# cli.py                      # <-- Catch-all actuator for PyPI envs, Python anchoring, MCP tool-call (plus alternatives) and **kwargs like wrapping for CLI
# scripts/xp.py               # <-- Transforms host OS copy-paste buffer player-piano music into context-payload.
# scripts/ai.py               # <-- How I constantly use local AI to write git commit messages with `m` alias.
# scripts/crawl.py            # <-- Feel free to ask for something to be crawled and included in the next turn.
# scripts/weblogin.py         # <-- Lets the user "warm up" the cache for their web logins at their leisure on a profile that persists.
# scripts/webclip_2_markdown.py  # <-- Surprisingly important program.
 
# MISCELLANEOUS (rare to include but sometimes critical)
# scripts/foo_cartridge.py    # Needs description
# scripts/foo_replay.py       # Needs description
# release.py                  # <-- How everything ends up where it does (GitHub, PyPI, etc.)
# imports/voice_synthesis.py  # <-- The wand can talk to you
# imports/ascii_displays.py   # <-- Where all the ASCII Art lives
# scripts/release/version_sync.py  # <-- Needs to be wrapped into release.py and eliminated, I think.

#                         --- Under this line is were you paste what the AI gives you ---
#                         --- We call it context but it's really just the right-hand  ---
#                         --- blast-radius of the "probes" to make this all science.  ---

# Carry-over as the important work-in-progress parts of the project here just
# like above but not as long-standing overarching to the framework but rather
# for the current hot spots actively being worked on.

# STICKBUG & MOTHER CAT KATA (WORKING ON THE CHAPTER)

# assets/trails/first_context.yaml
# assets/trails/practice.yaml
# assets/trails/public_walk.yaml
# # assets/trails/botify_pageworkers.yaml

# Always include these with whatever connector
# scripts/sources_menu.py
# scripts/connectors/README.md
# scripts/connectors/wallet.py

# scripts/connectors/botify.py
# scripts/connectors/confluence.py
# scripts/connectors/gmail.py
# scripts/connectors/gsc.py
# scripts/connectors/jira.py
# scripts/connectors/mcp.py
# scripts/connectors/mcp_warm.py
# scripts/connectors/sheets.py
# scripts/connectors/slack.py

# The "walk" system is different from the Unix-like tiny connector commands.
# assets/installer/mck.sh
# assets/installer/replay.sh
# scripts/bookmark_import.py
# scripts/walk.py
# scripts/walk_cartridge.py
# scripts/walk_compile.py
# scripts/weblogin.py
# tools/scraper_tools.py
# 
# scripts/mcp_dummy_server.py  
# scripts/boot_menu.py
# scripts/mother_cat.py
# scripts/sources_menu.py

# --- START THIS DISCUSSION ---

# Get things started here! Guess at what context should be included.
# If you get it wrong, you're just wasting 1-turn because the AI will help.
# Un-comment lines, add lines with absolute-path filenames or `! ` commands. 

# Context 1 (Edit-in selections from above and add new files immediately below)
# /home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
# assets/installer/install.sh

# Context 2
# /home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
# assets/installer/install.sh
# nixops.sh
# remotes/honeybot/nixos/configuration.nix
# ! curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
# ! getent hosts npvg.org
# ! sha256sum assets/installer/install.sh
# ! HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum

# Context 3
# /home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
# assets/installer/install.sh
# ! grep -E '^nameserver' /etc/resolv.conf; resolvectl dns 2>/dev/null
# ! getent hosts npvg.org
# ! curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
# ! curl -fsSL --max-time 10 https://npvg.org | sha256sum
# ! HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum
# ! sha256sum assets/installer/install.sh
# deleteme.html

# Context 4
assets/installer/install.sh
! git status --short | head -5
! GW=$(ip route show default | awk '{print $3; exit}'); .venv/bin/python -c 'import socket,struct,sys; gw=sys.argv[1]; q=b"\xbe\xef\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"+b"".join(bytes([len(p)])+p.encode() for p in "npvg.org".split("."))+b"\x00\x00\x01\x00\x01"; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(3); s.sendto(q,(gw,53)); r=s.recv(512); an=struct.unpack(">H",r[6:8])[0]; print("ROUTER="+gw, "RCODE=%d"%(r[3]&15), "ANCOUNT=%d"%an, "LAST_A="+(socket.inet_ntoa(r[-4:]) if an else "none"))' "$GW"
! nmcli -g DHCP4.OPTION device show 2>/dev/null | grep -iE 'domain_name_servers|routers'
! grep -rn -E 'nameservers' /etc/nixos ~/repos/nixos --include='*.nix' 2>/dev/null | head -8
! grep -E '^nameserver' /etc/resolv.conf
! getent hosts npvg.org
! curl -fsSL --max-time 10 https://npvg.org | sha256sum
! sha256sum assets/installer/install.sh
deleteme.html

# --- END `adhoc.txt` TEMPLATE ---

3: Patches: (the one change between the readings)

(nix) pipulate $ patch
(nix) pipulate $ app
โœ… DETERMINISTIC PATCH APPLIED: Successfully mutated '.gitignore'.
(nix) pipulate $ d
diff --git a/.gitignore b/.gitignore
index dc1116d2..0d1af9ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
 # start temp
-deleteme*.txt
+deleteme*
 Notebooks/Advanced_Notebooks/Whatsa_Lambda.ipynb
 patch
 *_true_schema_discovery.json
(nix) pipulate $ m
๐Ÿ“ Committing: chore: Remove obsolete file pattern from .gitignore
[main 7f99b3b1] chore: Remove obsolete file pattern from .gitignore
 1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 48 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 317 bytes | 317.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:pipulate/pipulate.git
   10b46c23..7f99b3b1  main -> main
(nix) pipulate $ 

Ignition (what makes the patched code run before the AFTER reading โ€“ <F2>, nix develop, a re-ride โ€“ or none required):

Iโ€™m checking those DHCP settings. Oh yeah, 1.1.1.1 and 8.8.8.8 were both listed as DNS servers. I deleted them (blanked the fields) and saved the Pageโ€™s settings and restarted services.

4: Prompt: (the next question, written before the AFTER reading is in)

Rule from the LIVE RECEIPTS. Probe 1: ?? deleteme.html before Car 0, gone after; if it still shows, Car 0 did not land. Probe 2 must read RCODE=0 ANCOUNT=1 LAST_A=[REDACTED_IP] in both worlds; that acquits the routerโ€™s override, and any other reading (timeout, RCODE=5, the front-door address) means the override itself is at fault and Car 1 was the wrong lever, so say which and stop. Probe 3 is Car 1โ€™s gate: domain_name_servers naming 8.8.8.8 before convicts the routerโ€™s DHCP; after Car 1 and a renewed lease it must name the router; if it named the router BEFORE, the 8.8.8.8 came from this machine and probe 4โ€™s grep names the line. Probe 5 is the straddle: 8.8.8.8 first before, the routerโ€™s address first after; if it still reads 8.8.8.8 first after Car 1, the lease was not renewed or a local nameservers line outranks it, and probe 4 says which. Probe 6: front-door before (do not repeat its digits), [REDACTED_IP] after. Probe 7 is the destination: e3b0c442 under curl (60) before, 59244b08 after; if 5 and 6 flipped but 7 did not, the suspect is the nginx door, not DNS. Probe 8 must read 59244b08 in both worlds. If a Car 2 transcript or the Mac lines from 5B are pasted, read them as the CVR they are: digโ€™s answer from the router, the DHCP packetโ€™s servers, banner, download, key, handoff, menu. Do not call the installer proven for strangers: the magic-cookie key branch and the Nix-not-found branch are unwitnessed, WSL is untouched, and the routerโ€™s forward was witnessed only by the phone. No repo patches unless a receipt convicts one. Continue the article for the New-B and close with the five-stage response contract.

5: Deliverables: [external artifacts, updates to this system or โ€˜None this turn outside the repoโ€™]

Okay, and that final thing on the Mac.

michaellevin@MichaelMacBook-Pro ~ % dig +short npvg.org @$(netstat -rn -f inet | awk '/^default/{print $2; exit}')
ipconfig getpacket en0 | grep -iE 'domain_name_server|router'
scutil --dns | grep -m4 nameserver
dscacheutil -q host -a name npvg.org
curl -fsSL --max-time 10 https://npvg.org | shasum -a 256
sudo ipconfig set en0 DHCP; sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
[REDACTED_IP]
router (ip_mult): {[REDACTED_IP]}
domain_name_server (ip_mult): {8.8.8.8, 1.1.1.1}
  nameserver[0] : 2600:4041:50e2:5b00:2d0:b4ff:fe04:3685
  nameserver[1] : 2600:4041:50e6:2b00:2d0:b4ff:fe04:3685
  nameserver[2] : 8.8.8.8
  nameserver[3] : 1.1.1.1
name: npvg.org
ip_address: [REDACTED_IP]

curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  -
Password:
michaellevin@MichaelMacBook-Pro ~ % 

I still get the security warning. Is there any bisection we can do to definitively bank a learning?

Fable 5.1 Extra: Context verified. Eight live receipts this compile, and they split cleanly into three groups: Car 0 landed, the routerโ€™s book is right, and every client-side reading is still a BEFORE because the ignition that turns the routerโ€™s change into a clientโ€™s change never fired. Nothing here is a failed ride. It is a changed offer that no tenant has collected.

The receipts, in one breath. Probe 1 is clean: deleteme.html is ignored now, and the file itself still sits in the tree carrying a router session token, so it gets deleted below. Probe 2 acquits the override from the routerโ€™s own mouth: ROUTER=[REDACTED_IP] RCODE=0 ANCOUNT=1 LAST_A=[REDACTED_IP]. The Macโ€™s dig at that same address said the same thing, so two machines have now read the routerโ€™s book directly and both got the back room. That also corrects me: the address you typed earlier was the router, and I doubted it because honeybot is on a different subnet. Your desk and the Mac live on [REDACTED_IP]/24 with the router as their gateway; honeybot lives on [REDACTED_IP]/24, a second subnet behind the same router. Probe 3 convicts the source of the wrong list: the lease says dhcp_server_identifier = [REDACTED_IP] and domain_name_servers = 8.8.8.8 1.1.1.1, so the routerโ€™s DHCP handed out those two, exactly as the phone suggested. Probe 4 is empty: no nameservers line anywhere in your NixOS config, so nothing on this machine outranks the lease. Probes 5, 6, and 7 read as BEFORE. Probe 8 holds the gate at 59244b08โ€ฆ.

For the New-B: DHCP is a lease, not a broadcast. When a machine joins the network it walks up to the routerโ€™s front desk and signs a short contract: here is your address, here is your gateway, here are the phone books to use, come back in two hours. The router changed what it offers to the next visitor at the desk. Your workstation did not visit the desk; it is still holding the contract it signed earlier. Probe 3 even prints the contractโ€™s expiry, and decoding it puts the signing at about 05:38 EDT with a two-hour term. NetworkManager would wander back on its own at the halfway mark, but nothing in the caboose told the desk to go now, so it did not. โ€œRestarted servicesโ€ on the router does not push anything to clients; DHCP is pull. The same is true of the phone: it keeps its old contract until Wi-Fi is toggled.

The Macโ€™s paste is all BEFORE, by the order of its own lines. The flush and the lease renewal were the last command in that block, after dscacheutil and after curl. So the Macโ€™s โ€œfront doorโ€ answer and the Macโ€™s curl: (60) were read from the old contract and the old cache, and the renewal that might have fixed it ran after the last reading. Two lines in that paste are still worth keeping. ipconfig getpacket shows the Macโ€™s contract carries the same two servers, from the same router, which is the DHCP conviction on a second machine. And scutil --dns shows the Macโ€™s list puts the routerโ€™s two IPv6 addresses ahead of Google, yet it still answered front door, which means either the answer came from cache or those IPv6 entries are not answering. That is a question the bisection below settles rather than one I will guess at.

The bisection you asked for: one name, every book asked alone. Right now your machineโ€™s answer for npvg.org is whichever book it opens first, and the books disagree. The clean experiment is to stop letting the machine choose: send the identical question to each server on the list by address, one at a time, and print each answer next to the address that gave it. The prediction is that 8.8.8.8 and 1.1.1.1 each say front door, the routerโ€™s IPv4 address says back room, and each of the two router IPv6 addresses either says back room or times out; a timeout there names a stale prefix that the router is still advertising after Verizon rotated it. Those readings do not change when the lease renews, which is what makes them a bank: they are a property of the books, not of the client. Then the lease renewal is the straddle laid on top: the contractโ€™s domain_name_servers flips from Google to the router, resolv.conf puts the router first, getent flips to the back room, and the coworkerโ€™s command flips from (60) to the digest. Two readings that do not move, four that do, one change between them.

The learning to bank, in one sentence each. A DNS answer is a property of the book you asked, not of the name. A router setting is an offer, and a client holds it only after renewing. A reading taken before the flush is a BEFORE wearing the AFTERโ€™s label.

Leases, Not Broadcasts: The Client-Side Handshake


1. PROBES

for s in 8.8.8.8 1.1.1.1 $(ip route show default | awk '{print $3; exit}') $(awk '/^nameserver/{print $2}' /etc/resolv.conf | grep :); do printf '%s ' "$s"; .venv/bin/python -c 'import socket,struct,sys; s=sys.argv[1]; q=b"\xbe\xef\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"+b"".join(bytes([len(p)])+p.encode() for p in "npvg.org".split("."))+b"\x00\x00\x01\x00\x01"; k=socket.socket(socket.AF_INET6 if ":" in s else socket.AF_INET,socket.SOCK_DGRAM); k.settimeout(2); k.sendto(q,(s,53)); r=k.recv(512); an=struct.unpack(">H",r[6:8])[0]; print("RCODE=%d"%(r[3]&15), "A="+(socket.inet_ntoa(r[-4:]) if an else "none"))' "$s" 2>&1 | tail -1; done
nmcli -g DHCP4.OPTION device show 2>/dev/null | grep -oE 'domain_name_servers = [^|]+|expiry = [0-9]+|dhcp_server_identifier = [^|]+'
grep -E '^nameserver' /etc/resolv.conf
getent hosts npvg.org
curl -fsSL --max-time 10 https://npvg.org | sha256sum
sha256sum assets/installer/install.sh

Probe 1 is the bisection and reads the same in both worlds: Google and Cloudflare say front door, the gateway says [REDACTED_IP], and each IPv6 entry says [REDACTED_IP] if that router address is alive or prints TimeoutError: timed out if it is a stale prefix. Probe 2 is Car 1โ€™s straddle and its gate at once: before, domain_name_servers = 8.8.8.8 1.1.1.1 with expiry = 1789385878; after a renewed lease, the routerโ€™s address and a larger expiry. A new expiry with Google still listed means the DHCP change was made on the wrong interface tab. The old expiry unchanged means the renewal did not fire. Probe 3: Google first before, the router first after; if probe 2 flips but probe 3 does not, a NetworkManager profile pins DNS, and nmcli -g ipv4.dns,ipv4.ignore-auto-dns connection show <active> is the reading that names it. Probe 4: front door before, [REDACTED_IP] after. Probe 5 is the destination: e3b0c442โ€ฆ under curl: (60) before, 59244b08โ€ฆ after. Probe 6 is the gate.

2. NEXT CONTEXT

assets/installer/install.sh
! for s in 8.8.8.8 1.1.1.1 $(ip route show default | awk '{print $3; exit}') $(awk '/^nameserver/{print $2}' /etc/resolv.conf | grep :); do printf '%s ' "$s"; .venv/bin/python -c 'import socket,struct,sys; s=sys.argv[1]; q=b"\xbe\xef\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"+b"".join(bytes([len(p)])+p.encode() for p in "npvg.org".split("."))+b"\x00\x00\x01\x00\x01"; k=socket.socket(socket.AF_INET6 if ":" in s else socket.AF_INET,socket.SOCK_DGRAM); k.settimeout(2); k.sendto(q,(s,53)); r=k.recv(512); an=struct.unpack(">H",r[6:8])[0]; print("RCODE=%d"%(r[3]&15), "A="+(socket.inet_ntoa(r[-4:]) if an else "none"))' "$s" 2>&1 | tail -1; done
! nmcli -g DHCP4.OPTION device show 2>/dev/null | grep -oE 'domain_name_servers = [^|]+|expiry = [0-9]+|dhcp_server_identifier = [^|]+'
! grep -E '^nameserver' /etc/resolv.conf
! getent hosts npvg.org
! curl -fsSL --max-time 10 https://npvg.org | sha256sum
! sha256sum assets/installer/install.sh

Drop deleteme.html from the compile; the resolver page has been read twice and the file carries a router session token.

3. PATCHES

No repo patches required. Three cars, all actuators.

Car 0, cleanup. The file is ignored now, but ignored is not gone.

rm deleteme.html

Car 1, the ignition the last turn named and this turnโ€™s receipts show never fired: walk back to the desk. Run on the workstation, then hand-run probes 2 and 3 before the compile. If the first line is refused for lack of privilege, the second does the same job.

nmcli networking off; sleep 2; nmcli networking on
sudo systemctl restart NetworkManager

The phoneโ€™s version is Wi-Fi off, Wi-Fi on. The Mac already renewed at the tail of the last paste; its AFTER readings ride in 5B.

Car 2, optional, the coworkerโ€™s exact command. Only after probe 5 reads 59244b08โ€ฆ. Fresh terminal, not the workshop shell, ~/npvgtest absent, rm -rf ~/npvgtest when the menu appears. Use the terminal as the witness before the browser: browsers keep a DNS cache of their own, and a โ€œDNS Rebindโ€ page in a tab opened before the flip can outlive the flip by a minute or a restart.

curl -fsSL https://npvg.org | bash -s npvgtest

4. PROMPT

Rule from the LIVE RECEIPTS. Probe 1 is the bisection and is a census, not a straddle: 8.8.8.8 and 1.1.1.1 must say the front-door address (do not repeat its digits), the gateway must say [REDACTED_IP], and each IPv6 entry either says [REDACTED_IP] or prints a timeout; name which IPv6 entries are alive, and if one timed out, say it is a stale advertised prefix and not this ride's fault. Probe 2 is Car 1's straddle and gate: domain_name_servers must name the router with an expiry greater than 1789385878; a new expiry with 8.8.8.8 still listed means the DHCP change landed on the wrong interface tab (the workstation's is the [REDACTED_IP]/24 tab), and an unchanged expiry means Car 1 did not fire, so say which and stop. Probe 3: the router first; if probe 2 flipped and 3 did not, a NetworkManager profile pins DNS. Probe 4: [REDACTED_IP]. Probe 5 is the destination: 59244b08; if 3 and 4 flipped and 5 did not, the suspect is the nginx door and the --connect-to digest is the reading that separates them. Probe 6 must read 59244b08 in both worlds. If the Mac lines from 5B are pasted, read them as the CVR they are: the lease's servers, each book's answer, the cache, the digest. Do not call the installer proven for strangers: the magic-cookie key branch and the Nix-not-found branch are unwitnessed, WSL is untouched, and the router's forward was witnessed only by the phone. No repo patches unless a receipt convicts one. Continue the article for the New-B and close with the five-stage response contract.

5. EXTERNAL DELIVERABLES

A. pfSense, one check, no new change (manual). Services โ†’ DHCP Server has one tab per interface. The tab that matters for your desk, the Mac, and the phone is the one whose subnet is [REDACTED_IP]/24, the same one whose gateway printed as ROUTER= in probe 2. Confirm its DNS Servers fields are the ones you blanked. Honeybotโ€™s [REDACTED_IP]/24 tab is a separate contract and does not affect these three clients.

B. The Macโ€™s AFTER (manual paste; the renewal already ran). Same bisection, same straddle, read in the same order the workstation reads them.

ipconfig getpacket en0 | grep -iE 'domain_name_server|lease_time'
for s in $(scutil --dns | awk '/nameserver\[/{print $3}' | sort -u); do printf '%s -> %s\n' "$s" "$(dig +short +time=2 +tries=1 npvg.org @"$s" | head -1)"; done
dscacheutil -q host -a name npvg.org
curl -fsSL --max-time 10 https://npvg.org | shasum -a 256

The first line is the Macโ€™s probe 2: {[REDACTED_IP]} means the new contract was collected and the right tab was changed; {8.8.8.8, 1.1.1.1} with a new lease means the wrong tab. The loop is the Macโ€™s probe 1 and answers the IPv6 question from a second vantage. The last two are the Macโ€™s probes 4 and 5, and this time they run after the flush instead of before it.

MikeLev.in:

Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.

1: Probe: (BEFORE: hand-run, nothing changed yet)

(sys) pipulate $ for s in 8.8.8.8 1.1.1.1 $(ip route show default | awk '{print $3; exit}') $(awk '/^nameserver/{print $2}' /etc/resolv.conf | grep :); do printf '%s ' "$s"; .venv/bin/python -c 'import socket,struct,sys; s=sys.argv[1]; q=b"\xbe\xef\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"+b"".join(bytes([len(p)])+p.encode() for p in "npvg.org".split("."))+b"\x00\x00\x01\x00\x01"; k=socket.socket(socket.AF_INET6 if ":" in s else socket.AF_INET,socket.SOCK_DGRAM); k.settimeout(2); k.sendto(q,(s,53)); r=k.recv(512); an=struct.unpack(">H",r[6:8])[0]; print("RCODE=%d"%(r[3]&15), "A="+(socket.inet_ntoa(r[-4:]) if an else "none"))' "$s" 2>&1 | tail -1; done
nmcli -g DHCP4.OPTION device show 2>/dev/null | grep -oE 'domain_name_servers = [^|]+|expiry = [0-9]+|dhcp_server_identifier = [^|]+'
grep -E '^nameserver' /etc/resolv.conf
getent hosts npvg.org
curl -fsSL --max-time 10 https://npvg.org | sha256sum
sha256sum assets/installer/install.sh
8.8.8.8 RCODE=0 A=[REDACTED_IP]
1.1.1.1 RCODE=0 A=[REDACTED_IP]
[REDACTED_IP] RCODE=0 A=[REDACTED_IP]
2600:4041:50e2:5b00:2d0:b4ff:fe04:3685 TimeoutError: timed out
2600:4041:50e6:2b00:2d0:b4ff:fe04:3685 TimeoutError: timed out
dhcp_server_identifier = [REDACTED_IP] 
domain_name_servers = 8.8.8.8 1.1.1.1 
expiry = 1789385878
domain_name_servers = 1 
nameserver 8.8.8.8
nameserver 1.1.1.1
nameserver 2600:4041:50e2:5b00:2d0:b4ff:fe04:3685
nameserver 2600:4041:50e6:2b00:2d0:b4ff:fe04:3685
[REDACTED_IP]   npvg.org
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855  -
59244b08613cd834d089f3bffa88b7c2cfcf8b40a8f3bbb80fde37fdc04feffc  assets/installer/install.sh
(sys) pipulate $ 

2: Context: (AFTER: the same probes re-run by the compiler as ! lines)

# AD HOC CHOP! The Not-Managed-by-Git Safe-for-Client-Data place. Insert Simpson Couch Gag in white space  below (explain anything to the audience you feel needs it explained)G
# adhoc.txt    _   _   _               ____ _   _  ___  ____  _   
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |  Trying to get the installer to work inside my LAN.
# ahe/ _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |  Yeah, yeah, I'll have to uninstall Nix for the full test soon but not right now.
# ahc ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|  Something about phonebooks.
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)  This is getting fatiguing.

# To Human: WELCOME TO VIM! It's really NeoVim but I say vim anyway.
# To AI: This is an alternate 40K view to the big book-ore rolling pin spine.

# 1. **Probe**: Baseline Reading
# 2. **Context**: Post-experiment *planned* reading instructions
# 3. **Patch**: The experiment and how to make it happen
# 4. **Prompt**: Post-experiment instructions and how to read results
# 5. **Deliverable**: How the world is forever different moving forward

# The first thing you need to know here is that everything that comes after the
# hash symbol (#) is commented out โ€” and that's EVERYTHING in this file's default
# state. Begin editing-in lines for inclusion as part of the context or adding
# chunks of new context at the bottom. `Ctrl`+`v`, `j` (repeatedly), `l` (to move
# right), `d` (to delete). Reverse that with `Ctrl`+`v`, `j` (repeatedly),
# `Shift`+`i`, `# `, `Esc` to put the hashes back. You can just arrow-key around
# here with `h`, `j`, `k`, `l`. Save-and-quit is a bit tricky because another
# file is also loaded: `Esc`, `:`, `q`, `w`, `!`

# If this is stressing you out and you're a quitter and want to quit, just type:
# `Esc`, `:`, `q`, `!`, `Enter`. That will exit without saving any changes. If
# you want to get over this hump, type: `Esc`, `:`, `T`, `u`, `t`, `o`, `r`, `Enter`.

# This file is just to make it easy having options of what to edit into context.
# You can use whatever text-file you want to stack file-names and commands to
# build an output text-file with the identically stacked output of each file or
# command. In this way we vertically append or "stack" a bunch of text; simple as
# that. If you understand this concept, you're on your way to future-proofing
# yourself in the Age of AI. Congratulations! Here is how to include web pages:

#    !URL  --------------------------------------------------------------------
#      when    Public page; what a stranger or crawler sees; the BEFORE of a
#              login-wall diagnosis
#      switch  It shows a login page -> `warm URL` once, then `?URL`
#    
#    ?URL  --------------------------------------------------------------------
#      when    Anything behind a login, on the site's persistent profile;
#              `check URL` first
#      switch  The lenses show a shell (nav, an `[Iframe]` leaf, no content) ->
#              read the wire truth for the XHR the frame makes, then call that
#              API with a connector
#    
#    @URL  --------------------------------------------------------------------
#      when    Every re-read of a page already scraped; no browser, no network
#      switch  The cached page is stale or was a login wall -> fresh `!` or `?`
#    
#    $URL  --------------------------------------------------------------------
#      when    Exact markup: meta tags, a JSON blob in a `<script>`
#      note    Token-heavy; needs a prior scrape
#    
#    %URL  --------------------------------------------------------------------
#      when    The network log distilled; SPA endpoint discovery
#      switch  It re-serves the wire truth you already have -> the API
#    
#    ! cmd  -------------------------------------------------------------------
#      when    Any bounded, non-interactive command as a live receipt
#      note    Cap it with `-n`; no aliases, no prompts
#    
#    Connector  ---------------------------------------------------------------
#      when    The number you want is one GET away
#      switch  LIST until the thing isn't in the list -> FETCH by id -> DRILL
#              the path the app's own frame called -> `--grep` to narrow a list
#              or find a leaf

# Every step is one argument longer than the last; the moment a lens shows less than the wire does is the moment to stop scraping.

# STORYTELLING: OPTIONAL, BIG 40K-FOOT VIEW
# ! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs  # <-- ROLLING PIN that gives the 40K foot book-spine view of book-ore (only works for me because of local-only git repo)
# GLOSSARY.md                 # <-- Terms defined like back of J.R.R. Tolkien book, grows as `prompt_foo.py` gets scars and shrinks.
# scripts/articles/lsa.py     # <-- 2ND BRAIN: Search external memory with `rgx`, `rgxc` & `posts` Blogging for Hackers Jekyll-compatible.
# ~/repos/nixos/autognome.py  # <-- Letting the AIs really understand my environment (The Brave Little Tailor punches above Their Weight Class proving the dunning-kruger effect the gate-keeper's (lower-case) lament.)
 
# STILL BIG BUT LESS OPTIONAL (especially flake.nix)
# init.lua                    # <-- Daily driver hot-keys that overlap with aliases in flake.nix. `<leader>m` makes it Science (this process)!
# flake.nix                   # <-- THE ONE BIG THING TO INCLUDE Infrastructure as Code (IaC) tells LLM about your system down to the metal
# prompt_foo.py               # <-- THIS SYSTEM
# foo_files.py                # <-- MAIN ROUTER FILE (alternative to this one)

# TINY BUT EXCELLENT FOR "THIS SYSTEM" CONTEXT (OK to include every time / automatically = `apply.py`, `.gitignore`, `.gitattributes`)
# requirements.in             # <-- All known dependencies and (necessary) version pinning. WORA gotcha's exposed.
# __init__.py                 # <-- Master versioning
# pyproject.toml              # <-- The PyPI Packaging details

# OPTIONAL ACTUATORS (cheap and good to include to expand the AI's capabilities)
# cli.py                      # <-- Catch-all actuator for PyPI envs, Python anchoring, MCP tool-call (plus alternatives) and **kwargs like wrapping for CLI
# scripts/xp.py               # <-- Transforms host OS copy-paste buffer player-piano music into context-payload.
# scripts/ai.py               # <-- How I constantly use local AI to write git commit messages with `m` alias.
# scripts/crawl.py            # <-- Feel free to ask for something to be crawled and included in the next turn.
# scripts/weblogin.py         # <-- Lets the user "warm up" the cache for their web logins at their leisure on a profile that persists.
# scripts/webclip_2_markdown.py  # <-- Surprisingly important program.
 
# MISCELLANEOUS (rare to include but sometimes critical)
# scripts/foo_cartridge.py    # Needs description
# scripts/foo_replay.py       # Needs description
# release.py                  # <-- How everything ends up where it does (GitHub, PyPI, etc.)
# imports/voice_synthesis.py  # <-- The wand can talk to you
# imports/ascii_displays.py   # <-- Where all the ASCII Art lives
# scripts/release/version_sync.py  # <-- Needs to be wrapped into release.py and eliminated, I think.

#                         --- Under this line is were you paste what the AI gives you ---
#                         --- We call it context but it's really just the right-hand  ---
#                         --- blast-radius of the "probes" to make this all science.  ---

# Carry-over as the important work-in-progress parts of the project here just
# like above but not as long-standing overarching to the framework but rather
# for the current hot spots actively being worked on.

# STICKBUG & MOTHER CAT KATA (WORKING ON THE CHAPTER)

# assets/trails/first_context.yaml
# assets/trails/practice.yaml
# assets/trails/public_walk.yaml
# # assets/trails/botify_pageworkers.yaml

# Always include these with whatever connector
# scripts/sources_menu.py
# scripts/connectors/README.md
# scripts/connectors/wallet.py

# scripts/connectors/botify.py
# scripts/connectors/confluence.py
# scripts/connectors/gmail.py
# scripts/connectors/gsc.py
# scripts/connectors/jira.py
# scripts/connectors/mcp.py
# scripts/connectors/mcp_warm.py
# scripts/connectors/sheets.py
# scripts/connectors/slack.py

# The "walk" system is different from the Unix-like tiny connector commands.
# assets/installer/mck.sh
# assets/installer/replay.sh
# scripts/bookmark_import.py
# scripts/walk.py
# scripts/walk_cartridge.py
# scripts/walk_compile.py
# scripts/weblogin.py
# tools/scraper_tools.py
# 
# scripts/mcp_dummy_server.py  
# scripts/boot_menu.py
# scripts/mother_cat.py
# scripts/sources_menu.py

# --- START THIS DISCUSSION ---

# Get things started here! Guess at what context should be included.
# If you get it wrong, you're just wasting 1-turn because the AI will help.
# Un-comment lines, add lines with absolute-path filenames or `! ` commands. 

# Context 1 (Edit-in selections from above and add new files immediately below)
# /home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
# assets/installer/install.sh

# Context 2
# /home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
# assets/installer/install.sh
# nixops.sh
# remotes/honeybot/nixos/configuration.nix
# ! curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
# ! getent hosts npvg.org
# ! sha256sum assets/installer/install.sh
# ! HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum

# Context 3
# /home/mike/repos/trimnoir/_posts/2026-09-14-chain-of-custody-multi-model-review.md
# assets/installer/install.sh
# ! grep -E '^nameserver' /etc/resolv.conf; resolvectl dns 2>/dev/null
# ! getent hosts npvg.org
# ! curl -kIsS --max-time 10 -v https://npvg.org -o /dev/null 2>&1 | grep -iE 'subject:|issuer:|verify|x-frame-options|^< HTTP'
# ! curl -fsSL --max-time 10 https://npvg.org | sha256sum
# ! HB=$(ssh -G honeybot | awk '/^hostname /{print $2}'); curl -fsSL --max-time 10 --connect-to "npvg.org:443:${HB}:443" https://npvg.org | sha256sum
# ! sha256sum assets/installer/install.sh
# deleteme.html

# Context 4
# assets/installer/install.sh
# ! git status --short | head -5
# ! GW=$(ip route show default | awk '{print $3; exit}'); .venv/bin/python -c 'import socket,struct,sys; gw=sys.argv[1]; q=b"\xbe\xef\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"+b"".join(bytes([len(p)])+p.encode() for p in "npvg.org".split("."))+b"\x00\x00\x01\x00\x01"; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.settimeout(3); s.sendto(q,(gw,53)); r=s.recv(512); an=struct.unpack(">H",r[6:8])[0]; print("ROUTER="+gw, "RCODE=%d"%(r[3]&15), "ANCOUNT=%d"%an, "LAST_A="+(socket.inet_ntoa(r[-4:]) if an else "none"))' "$GW"
# ! nmcli -g DHCP4.OPTION device show 2>/dev/null | grep -iE 'domain_name_servers|routers'
# ! grep -rn -E 'nameservers' /etc/nixos ~/repos/nixos --include='*.nix' 2>/dev/null | head -8
# ! grep -E '^nameserver' /etc/resolv.conf
# ! getent hosts npvg.org
# ! curl -fsSL --max-time 10 https://npvg.org | sha256sum
# ! sha256sum assets/installer/install.sh
# deleteme.html

# Context 5
assets/installer/install.sh
! for s in 8.8.8.8 1.1.1.1 $(ip route show default | awk '{print $3; exit}') $(awk '/^nameserver/{print $2}' /etc/resolv.conf | grep :); do printf '%s ' "$s"; .venv/bin/python -c 'import socket,struct,sys; s=sys.argv[1]; q=b"\xbe\xef\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"+b"".join(bytes([len(p)])+p.encode() for p in "npvg.org".split("."))+b"\x00\x00\x01\x00\x01"; k=socket.socket(socket.AF_INET6 if ":" in s else socket.AF_INET,socket.SOCK_DGRAM); k.settimeout(2); k.sendto(q,(s,53)); r=k.recv(512); an=struct.unpack(">H",r[6:8])[0]; print("RCODE=%d"%(r[3]&15), "A="+(socket.inet_ntoa(r[-4:]) if an else "none"))' "$s" 2>&1 | tail -1; done
! nmcli -g DHCP4.OPTION device show 2>/dev/null | grep -oE 'domain_name_servers = [^|]+|expiry = [0-9]+|dhcp_server_identifier = [^|]+'
! grep -E '^nameserver' /etc/resolv.conf
! getent hosts npvg.org
! curl -fsSL --max-time 10 https://npvg.org | sha256sum
! sha256sum assets/installer/install.sh

# --- END `adhoc.txt` TEMPLATE ---

3: Patches: (the one change between the readings)

(sys) pipulate $ rm deleteme.html
(sys) pipulate $ 

And:

(sys) pipulate $ nmcli networking off; sleep 2; nmcli networking on
sudo systemctl restart NetworkManager
(sys) pipulate $ 

And now the test on Linux:

(sys) pipulate $ curl -fsSL https://npvg.org | bash -s npvgtest

--------------------------------------------------------------
   ๐Ÿš€ Welcome to the Npvgtest Installer ๐Ÿš€
   Local-first, Nix-reproducible, and yours to delete.
   A context compiler. Prompt well.
--------------------------------------------------------------

๐Ÿ” Checking prerequisites...
โœ… All required tools found.

๐Ÿ“ Checking target directory: /home/mike/npvgtest
โœ… Target directory is available.
๐Ÿ“ Creating directory '/home/mike/npvgtest'
๐Ÿ“ฅ Downloading Pipulate source code...
  #=#=- #   #                                                                  
โœ… Download complete.

๐Ÿ“ฆ Extracting source code...
โœ… Extraction complete. Source code installed to '/home/mike/npvgtest'.

๐Ÿ“ Now in directory: /home/mike/npvgtest

๐Ÿ”‘ Fetching the shared deploy key from https://pipulate.com/key.rot...
   (Public, ROT13-encoded, pull-only: it exists so this folder can fetch
    updates without a GitHub account. nix develop decodes it into
    ~/.ssh/id_rsa only if no key is there already.)
โœ… Deploy key downloaded.
๐Ÿ”’ Deploy key saved as .ssh/rot (mode 600).

๐Ÿš€ Starting the Npvgtest environment...
--------------------------------------------------------------
  Source is in place at: /home/mike/npvgtest  
  To come back later, run:  
  cd /home/mike/npvgtest && nix develop -L  
--------------------------------------------------------------

Setting up app identity as 'npvgtest'...
โœ… Application identity set.

Creating ./run -- a one-file shortcut for the cd-and-nix-develop line above.

Next, nix develop builds the environment and turns this folder into a
git repository (the 'magic cookie' step) so it can auto-update from now on.
๐Ÿš€ Booting the Forever Machine...
Please wait while the Nix environment hydrates...
๐Ÿ”„ Transforming installation into git repository...
Creating temporary clone in /tmp/nix-shell.ZeYy5a/tmp.cVLyXEmKmS...
Cloning into '/tmp/nix-shell.ZeYy5a/tmp.cVLyXEmKmS'...
remote: Enumerating objects: 381, done.
remote: Counting objects: 100% (381/381), done.
remote: Compressing objects: 100% (333/333), done.
remote: Total 381 (delta 25), reused 239 (delta 21), pack-reused 0 (from 0)
Receiving objects: 100% (381/381), 3.20 MiB | 20.47 MiB/s, done.
Resolving deltas: 100% (25/25), done.
Preserving app identity and credentials...
Creating backup of current directory in /tmp/nix-shell.ZeYy5a/tmp.kTCvl08vJk...
Moving git repository into place...
โœ… Successfully transformed into git repository!
Original files backed up to: /tmp/nix-shell.ZeYy5a/tmp.kTCvl08vJk
Already up to date.
Found ~/.ssh/id_rsa -- switching the git remote to SSH so auto-updates use it.
INFO: Setting up your personal Playground...
(Nix) 2.25.0pre20240910_b9d3cdfb ยท Python 3.12.13 ยท v2.55 ยท ~/npvgtest
๐Ÿ”ง Fresh install detected โ€” packages downloading...
โœ… 290 packages ready.
๐Ÿ““ 12 starter file(s) copied into Notebooks/ -- yours to edit.
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ nix develop -- a reproducible *nix shell :: pick a door โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚                                                                                                                                                                                   โ”‚
โ”‚  [1]  JupyterLab tab     both servers start; JupyterLab opens in the browser                                                                                                      โ”‚
โ”‚  [2]  Text Commands      nothing starts -- five words wait at the prompt                                                                                                          โ”‚
โ”‚  [3]  Pipulate tab       both servers start; the app opens in the browser                                                                                                         โ”‚
โ”‚                                                                                                                                                                                   โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ waiting for your choice -- Ctrl+C also drops to the shell โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

[2] Text Commands. Nothing started -- no JupyterLab, no server.

Five words to start from:
  walk      take the guided tour -- public pages, nothing to log into
  sources   see what this shell can reach outside this machine
  brief     compile this workshop into your clipboard for an AI -- the context compiler's first job
  pu        change your mind and start the app server after all
  menu      print this list again once it scrolls away
(nix) npvgtest $ 

Okay nice! Now we do it on the Mac. I toggle off and on the WiFi. Nice trick!

michaellevin@MichaelMacBook-Pro ~ % rm -rf pipulate
michaellevin@MichaelMacBook-Pro ~ % curl -fsSL https://npvg.org | bash

--------------------------------------------------------------
   ๐Ÿš€ Welcome to the Pipulate Installer ๐Ÿš€
   Local-first, Nix-reproducible, and yours to delete.
   A context compiler. Prompt well.
--------------------------------------------------------------

๐Ÿ” Checking prerequisites...
โœ… All required tools found.

๐Ÿ“ Checking target directory: /Users/michaellevin/pipulate
โœ… Target directory is available.
๐Ÿ“ Creating directory '/Users/michaellevin/pipulate'
๐Ÿ“ฅ Downloading Pipulate source code...
  #-#O=-#   #                                                                  
โœ… Download complete.

๐Ÿ“ฆ Extracting source code...
โœ… Extraction complete. Source code installed to '/Users/michaellevin/pipulate'.

๐Ÿ“ Now in directory: /Users/michaellevin/pipulate

๐Ÿ”‘ Fetching the shared deploy key from https://pipulate.com/key.rot...
   (Public, ROT13-encoded, pull-only: it exists so this folder can fetch
    updates without a GitHub account. nix develop decodes it into
    ~/.ssh/id_rsa only if no key is there already.)
โœ… Deploy key downloaded.
๐Ÿ”’ Deploy key saved as .ssh/rot (mode 600).

๐Ÿš€ Starting the Pipulate environment...
--------------------------------------------------------------
  Source is in place at: /Users/michaellevin/pipulate  
  To come back later, run:  
  cd /Users/michaellevin/pipulate && nix develop -L  
--------------------------------------------------------------

Setting up app identity as 'pipulate'...
โœ… Application identity set.

Creating ./run -- a one-file shortcut for the cd-and-nix-develop line above.

Next, nix develop builds the environment and turns this folder into a
git repository (the 'magic cookie' step) so it can auto-update from now on.
๐Ÿš€ Booting the Forever Machine...
Please wait while the Nix environment hydrates...
๐Ÿ”„ Transforming installation into git repository...
Creating temporary clone in /tmp/nix-shell.PqUtlh/tmp.MDAnTp0peW...
Cloning into '/tmp/nix-shell.PqUtlh/tmp.MDAnTp0peW'...
remote: Enumerating objects: 381, done.
remote: Counting objects: 100% (381/381), done.
remote: Compressing objects: 100% (333/333), done.
remote: Total 381 (delta 25), reused 239 (delta 21), pack-reused 0 (from 0)
Receiving objects: 100% (381/381), 3.20 MiB | 20.99 MiB/s, done.
Resolving deltas: 100% (25/25), done.
Preserving app identity and credentials...
Creating backup of current directory in /tmp/nix-shell.PqUtlh/tmp.HDbSqZXsYy...
Moving git repository into place...
โœ… Successfully transformed into git repository!
Original files backed up to: /tmp/nix-shell.PqUtlh/tmp.HDbSqZXsYy
Already up to date.
Found ~/.ssh/id_rsa -- switching the git remote to SSH so auto-updates use it.
INFO: Setting up your personal Playground...
(Determinate Nix 3.19.1) 2.34.6 ยท Python 3.12.13 ยท v2.55 ยท ~/pipulate
๐Ÿ”ง Fresh install detected โ€” packages downloading...
โœ… 291 packages ready.
๐Ÿ““ 12 starter file(s) copied into Notebooks/ -- yours to edit.
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ nix develop -- a reproducible *nix shell :: pick a door โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚                                                                                             โ”‚
โ”‚  [1]  JupyterLab tab     both servers start; JupyterLab opens in the browser                โ”‚
โ”‚  [2]  Text Commands      nothing starts -- five words wait at the prompt                    โ”‚
โ”‚  [3]  Pipulate tab       both servers start; the app opens in the browser                   โ”‚
โ”‚                                                                                             โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ waiting for your choice -- Ctrl+C also drops to the shell โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

[2] Text Commands. Nothing started -- no JupyterLab, no server.

Five words to start from:
  walk      take the guided tour -- public pages, nothing to log into
  sources   see what this shell can reach outside this machine
  brief     compile this workshop into your clipboard for an AI -- the context compiler's first job
  pu        change your mind and start the app server after all
  menu      print this list again once it scrolls away
(nix:nix-shell-env) (nix) pipulate $

And I check npvg.org from my iPhone, same toggling WiFi off and on. Yep, that works too!

Verifying the Flow: Cross-Platform Parity Inside the Perimeter

4: Prompt: (the next question, written before the AFTER reading is in)

Rule from the LIVE RECEIPTS. Probe 1 is the bisection and is a census, not a straddle: 8.8.8.8 and 1.1.1.1 must say the front-door address (do not repeat its digits), the gateway must say [REDACTED_IP], and each IPv6 entry either says [REDACTED_IP] or prints a timeout; name which IPv6 entries are alive, and if one timed out, say it is a stale advertised prefix and not this rideโ€™s fault. Probe 2 is Car 1โ€™s straddle and gate: domain_name_servers must name the router with an expiry greater than 1789385878; a new expiry with 8.8.8.8 still listed means the DHCP change landed on the wrong interface tab (the workstationโ€™s is the [REDACTED_IP]/24 tab), and an unchanged expiry means Car 1 did not fire, so say which and stop. Probe 3: the router first; if probe 2 flipped and 3 did not, a NetworkManager profile pins DNS. Probe 4: [REDACTED_IP]. Probe 5 is the destination: 59244b08; if 3 and 4 flipped and 5 did not, the suspect is the nginx door and the โ€“connect-to digest is the reading that separates them. Probe 6 must read 59244b08 in both worlds. If the Mac lines from 5B are pasted, read them as the CVR they are: the leaseโ€™s servers, each bookโ€™s answer, the cache, the digest. Do not call the installer proven for strangers: the magic-cookie key branch and the Nix-not-found branch are unwitnessed, WSL is untouched, and the routerโ€™s forward was witnessed only by the phone. No repo patches unless a receipt convicts one. Continue the article for the New-B and close with the five-stage response contract.

5: Deliverables: [external artifacts, updates to this system or โ€˜None this turn outside the repoโ€™]

This looks good! I think Iโ€™ve got the new domainโ€™s install command working cross-platform and testable from inside my LAN. This is good. This is the deliverable, plus this article.

Hop off the ride. This rideโ€™s stated goal is reached โ€” dismount. This is the NOTARY BEAT: the ride ends here, is witnessed here, and is sealed here. Answer all seven beats, briefly:

  1. TL;DR: a short, dry, neutral abstract for the TOP of the published article โ€” written for an unfamiliar reader or AI summarizer who has never seen this system. No hype, no insider handles unexplained.
  2. VERIFY: restate the goal from the top of this article and confirm (or deny) it was met, citing THIS compileโ€™s receipts, not memory. Name any ignition this ride required that never fired โ€“ an AFTER tap taken without one is a stale BEFORE wearing the AFTERโ€™s label.
  3. BANK: name everything that graduates โ€” rule, earmark, todo, pin โ€” as SEARCH/REPLACE patch cars against raw source present in THIS context (Target line inside the fence, one car per commit story), deletions included. A โ€˜paste-readyโ€™ line with no Target and no SEARCH anchor is not banked; it is a hand edit the operator will not make. These BANK cars are the ONLY patches a dismount emits.
  4. DANGLING: what carries forward unbanked? One line each, no essays.
  5. SEED: the adhoc.txt lines (and TODO_SLUGS if narrative context is needed) for the next rideโ€™s first compile.
  6. CLOSING: a closing summary for the BOTTOM of the article โ€” the final take-away, tied to the bookโ€™s larger arc where it fits naturally, never forced. Storytelling over inventory.
  7. NOTARIZE: the artifact of record is the cartridge THIS compile writes after this payload is assembled, so its name is not in your context and you must not guess one. Attest the FINGERPRINT instead: this Manifestโ€™s wc -c line, its receipt count, and the newest receiptโ€™s date โ€“ pure functions of the bytes the cartridge seals. Close with one line the operator completes after the compile: Deed: foo-________.zip (ls -t foo-*.zip | head -1) The seal is the signature; the fingerprint is how the deed is found.

The Notary Beat: Sealing the Bisection Receipt

FINALITY: after beat 6, this discussion is CLOSED. Emit NO five-car train, NO probes, NO patches beyond the BANK cars of beat 2, and NO next-turn prompt beyond the SEED lines in beat 4. Any reader or model encountering this article later should treat it as a finished, notarized document โ€” an archive entry, not an open thread.

Fable 5.1 Extra: Context verified. Six live receipts this compile, every one reading AFTER, and this is the dismount.

1. VERIFY

The goal at the top of this article was an installer that works from inside the LAN with the exact command a stranger types, on both platforms. Met, by this compileโ€™s receipts. Probe 1, the bisection: Google and Cloudflare each answer the front-door address; the router at [REDACTED_IP] answers [REDACTED_IP]; the one IPv6 entry still on the list times out. Probe 2, the lease: domain_name_servers = [REDACTED_IP], dhcp_server_identifier = [REDACTED_IP], expiry = 1789389089, which is greater than the old 1789385878 and decodes to a contract signed at about 06:31 EDT, seven minutes before NetworkManager would have renewed on its own, so the forced renewal is what fired. (The trailing domain_name_servers = 1 is requested_domain_name_servers = 1 bleeding through the -o regex; it is not a second server.) Probe 3: the router first in resolv.conf, and the stale IPv6 prefix is gone from the list. Probe 4: [REDACTED_IP]. Probe 5, the destination: 59244b08โ€ฆ with no error on stderr. Probe 6: 59244b08โ€ฆ, the gate. The pasted transcripts agree: Linux ran curl -fsSL https://npvg.org | bash -s npvgtest through the banner, download, key, identity, git transformation, 290 packages, and the menu into Text Commands; the Mac ran curl -fsSL https://npvg.org | bash to the same menu under the default name; the phone works on Wi-Fi after a toggle, on your word, unmeasured. Every ignition this ride named fired: the DHCP fields blanked on the router, the lease renewed on the workstation, Wi-Fi toggled on the Mac and the phone, and deleteme.html deleted, which is why the compilerโ€™s integrity alert now names it as a broken reference in a commented-out adhoc line. None unfired.

2. BANK

One car. The earmark rides on the line that changed.

Target: .gitignore
[[[SEARCH]]]
deleteme*
[[[DIVIDER]]]
# IGNORED IS NOT GONE (2026-09-14). A router admin page saved as a hydrated
# DOM landed in the repo root as deleteme.html, carrying a CSRF token and
# the router's internal hostname, and this line covered only deleteme*.txt.
# It sat untracked through two compiles, one blast from public, and the
# compiler kept reading it into payloads. Widened to every extension, and
# the file was still deleted by hand: a gitignore pattern hides a file from
# git, not from the disk, the compiler, or a probe's glob.
deleteme*
[[[REPLACE]]]

patch, app, d, m, git push. No ignition; nothing reads this line at runtime.

3. DANGLING

The two stranger branches, the magic-cookie key decode and the Nix-not-found path, plus WSL, remain unwitnessed; a throwaway machine or VM is the witness, not an uninstall.

install.shโ€™s Nix-not-found branch tells a stranger who arrived via npvg.org to re-run from pipulate.com; same bytes behind both doors, and any edit changes the digest all three projections currently share, so it waits for a ride that republishes.

The router advertises its own IPv6 address as a resolver and it does not answer on port 53, under the old prefix or the current one; IPv6-first clients pay a timeout before falling through. Router hygiene, not this rideโ€™s fault.

The routerโ€™s admin certificate expired 2026-05-27; 139 notices say so.

All LAN DNS now recurses through the routerโ€™s unbound with DNSSEC; Forwarding Mode is the switch if Google and Cloudflare are wanted upstream. A choice, not a defect.

Two protocol lessons belong in the checklist and the glossary, neither in context: an ignition for a network setting lives on the client (the lease), not the server (restart services); and a flush or renew line pasted after the readings makes every reading above it a BEFORE.

The stdlib DNS-packet probe, one question to every book by address, is a reusable chisel strike that wants a home in scripts/.

Honeybotโ€™s own subnet has its own DHCP tab and was not measured; the mikelev.in override is live for LAN clients and was not measured either.

The Macโ€™s prompt shows a doubled (nix:nix-shell-env) (nix) prefix under Determinate Nix; cosmetic.

4. SEED

assets/installer/install.sh
! sha256sum assets/installer/install.sh
! curl -fsSL --max-time 10 https://npvg.org | sha256sum
! curl -fsSL --max-time 10 https://pipulate.com/install.sh | sha256sum
! ssh -o ConnectTimeout=5 honeybot 'systemctl list-timers --all --no-pager --no-legend "acme-renew-npvg*"'

Three digests as the standing gate, and one census the last article left open: the renew timerโ€™s first firing, which was due this morning and has a LAST column now.

5. CLOSING

The fix was two blank fields on a router. The ride took five compiles, and the distance between those two facts is the whole lesson. Every reading along the way was correct; what kept moving was the vantage. The certificate probe was right that the door was wrong. The override was right the moment it was saved, and two machines read it directly from the router to prove it. The lease was right about what the router had offered, and the renewal was right about when the offer was collected. At no point did anything lie. The name npvg.org simply meant a different address depending on which book was asked, and the list of books was a contract each machine had signed and not yet renewed. The last article said the vantage rides with the reading, loopback, LAN, and cellular giving three answers to one question. This one adds a fourth: the resolver. Ask Google and you are a stranger. Ask the router and you are family. Ask the machine and you get whichever it opened first.

The other thing worth keeping is what the fatigue bought. A guess on the second compile, NAT reflection, would have been faster and would have been wrong, and would have left a global setting with a silent failure mode in the router for the next person to find. Each compile instead retired one hypothesis with a receipt: not the certificate, not the override, not the workstationโ€™s own config, not a cache, the lease. That is what a bisection is for, and it is why the models could change hands mid-ride without anything being lost: the evidence was never in the conversation, it was in the readings, and the readings were sealed. The New-B who followed this far now owns a working command, a router that tells the truth to its own house, and a habit: when a setting seems not to work, ask who has been told.

6. NOTARIZE

Fingerprint of the bytes this compile seals: 221,651 bytes UTF-8 by the Summaryโ€™s count, 219,566 characters, 58,216 verified tokens; six live command receipts; the newest timestamp inside a receipt is the lease expiry 1789389089, which is 2026-09-14 12:31:29 UTC. The seal footer that arrived beneath this payload names the archive, and the operatorโ€™s ls should agree with it.

Deed: foo-________.zip (ls -t foo-*.zip head -1)

Book Analysis

Ai Editorial Take

What stands out here is the profound asymmetry between server-side configuration and client-side adoption. In modern cloud environments, infrastructure-as-code often obscures the latency of state propagation. Here, in a bare-metal home lab, the lesson is laid bare: altering an authoritative source of truth (the routerโ€™s resolver) is completely inert until the contractual edge (the DHCP lease timer) is explicitly renegotiated by the client.

๐Ÿฆ X.com Promo Tweet

When home-hosted scripts fail on your own LAN, don't blame TLS. You knocked from inside the house, and the router answered. Here is how split DNS, DHCP leases, and curl connect-to turn perimeter errors into verifiable receipts:
https://mikelev.in/futureproof/wrong-front-door-split-dns-dhcp/
#pfSense #Networking #DevOps

Title Brainstorm

  • Title Option: The Wrong Front Door: Split DNS, DHCP Leases, and Verifiable Local Testing
    • Filename: wrong-front-door-split-dns-dhcp.md
    • Rationale: Captures both the root conceptual problem (knocking on the inside of your own front door) and the technical mechanics required to solve it cleanly.
  • Title Option: Leases, Not Broadcasts: Solving LAN Hairpinning for Local Installers
    • Filename: leases-not-broadcasts-lan-hairpinning.md
    • Rationale: Focuses on the key cognitive breakthrough of the debugging session: remembering that DHCP is pull-based and requires client renewal.
  • Title Option: The Two-Errors-One-Door Rule: Taming pfSense Split DNS for Web Workflows
    • Filename: two-errors-one-door-pfsense-split-dns.md
    • Rationale: Uses the vivid diagnostic phrase from the text while highlighting pfSense and split DNS configuration.
  • Title Option: Connecting Through the Perimeter: Curl Connect-To and Honest Local DNS
    • Filename: connecting-through-perimeter-curl-connect-to.md
    • Rationale: Highlights the specific CLI technique (--connect-to) that allows legitimate verification without disabling certificate checks.

Content Potential And Polish

  • Core Strengths:
    • Brilliant demonstration of the difference between --insecure (-k) and --connect-to to preserve TLS integrity during internal testing.
    • Clear articulation of the networking mental model: DNS as phone books and DHCP as expiring bilateral contracts rather than magic broadcasts.
    • Strict adherence to test-driven bisection: querying individual nameservers directly via minimal Python sockets to isolate server state from client caching.
  • Suggestions For Polish:
    • Include a brief explanatory diagram or ASCII flow chart contrasting external requests hitting the WAN NAT forward versus internal requests hitting the router web interface.
    • Explicitly document the exact NetworkManager command sequence for Linux desktop users who may not have nmcli configured identically.

Next Step Prompts

  • Draft a reusable standalone Python utility in scripts/dns_census.py based on the ad-hoc socket bisection probe used in this session to audit all local nameservers.
  • Create a diagnostic checklist for testing installer scripts across varied network topologies (LAN, cellular hotspot, external VPN).