Settings ▸ Remote gains a "Connect via" picker — LAN (default), Tailscale
(tailnet), or Relay (disabled, coming soon). On Tailnet, both devices run an
embedded tsnet node via TailscaleKit (tailscale/libtailscale) and sync frames
flow over the user's tailnet, so the phone can connect from anywhere the
tailnet reaches; Noise E2EE runs above the transport unchanged.
- NucleicTailnet (new target, macOS + iOS): TailnetNode wraps TailscaleKit's
node lifecycle (auth-key login, generation-fenced start/stop since up() is
un-cancellable) and drops to the framework's public C API for the data path
— tailscale_dial/listen/accept hand back full-duplex socketpair fds, wrapped
by FDFrameChannel (DispatchIO) into the shared FrameChannel seam. The Swift
wrapper's one-way connection actors can't carry a bidirectional stream.
- Host: TailnetListener adopts SyncListener; startSyncServer is single-flight
and honors toggle-off/picker changes at the commit point; pairing QRs carry
transport + tailnet IP/port hints (PairingPayload additive optional fields,
forward/backward compatible over CBOR).
- iPhone: pair/reconnect dial over whichever transport the pairing recorded;
Settings gains a Tailscale auth-key field (Keychain, committed on editing
end); connectivity chip shows "Connected · Tailnet".
- TailscaleKit has no SwiftPM distribution: scripts/build-tailscalekit.sh
builds a pinned libtailscale commit into an untracked local xcframework;
Package.swift links it only when present (everything builds without it, the
picker then reports Tailscale support as not built in), and the script
clears SwiftPM's content-keyed manifest cache so the toggle is picked up.
- iOS floor 17.0 → 18.1 (TailscaleKit requires the iOS 18 Swift runtime);
package-app.sh embeds the framework in the .app like Sparkle.
703-test suite: no new failures (the 7 fake-claude/fake-grok staging issues
reproduce identically on an untouched checkout — pre-existing, tracked
separately). New coverage: FDFrameChannel over socketpairs, pairing-payload
version-skew both directions, transport-setting resolution.
Co-Authored-By: Claude Fable 5 <[email protected]>
Squash-merge nucleic/trunk into dev (234 commits of accumulated nvrsion work).
Resolved two auto-generated-file conflicts:
- cloud/nucleic-edge/wrangler.jsonc: kept dev's real KV namespace ids and the
xyz.blakeslee.nucleic.remote APNS topic, with binding names corrected to
RELAY_TOKENS/PUSH_TOKENS to match the worker code on both branches.
- Package.resolved: took trunk's newer dependency pins.
Nucleic-Promote: 1
Co-Authored-By: Claude Opus 4.8 <[email protected]>
./VERSION (NUCLEIC_MARKETING_VERSION + NUCLEIC_BUILD_NUMBER) is the single source of truth, read by package-app.sh and bumped by release-macos.sh. Every release increments the build number by 1; 'make release-beta BUMP=major|minor|patch' also bumps that semver component (resetting lower ones). The bumped VERSION is committed on success so build numbers stay monotonic. Seeded at 827 (the prior git-derived count) so numbering is continuous.
scripts/bump-version.sh does the arithmetic and rewrites VERSION; package-app.sh now reads version+build from VERSION (env NUCLEIC_VERSION/NUCLEIC_BUILD still override; missing file falls back to 0.1.0 + git count, so app-* and fresh checkouts are unaffected).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Hardened runtime forces library validation (CS_REQUIRE_LV): every loaded Mach-O must share the main executable's Team ID. An ad-hoc binary has no team identity, and the app + embedded Sparkle.framework are signed in separate ad-hoc passes, so the loader rejects Sparkle at launch with "different Team IDs" (EXC_CRASH/DYLD) — even though `codesign --verify` (a static check) passes. This was the recurring nucleic-beta launch crash.
Apply --options runtime only for a real Developer ID identity (required by notarization; there the re-signed Sparkle shares the team, so library validation passes). Ad-hoc local builds omit it, dropping the library-validation requirement so the embedded Sparkle loads. Verified end-to-end: a fresh `make app-beta` now launches past dyld.
RT_FLAG is empty for ad-hoc; guard its expansion (${RT_FLAG[@]+...}) because macOS's bash 3.2 errors on "${empty[@]}" under set -u.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Desktop channels → xyz.blakeslee.nucleic.desktop.{dev,beta,rc,release} (the stable channel keyword is unchanged; only its bundle-id suffix is "release"). iOS remote → xyz.blakeslee.nucleic.remote, including its Keychain account namespaces, the scanner log subsystem, and the coupled APNS topic.
Correct the Apple Developer Team ID to L7UDTQ6F5W across the Xcode project, ExportOptions, the APNS config + tests, and the signing/cloud docs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The kernel was the last bundled asset (needing scripts/fetch-kernel.sh +
codesign-time bundling). Now it's acquired automatically on first use like the
vminitd initfs and the sandbox image — nothing is bundled and the user runs no
setup step.
- ContainerEngine.ensureKernel: resolve order = NUCLEIC_KERNEL_PATH → on-disk
cache → bundled Resources (optional) → reuse Apple `container`'s installed
kernel (no download) → download the kernel release asset. The asset is hosted
on a GitHub release (ProjectSandbox.kernelRelease*) and fetched with the app's
GitHub token when the repo is private (same token as the sandbox image).
NUCLEIC_KERNEL_NO_REUSE forces the download (testing / incompatible local kernel).
- isSupported/unsupportedReason now gate only on Apple silicon; the kernel is no
longer a precondition. Settings copy + package-app.sh + BUILD.md updated;
bundling is an optional offline fast-path.
- registryAuth: scope credentials to the sandbox image's registry host, so a
configured token is never attached to a different registry (e.g. docker.io
public bases, which 401 on unrelated Basic creds). Fixes anonymous base pulls.
- Verified end-to-end on macOS 27 / Apple silicon: forced kernel download +
anonymous docker.io base pull + boot + exec + stdout. Builds clean; 21 tests pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replace the Apple `container` CLI wrapper with Apple's `containerization`
Swift framework, driven in-process — no external CLI or daemon.
- ContainerEngine: in-process runtime (shared VZVirtualMachineManager from a
bundled kernel + runtime-pulled vminitd initfs, ImageStore, VmnetNetwork,
live-container registry, typed statistics for CPU/mem + OOM diagnosis).
Daemonless ⇒ ephemeral VMs; reconcile is on-disk GC.
- ContainerizedProcessHandle: bridges a guest LinuxProcess onto the existing
ProcessHandle contract (reusing LineSplitter), so backends stream NDJSON
identically in-container and on-host. Closes the stdio writers after wait()
to finish the line streams (the framework never calls Writer.close()).
- Sandbox image is built in CI (containers/nucleic-sandbox/Dockerfile +
.github/workflows/sandbox-image.yml) and pushed to GHCR; the app pulls +
unpacks it on first use (no on-device build, no user-installed tools). The
GHCR package may stay private — pulls authenticate with the user's GitHub
token via ContainerEngine.registryAuth (Settings → Sandbox, or
NUCLEIC_REGISTRY_USER/NUCLEIC_REGISTRY_TOKEN). vminitd is pulled from Apple's
public GHCR; only the kernel is bundled (scripts/fetch-kernel.sh, curl-only).
- ContainerManager rewired to the engine (policy preserved); ClaudeCodeBackend
execs in-container via the engine; Settings/ProviderAvailability use a static
capability check. Platform floor raised to macOS 26 (Apple silicon) + the
com.apple.security.virtualization entitlement (swift-tools 6.2).
- Verified end-to-end on macOS 27 / Apple silicon via Sources/container-spike:
pull vminitd + image, boot VM, exec, stream stdout. Builds clean; 21 tests pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Terminology: "TestFlight" -> "Beta" across the banner, build config, Makefile, and
docs. The beta channel's process/product is renamed nucleic-tf -> nucleic-beta so
everything (process, app name, bundle id, label) reads "beta".
New rc channel: NUCLEIC_CHANNEL=rc -> process nucleic-rc, define NUCLEIC_RC, gold
"Release Candidate" banner, Nucleic RC.app (com.abkslm.nucleic.rc). Sits between
staging and main in the gated flow: feature -> dev -> staging -> rc -> main.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add scripts/package-app.sh + make app-dev/app-beta/app-stable/apps targets that
wrap each channel's SwiftPM executable in a coexisting, ad-hoc-signed .app:
dev -> dist/Nucleic Dev.app (com.abkslm.nucleic.dev)
beta -> dist/Nucleic Beta.app (com.abkslm.nucleic.beta)
stable -> dist/Nucleic.app (com.abkslm.nucleic)
The bundle keeps the channel product name as Contents/MacOS/<name> (preserving the
process name), ships GRDB/SwiftTerm resource bundles in Contents/Resources, writes
Info.plist (version = NUCLEIC_VERSION, build = commit count), and supports an
optional Resources/AppIcon[-<channel>].icns. Distribution signing/notarization/
TestFlight upload is left as configurable hooks (NUCLEIC_SIGN_ID). dist/ gitignored;
BUILD.md documents it.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>