From d1f703ef508b58c7ed38865ffb68ba0806ee002b Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sat, 11 Apr 2026 19:55:29 -0700 Subject: [PATCH] native: wire Tangled device repo access --- fedac/native/SCORE.md | 19 +++ fedac/native/ac-os | 35 +++++ fedac/native/device-claude.md | 20 ++- fedac/native/initramfs/init | 6 + .../scripts/build-and-flash-initramfs.sh | 6 +- fedac/native/scripts/build-and-flash.sh | 6 +- fedac/native/src/ac-native.c | 40 +++-- fedac/native/src/pty.c | 19 ++- ...2026-04-11-notepat-ota-usb-flash-report.md | 138 ++++++++++++++++++ 9 files changed, 262 insertions(+), 27 deletions(-) create mode 100644 reports/2026-04-11-notepat-ota-usb-flash-report.md diff --git a/fedac/native/SCORE.md b/fedac/native/SCORE.md index 2c1e39ea27..f2176c75e9 100644 --- a/fedac/native/SCORE.md +++ b/fedac/native/SCORE.md @@ -23,6 +23,13 @@ ac-os upload # Upload current build as OTA release ac-os flash+upload # Build + flash + upload ``` +`ac-os` is the preferred path for real devices. It layers user-local credentials +and repo context into the initramfs on top of the base image: + +- Claude OAuth state, when present locally +- GitHub PAT, when `gh auth token` succeeds +- Tangled SSH identity, when `ssh -G knot.aesthetic.computer` resolves a local key + ### Manual steps ```bash @@ -50,6 +57,18 @@ curl -fsSL https://releases.aesthetic.computer/os/releases.json | jq '.latest' - **GPG private key**: `.tmp-key-jeffrey-private.asc` (in repo root, gitignored) - **Handle colors API**: `https://aesthetic.computer/.netlify/functions/handle-colors` (public, no auth) +## Device Repo Access + +- The on-device working tree lives at `/mnt/ac-repo`. +- On WiFi connect, AC Native clones or pulls from the public GitHub HTTPS mirror: + - `https://github.com/whistlegraph/aesthetic-computer.git` +- If a Tangled SSH key was baked at build time, the repo is also configured so: + - `origin` fetches from GitHub + - `origin` push mirrors to `git@knot.aesthetic.computer:aesthetic.computer/core` + - `origin` also pushes to the GitHub mirror + - `tangled` points directly at the knot remote +- If no Tangled key is baked, the repo stays GitHub-only for pushes. + ## Update Signal Expectations - Native notepat checks `native-notepat-latest.version` on WiFi connect and periodic background checks. diff --git a/fedac/native/ac-os b/fedac/native/ac-os index 0b227f3b4f..705915b48d 100755 --- a/fedac/native/ac-os +++ b/fedac/native/ac-os @@ -152,6 +152,41 @@ build_initramfs() { sudo chmod 600 "${INITRAMFS_ROOT}/github-pat" log " github pat: baked (${#GH_PAT} chars)" fi + # Bake Tangled SSH identity if available so on-device git pushes can + # reach knot.aesthetic.computer as well as the GitHub mirror. + local TANGLED_KEY + TANGLED_KEY="$(ssh -G knot.aesthetic.computer 2>/dev/null | awk '/^identityfile / {print $2; exit}')" + case "${TANGLED_KEY}" in + "~/"*) TANGLED_KEY="${HOME}/${TANGLED_KEY#~/}" ;; + esac + if [ -z "${TANGLED_KEY}" ] && [ -f "${HOME}/.ssh/tangled" ]; then + TANGLED_KEY="${HOME}/.ssh/tangled" + fi + if [ -n "${TANGLED_KEY}" ] && [ -f "${TANGLED_KEY}" ]; then + sudo cp "${TANGLED_KEY}" "${INITRAMFS_ROOT}/tangled-key" + sudo chmod 600 "${INITRAMFS_ROOT}/tangled-key" + if [ -f "${HOME}/.ssh/known_hosts" ]; then + sudo cp "${HOME}/.ssh/known_hosts" "${INITRAMFS_ROOT}/tangled-known-hosts" + sudo chmod 600 "${INITRAMFS_ROOT}/tangled-known-hosts" + elif command -v ssh-keyscan >/dev/null 2>&1; then + ssh-keyscan -H knot.aesthetic.computer 2>/dev/null | sudo tee "${INITRAMFS_ROOT}/tangled-known-hosts" >/dev/null || true + sudo chmod 600 "${INITRAMFS_ROOT}/tangled-known-hosts" 2>/dev/null || true + fi + sudo tee "${INITRAMFS_ROOT}/tangled-ssh-config" > /dev/null << 'SSHCFG' +Host knot.aesthetic.computer + HostName knot.aesthetic.computer + User git + IdentityFile ~/.ssh/tangled + IdentitiesOnly yes + BatchMode yes + StrictHostKeyChecking accept-new + UserKnownHostsFile ~/.ssh/known_hosts +SSHCFG + sudo chmod 600 "${INITRAMFS_ROOT}/tangled-ssh-config" + log " tangled ssh: baked (${TANGLED_KEY})" + else + log " tangled ssh: not found (skipping knot push setup)" + fi # Bake CLAUDE.md for device context (init copies to /tmp/ac/CLAUDE.md) if [ -f "${SCRIPT_DIR}/device-claude.md" ]; then sudo cp "${SCRIPT_DIR}/device-claude.md" "${INITRAMFS_ROOT}/device-claude.md" diff --git a/fedac/native/device-claude.md b/fedac/native/device-claude.md index 6c8c31c61c..42197c5cfc 100644 --- a/fedac/native/device-claude.md +++ b/fedac/native/device-claude.md @@ -27,11 +27,13 @@ If trace files exist in `/mnt/trace/`, summarize active tracers. ### File Operations - Read/write files in /tmp (lost on reboot) - Read/write files in /mnt (persistent, VFAT — no symlinks, no permissions) -- The main AC repo is NOT on this device — use git clone if needed +- `/mnt/ac-repo` is the preferred working tree when WiFi has already done the background clone +- If `/mnt/ac-repo` does not exist yet, clone manually or work in `/tmp/ac` as a scratch fallback ### Network - curl is available for HTTP requests -- git is available (GitHub PAT is pre-configured via GH_TOKEN) +- git is available +- `ssh` is available for Tangled pushes when the image was built via `ac-os` with a local Tangled key - DNS works (8.8.8.8 / 1.1.1.1 fallback) ### Development @@ -52,13 +54,16 @@ If trace files exist in `/mnt/trace/`, summarize active tracers. | Path | Purpose | |------|---------| | /mnt/config.json | User identity + auth tokens | +| /mnt/ac-repo | Persistent shallow clone of `aesthetic-computer` | | /mnt/ac-native.log | System log (persistent) | | /claude-token | Claude OAuth token (initramfs) | | /github-pat | GitHub PAT (initramfs) | | /tmp/.claude/ | Claude Code config directory | +| /tmp/.ssh/ | Tangled SSH key/config restored on boot, when baked | | /ac-native | Main system binary | | /piece.mjs | Default JS piece | | /bin/claude | Claude Code binary | +| /bin/ssh | OpenSSH client for Tangled git push | ## Architecture @@ -127,10 +132,13 @@ curl -fsSL https://aesthetic.computer ### Git operations ```sh -cd /tmp -git clone https://github.com/whistlegraph/aesthetic-computer.git -cd aesthetic-computer -# GH_TOKEN is pre-set — push works +cd /mnt/ac-repo 2>/dev/null || { + git clone https://github.com/whistlegraph/aesthetic-computer.git /mnt/ac-repo + cd /mnt/ac-repo +} +git remote -v +# fetch uses the GitHub mirror +# if /tmp/.ssh/tangled exists, origin push mirrors to knot + GitHub ``` ### OTA update diff --git a/fedac/native/initramfs/init b/fedac/native/initramfs/init index 074b44d5a8..65ca260bb9 100755 --- a/fedac/native/initramfs/init +++ b/fedac/native/initramfs/init @@ -31,6 +31,12 @@ if [ -f /claude-creds.json ]; then cp /claude-state.json /tmp/.claude.json 2>/dev/null printf '{"permissions":{"allow":["Bash(*)","Read(*)","Write(*)","Edit(*)","Glob(*)","Grep(*)","WebFetch(*)","WebSearch(*)"]},"autoUpdates":false,"installMethod":"native"}\n' > /tmp/.claude/settings.json fi +if [ -f /tangled-key ] || [ -f /tangled-ssh-config ] || [ -f /tangled-known-hosts ]; then + mkdir -p /tmp/.ssh + [ -f /tangled-key ] && cp /tangled-key /tmp/.ssh/tangled 2>/dev/null && chmod 600 /tmp/.ssh/tangled 2>/dev/null + [ -f /tangled-ssh-config ] && cp /tangled-ssh-config /tmp/.ssh/config 2>/dev/null && chmod 600 /tmp/.ssh/config 2>/dev/null + [ -f /tangled-known-hosts ] && cp /tangled-known-hosts /tmp/.ssh/known_hosts 2>/dev/null && chmod 600 /tmp/.ssh/known_hosts 2>/dev/null +fi mkdir -p /tmp/ac [ -f /device-claude.md ] && cp /device-claude.md /tmp/ac/CLAUDE.md 2>/dev/null [ -f /device-score.md ] && cp /device-score.md /tmp/ac/SCORE.md 2>/dev/null diff --git a/fedac/native/scripts/build-and-flash-initramfs.sh b/fedac/native/scripts/build-and-flash-initramfs.sh index 9eee1f528a..c959e2bab4 100755 --- a/fedac/native/scripts/build-and-flash-initramfs.sh +++ b/fedac/native/scripts/build-and-flash-initramfs.sh @@ -636,12 +636,12 @@ else warn "dropbear not found — SSH remote access not available" fi -# ── Claude Code utilities (git, ripgrep, jq) ── +# ── Claude Code utilities (git, ripgrep, jq, ssh) ── # These tools make Claude Code significantly more capable on the device. -for util in git rg jq; do +for util in git rg jq ssh; do UTIL_PATH="$(command -v "$util" 2>/dev/null || true)" if [ -n "$UTIL_PATH" ] && [ -f "$UTIL_PATH" ]; then - cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/" + cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/${util}" for lib in $(ldd "$UTIL_PATH" 2>/dev/null | grep -oP '/\S+'); do [ -f "$lib" ] && cp -n "$lib" "${INITRAMFS_DIR}/lib64/" 2>/dev/null || true done diff --git a/fedac/native/scripts/build-and-flash.sh b/fedac/native/scripts/build-and-flash.sh index 483ecef7ef..7bb73482d4 100755 --- a/fedac/native/scripts/build-and-flash.sh +++ b/fedac/native/scripts/build-and-flash.sh @@ -537,12 +537,12 @@ else warn "dropbear not found — SSH remote access not available" fi -# ── Claude Code utilities (git, ripgrep, jq) ── +# ── Claude Code utilities (git, ripgrep, jq, ssh) ── # These tools make Claude Code significantly more capable on the device. -for util in git rg jq; do +for util in git rg jq ssh; do UTIL_PATH="$(command -v "$util" 2>/dev/null || true)" if [ -n "$UTIL_PATH" ] && [ -f "$UTIL_PATH" ]; then - cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/" + cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/${util}" for lib in $(ldd "$UTIL_PATH" 2>/dev/null | grep -oP '/\S+'); do [ -f "$lib" ] && cp -nL "$lib" "${INITRAMFS_DIR}/lib64/" 2>/dev/null || true done diff --git a/fedac/native/src/ac-native.c b/fedac/native/src/ac-native.c index fd584b9e6d..cd6b2b2ddd 100644 --- a/fedac/native/src/ac-native.c +++ b/fedac/native/src/ac-native.c @@ -3960,9 +3960,11 @@ int main(int argc, char *argv[]) { } // Clone (or pull) the aesthetic-computer repo to // /mnt/ac-repo so Claude Code has a real project to - // work in. Runs in the background so boot isn't - // delayed. If /github-pat is present, git picks it up - // via the git-credential-ac helper. We use a shallow + // work in. Fetch stays on the GitHub HTTPS mirror for + // simple read access; if a Tangled SSH key was baked, + // the repo also gets origin pushurls for knot + GitHub + // and a dedicated `tangled` remote. Runs in the + // background so boot isn't delayed. We use a shallow // clone (--depth=50) to keep the size manageable // (~200 MB) while still giving Claude enough history // for basic blame/log work. @@ -3971,16 +3973,36 @@ int main(int argc, char *argv[]) { // `git pull` runs instead (bounded by 30s timeout). // Logs go to /tmp/ac-repo-clone.log. { - char clone_cmd[1024]; + char clone_cmd[3072]; snprintf(clone_cmd, sizeof(clone_cmd), - "( if [ -d /mnt/ac-repo/.git ]; then " + "( REPO=/mnt/ac-repo; " + " FETCH_URL='https://github.com/whistlegraph/aesthetic-computer.git'; " + " TANGLED_URL='git@knot.aesthetic.computer:aesthetic.computer/core'; " + " if [ -d \"$REPO/.git\" ]; then " " echo '[ac-repo] pulling latest' && " - " cd /mnt/ac-repo && timeout 30 git pull --ff-only 2>&1; " + " cd \"$REPO\" && timeout 30 git pull --ff-only 2>&1; " " else " " echo '[ac-repo] cloning (shallow)' && " - " timeout 300 git clone --depth=50 --branch=main " - " https://github.com/whistlegraph/aesthetic-computer.git " - " /mnt/ac-repo 2>&1; " + " timeout 300 git clone --depth=50 --branch=main \"$FETCH_URL\" \"$REPO\" 2>&1; " + " fi; " + " if [ -d \"$REPO/.git\" ]; then " + " cd \"$REPO\"; " + " git remote set-url origin \"$FETCH_URL\"; " + " git config --unset-all remote.origin.pushurl 2>/dev/null || true; " + " if [ -f /tmp/.ssh/tangled ]; then " + " git config --add remote.origin.pushurl \"$TANGLED_URL\"; " + " git config --add remote.origin.pushurl \"$FETCH_URL\"; " + " if git remote | grep -qx tangled; then " + " git remote set-url tangled \"$TANGLED_URL\"; " + " else " + " git remote add tangled \"$TANGLED_URL\"; " + " fi; " + " echo '[ac-repo] origin pushurl: tangled + github'; " + " else " + " git config --add remote.origin.pushurl \"$FETCH_URL\"; " + " git remote remove tangled 2>/dev/null || true; " + " echo '[ac-repo] origin pushurl: github only (no tangled key)'; " + " fi; " " fi " ") >> /tmp/ac-repo-clone.log 2>&1 &"); system(clone_cmd); diff --git a/fedac/native/src/pty.c b/fedac/native/src/pty.c index dd71cd0297..fd7bca3acb 100644 --- a/fedac/native/src/pty.c +++ b/fedac/native/src/pty.c @@ -561,12 +561,17 @@ int pty_spawn(ACPty *pty, int cols, int rows, const char *cmd, char *const argv[ setenv("SHELL", "/bin/bash", 1); setenv("USER", "root", 1); setenv("LOGNAME", "root", 1); + setenv("GIT_TERMINAL_PROMPT", "0", 1); setenv("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1", 1); // SSL certs for API connections setenv("SSL_CERT_FILE", "/etc/pki/tls/certs/ca-bundle.crt", 0); setenv("SSL_CERT_DIR", "/etc/ssl/certs", 0); setenv("CURL_CA_BUNDLE", "/etc/pki/tls/certs/ca-bundle.crt", 0); setenv("NODE_EXTRA_CA_CERTS", "/etc/pki/tls/certs/ca-bundle.crt", 0); + if (access("/bin/ssh", X_OK) == 0 && access("/tmp/.ssh/config", R_OK) == 0) { + setenv("GIT_SSH_COMMAND", "/bin/ssh -F /tmp/.ssh/config -o BatchMode=yes", 1); + setenv("GIT_SSH_VARIANT", "ssh", 1); + } // GitHub PAT (for git operations) { FILE *gf = fopen("/github-pat", "r"); @@ -625,8 +630,8 @@ int pty_spawn(ACPty *pty, int cols, int rows, const char *cmd, char *const argv[ // Working directory strategy: // 1. /mnt/ac-repo (persistent shallow clone of aesthetic-computer, - // cloned on first boot or manually by the user) — PREFERRED so - // Claude can actually `git commit` real project files + // auto-cloned on WiFi connect or manually by the user) — PREFERRED + // so Claude can actually `git commit` real project files // 2. /tmp/ac (tmpfs, persists per-session only) — FALLBACK for // when there's no repo yet. Claude can still edit scratch // files here. A placeholder git init makes it look like a @@ -665,19 +670,21 @@ int pty_spawn(ACPty *pty, int cols, int rows, const char *cmd, char *const argv[ "- **No package manager** — binaries are baked into initramfs\n\n" "## Filesystem\n" "- `/mnt` — USB boot drive (FAT32, has config.json and logs)\n" - "- `/mnt/ac-repo` — full aesthetic-computer git repo (persistent)\n" + "- `/mnt/ac-repo` — persistent shallow repo clone (GitHub fetch mirror)\n" "- `/mnt/tapes` — MP4 tapes recorded via PrintScreen\n" "- `/tmp` — tmpfs (lost on reboot)\n" - "- `/bin` — busybox + baked binaries (bash, git, rg, jq)\n\n" + "- `/tmp/.ssh` — Tangled SSH key/config, when baked via `ac-os`\n" + "- `/bin` — busybox + baked binaries (bash, git, rg, jq, ssh)\n\n" "## Networking\n" "- WiFi auto-connects to saved networks\n" "- `curl` is available for HTTP requests\n" - "- GitHub PAT is pre-configured for git push/pull\n\n" + "- `/mnt/ac-repo` fetches from the GitHub mirror\n" + "- If `/tmp/.ssh/tangled` exists, `git push origin` mirrors to knot + GitHub\n\n" "## What you can do\n" "- Write and run shell scripts (bash, not busybox)\n" "- Use curl to interact with APIs\n" "- Edit files in /mnt/ac-repo and `git commit` them\n" - "- Use git (GitHub PAT is wired up for whistlegraph)\n" + "- Use git (GitHub PAT is wired up; Tangled push works when the SSH key is baked)\n" "- Read system logs at /mnt/ac-native.log\n", handle, handle); fclose(cm); diff --git a/reports/2026-04-11-notepat-ota-usb-flash-report.md b/reports/2026-04-11-notepat-ota-usb-flash-report.md new file mode 100644 index 0000000000..915a60397f --- /dev/null +++ b/reports/2026-04-11-notepat-ota-usb-flash-report.md @@ -0,0 +1,138 @@ +# 2026-04-11 — notepat OTA build and USB flash report + +Session focus: ship the async DJ USB mount check and Notepat live-percussion +work as a fresh OTA build, then get that exact OTA image onto the USB with +Jeffrey's inscribed credentials and preserved Wi-Fi seeds. + +Final release published in oven: + +- Release: `dynamic-milksnake` +- Commit: `848d3231a` +- Build stamp: `848d3231a-2026-04-11T21:30` +- Oven job: `fff0bd10-0` + +--- + +## 1. What shipped + +This build includes the native change that makes the DJ music-USB probe +non-blocking: + +- `system.mountMusic()` now schedules its probe off-thread instead of blocking + the main JS/render path +- JS callers now consume cached mount state via + `system.mountMusicMounted` / `system.mountMusicPending` +- This closes the periodic crawl caused by synchronous USB polling during play + +Work was committed and pushed as: + +- Commit: `848d3231a5c5126cf2df1f731fd0d293d7a62350` +- Message: `native: make dj usb mount check async` + +--- + +## 2. OTA build result + +Manual oven trigger was required because the native poller was not picking the +build up automatically at the time of the session. + +Published artifacts included: + +- `os/releases.json` +- `os/native-notepat-latest.vmlinuz` +- `os/native-notepat-latest.vmlinuz-slim` +- `os/native-notepat-latest.initramfs.cpio.gz` + +OTA publication succeeded and `ac-os pull` was able to download and verify the +new release payload. + +--- + +## 3. Standard flash-path failure + +The normal `./fedac/native/ac-os pull` flash path did not complete on this host. + +Observed failure: + +- partitioning completed, but kernel partition-table reread stayed busy +- `mkfs.vfat` on `/dev/sda1` retried repeatedly and failed with + `Device or resource busy` +- direct host-side reread attempts (`blockdev --rereadpt`, `partx -u`) were + also blocked + +This meant the OTA payload was downloaded correctly, but the standard helper +could not safely move from partitioning into filesystem creation on the live +USB device. + +--- + +## 4. Raw-image fallback + +To bypass the host's live partition-reread problem, the USB was flashed through +a raw-disk fallback: + +1. Download OTA artifacts with `ac-os pull` +2. Stage `config.json`, `wifi_creds.json`, kernel, initramfs, and EFI payloads +3. Build full `ACBOOT`, `ACEFI`, and `AC-MAC` partition images offline +4. Assemble a complete GPT raw USB image +5. Write that raw image directly to `/dev/sda` +6. Read files back from the physical device by partition offset to verify the + result + +The bulk raw write completed successfully: + +- bytes written: `15518924800` +- elapsed: `742.457 s` +- sustained rate at completion: `20.9 MB/s` + +--- + +## 5. Device readback verification + +Readback from the physical USB confirmed: + +- `config.json` present on the device +- `wifi_creds.json` present on the device +- handle: `jeffrey` +- AC token: present +- Claude token: present +- GitHub PAT: present +- `claudeCreds`: present +- `claudeState`: present +- Wi-Fi networks preserved: `4` + +Readback SSIDs: + +- `aesthetic.computer` +- `ATT2AWTpcr` +- `GettyLink` +- `Tondo_Guest` + +Partition content checks: + +- `ACBOOT` contained `EFI/BOOT/BOOTX64.EFI` and `BOOTIA32.EFI` +- `ACEFI` contained `EFI/BOOT/BOOTX64.EFI`, `LOADER.EFI`, and `KERNEL.EFI` + +--- + +## 6. Caveat + +Immediately after the raw write, host-side `lsblk` still showed `/dev/sda1-3` +without refreshed filesystem metadata. That appears to be the same host/kernel +partition-refresh limitation that broke the standard flash path. + +Important distinction: + +- host metadata refresh was stale +- direct readback from the physical USB contents succeeded + +So the USB image itself verified cleanly even though the host did not fully +refresh its partition view until replug. + +--- + +## 7. Outcome + +The `dynamic-milksnake` OTA build for commit `848d3231a` was successfully +published and successfully written to the USB, with credentials and Wi-Fi data +inscribed and verified from the device itself. -- 2.54.0