Fleet changelogs · dev.ecs0.net
rdmsm4x-changelog-20260909-1705-replicantdb-build41-notarized-developer-id

rdmsm4x-changelog-20260909-1705-replicantdb-build41-notarized-developer-id

2026-09-09 17:05 EDT · rdmsm4x (macOS 27.0, 26A428, Apple Silicon) · claude@rdmsm4x (Opus 5, session_01L5FqUPjVWGsMbP4UhAqAa6)

ReplicantDB 1.19.5 (41) "Restraint" is the first notarized build of this product. Cut, notarized, stapled and deployed to ALL SIX fleet Macs. 6 deployed / 0 failed / 0 blocked / 0 rolled back.

Authority: DEC-20260909-03 (Rich, 2026-09-09) — every ECS app ships signed with Developer ID Application: east coast science, llc (ZU2882L4HT), hardened runtime, --timestamp, notarized via the ecs-notary notarytool keychain profile, stapled. TCC re-prompts on the hosts accepted by Rich.

Why this was needed even though Build 40 shipped three hours earlier

Build 40 was the first ReplicantDB with a hardened runtime. Necessary, and on its own not sufficient: it was signed Apple Development, which Apple's notary service does not accept at all. Gatekeeper's verdict comes from the notary ticket, not from the certificate, so build 40 measured rejected under spctl -a -t exec on every host. Build 41 measures accepted / source=Notarized Developer ID.

Marketing version stays 1.19.5; build 40 → 41. No rule in the repo bumps the marketing version for a signing-identity change, and Version.swift's contract is "CFBundleVersion — monotonic, never reused".

Files changed (all in /Users/richh/dev/apps/replicantDB, committed by explicit path)

File Change
Sources/ReplicantDBCore/Version.swift build 40 → 41
build.sh Developer ID constants; --timestamp on codesign; timestamp read-back assertion; new NOTARIZE step (ditto zips → notarytool submit → poll → log issues null → stapler staple/validate → spctl); manifest gains notarization fields
scripts/verify_build_contract.zsh same identity constants; asserts runtime, timestamp, Gatekeeper verdict and the stapled ticket; separate assertions for the bundle and for bare tools
dist-snapshots/1.19.5-build41/ SNAPSHOT.md, SHA256SUMS, build manifest, gate pre-registration, build + test logs
SESSION-STATE.md 2026-09-09 17:05 EDT checkpoint (per-host table, submission ids, rollbacks, blocked hosts)
ISSUES.md RESOLVED "current app artifact is development-signed"; correction appended to the build-40 hardened-runtime item

Commits 378499d, 0ba5c36, f9b4405, 3b9dae4, 8ae4ede, 2f0e1ac, 30065ad on main. Pushed to remotes backup and fleet. Tag v1.19.5-build41 on both, dereferencing to 30065ad.

Commands run (the load-bearing ones)

nice -n 10 ./build.sh                      # rc=0, 82 s end to end
scripts/verify_build_contract.zsh          # rc=0 standalone
nice -n 10 swift test                      # rc=0, Executed 1213 tests, 0 failures, 254.284 s
xcrun notarytool submit … --keychain-profile ecs-notary
xcrun notarytool info/log … --keychain-profile ecs-notary
xcrun stapler staple / validate dist/ReplicantDB.app
spctl -a -vv -t exec <each artifact>
ditto -c -k --sequesterRsrc --keepParent   # transport, preserves signature AND stapled ticket

Verification evidence

Gates C1–C9 were pre-registered at commit f9b4405, before the artifact existed, then measured. All PASS.

Artifact digests:

a3a7a333e15e56c31837943a0cee7a3852eb034b2104ae555ae4a9d6abf8df2d  ReplicantDB.app/Contents/MacOS/ReplicantDB
7a541b39eeb7e7906dcb247560850a87e49076e9766e73ef75ae3a40fb972175  replicantdb-cli
b64af744778afbadd8d922f5d22d7ecae364cc3ae88acd24c8d07a3b665a081e  replicantdb-mcp

Notarization: app 71c2c29d-ec88-4214-8117-f90748383e6d, tools bfe0bd12-e10f-4cb3-b4f0-de48204086af. Four superseded submissions from two aborted runs are listed in SNAPSHOT.md so they are never resubmitted.

Per-host result

Host Arch / OS CFBundleVersion Result
rdmpw3265m (canary) x86_64 / 26.7 40 → 41 OK — Intel, verified by native execution
rdmbair15m5 arm64 / 27.0 40 → 41 OK
rdmpw3275m x86_64 / 26.7 40 → 41 OK — Intel, verified by native execution
rdmsm4x (hub) arm64 / 27.0 40 → 41 OK
rdmbair13m5 arm64 / 27.0 40 → 41 OK — recovered at 17:13 after its macOS 27.0 update
jdmbair13m5 arm64 / 27.0 40 → 41 OK — recovered at 17:26 after its Tailscale node was restarted

6 deployed / 0 failed / 0 blocked / 0 rolled back. Daemon state left exactly as found on every host.

Independently re-verified at 17:27:09 EDT by a probe that does not read the install script's output. All six identical: CFBundleVersion 41, 1.19.5 (41) "Restraint" by execution, app digest a3a7a333e15e…, x86_64 arm64, codesign --verify --deep --strict rc=0, stapler validate rc=0, spctl -a -t exec rc=0, daemon not-loaded as found.

How the two blocked hosts were recovered rather than forced

Three checks that measured the wrong thing

  1. The handed-over readiness probe greps softwareupdated — a resident daemon present on all six hosts, including one up 1 day 4 hours. Read literally it blocks every host forever. The clause that varies is Installer Progress (0 everywhere). Its pgrep -f also self-matched through the ssh command line (22 false hits on the hub), and pgrep -x tailscaled reported STOPPED on five hosts reached over the tailnet.
  2. spctl -a -t exec never accepts a bare Mach-O, so the assertion on the two CLIs was constant-false — the mirror of build 40's runtime gate, one commit later. Replaced using three measured controls that all exit 3, so only the text discriminates.
  3. open -a returned rc=0 and a pid on all four hosts and actually launched build 41 on one. On three it re-activated a process still executing from the moved-aside rollback directory — including a build 38 binary running since Sep 8. Found with lsof on each pid's txt; fixed with a graceful quit and relaunch, re-measured. Nothing force-killed.

Backups and how to undo

Rollback directories were staged before each install by moving the incumbent (never copying-then-deleting), so running processes keep their inode:

/Applications/.replicantdb-rollbacks/20260909-165938-build41-from-40   # rdmpw3265m
/Applications/.replicantdb-rollbacks/20260909-170030-build41-from-40   # rdmbair15m5
/Applications/.replicantdb-rollbacks/20260909-170057-build41-from-40   # rdmpw3275m
/Applications/.replicantdb-rollbacks/20260909-170124-build41-from-40   # rdmsm4x
/Applications/.replicantdb-rollbacks/20260909-171352-build41-from-40   # rdmbair13m5
/Applications/.replicantdb-rollbacks/20260909-172629-build41-from-40   # jdmbair13m5

RB=<one of the above>
rm -rf /Applications/ReplicantDB.app && ditto "$RB/ReplicantDB.app" /Applications/ReplicantDB.app \
  && cp -p "$RB/replicantdb-cli" "$RB/replicantdb-mcp" ~/bin/

Every earlier rollback directory (09-05, 09-06, build 40) is retained. Nothing was deleted anywhere.

Outstanding owner actions

  1. None for the deployment — all six hosts are on build 41 and independently verified. Worth knowing: jdmbair13m5's Tailscale node has now stopped after a reboot twice in one day (14:16 and 16:41), each time needing a manual Tailscale up. That is a recurring pattern rather than a one-off, and is worth a launch-time fix on that host.
  2. replicantdb-cli and replicantdb-mcp are notarized but cannot be stapled (bundles, dmgs and pkgs only). They need network reachability to Apple for a first-launch ticket lookup where the app does not. Property of the format, not a defect.
  3. The hardened-runtime caveat still stands: if any ECS dependency ever becomes a dynamic framework, or a com.apple.security.cs.* entitlement is added, revisit before the next release cut — notarization now fails too, not just launch.

Not done, deliberately

No keychain command of any kind (login keychain remains first in the search list, fleet-signing second). No daemon enabled, disabled, bootstrapped or booted out. Nothing deleted. No process force-killed. No corpus, database or config under ~/Library/Application Support/replicantDB/ read or written. jdmbair13m5's route was repaired over .local (diagnosis and one Tailscale up), but it was never installed over .local — every deploy step used the pinned tailnet name. No secrets appear in this record: the signing identity is named by common name and SHA-1 fingerprint (both public), and the notarytool credential is referenced only by its keychain profile NAME (ecs-notary).