Monorepo for Aesthetic.Computer aesthetic.computer

native: wire Tangled device repo access

+262 -27
+19
fedac/native/SCORE.md
··· 23 23 ac-os flash+upload # Build + flash + upload 24 24 ``` 25 25 26 + `ac-os` is the preferred path for real devices. It layers user-local credentials 27 + and repo context into the initramfs on top of the base image: 28 + 29 + - Claude OAuth state, when present locally 30 + - GitHub PAT, when `gh auth token` succeeds 31 + - Tangled SSH identity, when `ssh -G knot.aesthetic.computer` resolves a local key 32 + 26 33 ### Manual steps 27 34 28 35 ```bash ··· 49 56 - Contains: `DO_SPACES_KEY`, `DO_SPACES_SECRET` 50 57 - **GPG private key**: `.tmp-key-jeffrey-private.asc` (in repo root, gitignored) 51 58 - **Handle colors API**: `https://aesthetic.computer/.netlify/functions/handle-colors` (public, no auth) 59 + 60 + ## Device Repo Access 61 + 62 + - The on-device working tree lives at `/mnt/ac-repo`. 63 + - On WiFi connect, AC Native clones or pulls from the public GitHub HTTPS mirror: 64 + - `https://github.com/whistlegraph/aesthetic-computer.git` 65 + - If a Tangled SSH key was baked at build time, the repo is also configured so: 66 + - `origin` fetches from GitHub 67 + - `origin` push mirrors to `git@knot.aesthetic.computer:aesthetic.computer/core` 68 + - `origin` also pushes to the GitHub mirror 69 + - `tangled` points directly at the knot remote 70 + - If no Tangled key is baked, the repo stays GitHub-only for pushes. 52 71 53 72 ## Update Signal Expectations 54 73
+35
fedac/native/ac-os
··· 152 152 sudo chmod 600 "${INITRAMFS_ROOT}/github-pat" 153 153 log " github pat: baked (${#GH_PAT} chars)" 154 154 fi 155 + # Bake Tangled SSH identity if available so on-device git pushes can 156 + # reach knot.aesthetic.computer as well as the GitHub mirror. 157 + local TANGLED_KEY 158 + TANGLED_KEY="$(ssh -G knot.aesthetic.computer 2>/dev/null | awk '/^identityfile / {print $2; exit}')" 159 + case "${TANGLED_KEY}" in 160 + "~/"*) TANGLED_KEY="${HOME}/${TANGLED_KEY#~/}" ;; 161 + esac 162 + if [ -z "${TANGLED_KEY}" ] && [ -f "${HOME}/.ssh/tangled" ]; then 163 + TANGLED_KEY="${HOME}/.ssh/tangled" 164 + fi 165 + if [ -n "${TANGLED_KEY}" ] && [ -f "${TANGLED_KEY}" ]; then 166 + sudo cp "${TANGLED_KEY}" "${INITRAMFS_ROOT}/tangled-key" 167 + sudo chmod 600 "${INITRAMFS_ROOT}/tangled-key" 168 + if [ -f "${HOME}/.ssh/known_hosts" ]; then 169 + sudo cp "${HOME}/.ssh/known_hosts" "${INITRAMFS_ROOT}/tangled-known-hosts" 170 + sudo chmod 600 "${INITRAMFS_ROOT}/tangled-known-hosts" 171 + elif command -v ssh-keyscan >/dev/null 2>&1; then 172 + ssh-keyscan -H knot.aesthetic.computer 2>/dev/null | sudo tee "${INITRAMFS_ROOT}/tangled-known-hosts" >/dev/null || true 173 + sudo chmod 600 "${INITRAMFS_ROOT}/tangled-known-hosts" 2>/dev/null || true 174 + fi 175 + sudo tee "${INITRAMFS_ROOT}/tangled-ssh-config" > /dev/null << 'SSHCFG' 176 + Host knot.aesthetic.computer 177 + HostName knot.aesthetic.computer 178 + User git 179 + IdentityFile ~/.ssh/tangled 180 + IdentitiesOnly yes 181 + BatchMode yes 182 + StrictHostKeyChecking accept-new 183 + UserKnownHostsFile ~/.ssh/known_hosts 184 + SSHCFG 185 + sudo chmod 600 "${INITRAMFS_ROOT}/tangled-ssh-config" 186 + log " tangled ssh: baked (${TANGLED_KEY})" 187 + else 188 + log " tangled ssh: not found (skipping knot push setup)" 189 + fi 155 190 # Bake CLAUDE.md for device context (init copies to /tmp/ac/CLAUDE.md) 156 191 if [ -f "${SCRIPT_DIR}/device-claude.md" ]; then 157 192 sudo cp "${SCRIPT_DIR}/device-claude.md" "${INITRAMFS_ROOT}/device-claude.md"
+14 -6
fedac/native/device-claude.md
··· 27 27 ### File Operations 28 28 - Read/write files in /tmp (lost on reboot) 29 29 - Read/write files in /mnt (persistent, VFAT — no symlinks, no permissions) 30 - - The main AC repo is NOT on this device — use git clone if needed 30 + - `/mnt/ac-repo` is the preferred working tree when WiFi has already done the background clone 31 + - If `/mnt/ac-repo` does not exist yet, clone manually or work in `/tmp/ac` as a scratch fallback 31 32 32 33 ### Network 33 34 - curl is available for HTTP requests 34 - - git is available (GitHub PAT is pre-configured via GH_TOKEN) 35 + - git is available 36 + - `ssh` is available for Tangled pushes when the image was built via `ac-os` with a local Tangled key 35 37 - DNS works (8.8.8.8 / 1.1.1.1 fallback) 36 38 37 39 ### Development ··· 52 54 | Path | Purpose | 53 55 |------|---------| 54 56 | /mnt/config.json | User identity + auth tokens | 57 + | /mnt/ac-repo | Persistent shallow clone of `aesthetic-computer` | 55 58 | /mnt/ac-native.log | System log (persistent) | 56 59 | /claude-token | Claude OAuth token (initramfs) | 57 60 | /github-pat | GitHub PAT (initramfs) | 58 61 | /tmp/.claude/ | Claude Code config directory | 62 + | /tmp/.ssh/ | Tangled SSH key/config restored on boot, when baked | 59 63 | /ac-native | Main system binary | 60 64 | /piece.mjs | Default JS piece | 61 65 | /bin/claude | Claude Code binary | 66 + | /bin/ssh | OpenSSH client for Tangled git push | 62 67 63 68 ## Architecture 64 69 ··· 127 132 128 133 ### Git operations 129 134 ```sh 130 - cd /tmp 131 - git clone https://github.com/whistlegraph/aesthetic-computer.git 132 - cd aesthetic-computer 133 - # GH_TOKEN is pre-set — push works 135 + cd /mnt/ac-repo 2>/dev/null || { 136 + git clone https://github.com/whistlegraph/aesthetic-computer.git /mnt/ac-repo 137 + cd /mnt/ac-repo 138 + } 139 + git remote -v 140 + # fetch uses the GitHub mirror 141 + # if /tmp/.ssh/tangled exists, origin push mirrors to knot + GitHub 134 142 ``` 135 143 136 144 ### OTA update
+6
fedac/native/initramfs/init
··· 31 31 cp /claude-state.json /tmp/.claude.json 2>/dev/null 32 32 printf '{"permissions":{"allow":["Bash(*)","Read(*)","Write(*)","Edit(*)","Glob(*)","Grep(*)","WebFetch(*)","WebSearch(*)"]},"autoUpdates":false,"installMethod":"native"}\n' > /tmp/.claude/settings.json 33 33 fi 34 + if [ -f /tangled-key ] || [ -f /tangled-ssh-config ] || [ -f /tangled-known-hosts ]; then 35 + mkdir -p /tmp/.ssh 36 + [ -f /tangled-key ] && cp /tangled-key /tmp/.ssh/tangled 2>/dev/null && chmod 600 /tmp/.ssh/tangled 2>/dev/null 37 + [ -f /tangled-ssh-config ] && cp /tangled-ssh-config /tmp/.ssh/config 2>/dev/null && chmod 600 /tmp/.ssh/config 2>/dev/null 38 + [ -f /tangled-known-hosts ] && cp /tangled-known-hosts /tmp/.ssh/known_hosts 2>/dev/null && chmod 600 /tmp/.ssh/known_hosts 2>/dev/null 39 + fi 34 40 mkdir -p /tmp/ac 35 41 [ -f /device-claude.md ] && cp /device-claude.md /tmp/ac/CLAUDE.md 2>/dev/null 36 42 [ -f /device-score.md ] && cp /device-score.md /tmp/ac/SCORE.md 2>/dev/null
+3 -3
fedac/native/scripts/build-and-flash-initramfs.sh
··· 636 636 warn "dropbear not found — SSH remote access not available" 637 637 fi 638 638 639 - # ── Claude Code utilities (git, ripgrep, jq) ── 639 + # ── Claude Code utilities (git, ripgrep, jq, ssh) ── 640 640 # These tools make Claude Code significantly more capable on the device. 641 - for util in git rg jq; do 641 + for util in git rg jq ssh; do 642 642 UTIL_PATH="$(command -v "$util" 2>/dev/null || true)" 643 643 if [ -n "$UTIL_PATH" ] && [ -f "$UTIL_PATH" ]; then 644 - cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/" 644 + cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/${util}" 645 645 for lib in $(ldd "$UTIL_PATH" 2>/dev/null | grep -oP '/\S+'); do 646 646 [ -f "$lib" ] && cp -n "$lib" "${INITRAMFS_DIR}/lib64/" 2>/dev/null || true 647 647 done
+3 -3
fedac/native/scripts/build-and-flash.sh
··· 537 537 warn "dropbear not found — SSH remote access not available" 538 538 fi 539 539 540 - # ── Claude Code utilities (git, ripgrep, jq) ── 540 + # ── Claude Code utilities (git, ripgrep, jq, ssh) ── 541 541 # These tools make Claude Code significantly more capable on the device. 542 - for util in git rg jq; do 542 + for util in git rg jq ssh; do 543 543 UTIL_PATH="$(command -v "$util" 2>/dev/null || true)" 544 544 if [ -n "$UTIL_PATH" ] && [ -f "$UTIL_PATH" ]; then 545 - cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/" 545 + cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/${util}" 546 546 for lib in $(ldd "$UTIL_PATH" 2>/dev/null | grep -oP '/\S+'); do 547 547 [ -f "$lib" ] && cp -nL "$lib" "${INITRAMFS_DIR}/lib64/" 2>/dev/null || true 548 548 done
+31 -9
fedac/native/src/ac-native.c
··· 3960 3960 } 3961 3961 // Clone (or pull) the aesthetic-computer repo to 3962 3962 // /mnt/ac-repo so Claude Code has a real project to 3963 - // work in. Runs in the background so boot isn't 3964 - // delayed. If /github-pat is present, git picks it up 3965 - // via the git-credential-ac helper. We use a shallow 3963 + // work in. Fetch stays on the GitHub HTTPS mirror for 3964 + // simple read access; if a Tangled SSH key was baked, 3965 + // the repo also gets origin pushurls for knot + GitHub 3966 + // and a dedicated `tangled` remote. Runs in the 3967 + // background so boot isn't delayed. We use a shallow 3966 3968 // clone (--depth=50) to keep the size manageable 3967 3969 // (~200 MB) while still giving Claude enough history 3968 3970 // for basic blame/log work. ··· 3971 3973 // `git pull` runs instead (bounded by 30s timeout). 3972 3974 // Logs go to /tmp/ac-repo-clone.log. 3973 3975 { 3974 - char clone_cmd[1024]; 3976 + char clone_cmd[3072]; 3975 3977 snprintf(clone_cmd, sizeof(clone_cmd), 3976 - "( if [ -d /mnt/ac-repo/.git ]; then " 3978 + "( REPO=/mnt/ac-repo; " 3979 + " FETCH_URL='https://github.com/whistlegraph/aesthetic-computer.git'; " 3980 + " TANGLED_URL='git@knot.aesthetic.computer:aesthetic.computer/core'; " 3981 + " if [ -d \"$REPO/.git\" ]; then " 3977 3982 " echo '[ac-repo] pulling latest' && " 3978 - " cd /mnt/ac-repo && timeout 30 git pull --ff-only 2>&1; " 3983 + " cd \"$REPO\" && timeout 30 git pull --ff-only 2>&1; " 3979 3984 " else " 3980 3985 " echo '[ac-repo] cloning (shallow)' && " 3981 - " timeout 300 git clone --depth=50 --branch=main " 3982 - " https://github.com/whistlegraph/aesthetic-computer.git " 3983 - " /mnt/ac-repo 2>&1; " 3986 + " timeout 300 git clone --depth=50 --branch=main \"$FETCH_URL\" \"$REPO\" 2>&1; " 3987 + " fi; " 3988 + " if [ -d \"$REPO/.git\" ]; then " 3989 + " cd \"$REPO\"; " 3990 + " git remote set-url origin \"$FETCH_URL\"; " 3991 + " git config --unset-all remote.origin.pushurl 2>/dev/null || true; " 3992 + " if [ -f /tmp/.ssh/tangled ]; then " 3993 + " git config --add remote.origin.pushurl \"$TANGLED_URL\"; " 3994 + " git config --add remote.origin.pushurl \"$FETCH_URL\"; " 3995 + " if git remote | grep -qx tangled; then " 3996 + " git remote set-url tangled \"$TANGLED_URL\"; " 3997 + " else " 3998 + " git remote add tangled \"$TANGLED_URL\"; " 3999 + " fi; " 4000 + " echo '[ac-repo] origin pushurl: tangled + github'; " 4001 + " else " 4002 + " git config --add remote.origin.pushurl \"$FETCH_URL\"; " 4003 + " git remote remove tangled 2>/dev/null || true; " 4004 + " echo '[ac-repo] origin pushurl: github only (no tangled key)'; " 4005 + " fi; " 3984 4006 " fi " 3985 4007 ") >> /tmp/ac-repo-clone.log 2>&1 &"); 3986 4008 system(clone_cmd);
+13 -6
fedac/native/src/pty.c
··· 561 561 setenv("SHELL", "/bin/bash", 1); 562 562 setenv("USER", "root", 1); 563 563 setenv("LOGNAME", "root", 1); 564 + setenv("GIT_TERMINAL_PROMPT", "0", 1); 564 565 setenv("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1", 1); 565 566 // SSL certs for API connections 566 567 setenv("SSL_CERT_FILE", "/etc/pki/tls/certs/ca-bundle.crt", 0); 567 568 setenv("SSL_CERT_DIR", "/etc/ssl/certs", 0); 568 569 setenv("CURL_CA_BUNDLE", "/etc/pki/tls/certs/ca-bundle.crt", 0); 569 570 setenv("NODE_EXTRA_CA_CERTS", "/etc/pki/tls/certs/ca-bundle.crt", 0); 571 + if (access("/bin/ssh", X_OK) == 0 && access("/tmp/.ssh/config", R_OK) == 0) { 572 + setenv("GIT_SSH_COMMAND", "/bin/ssh -F /tmp/.ssh/config -o BatchMode=yes", 1); 573 + setenv("GIT_SSH_VARIANT", "ssh", 1); 574 + } 570 575 // GitHub PAT (for git operations) 571 576 { 572 577 FILE *gf = fopen("/github-pat", "r"); ··· 625 630 626 631 // Working directory strategy: 627 632 // 1. /mnt/ac-repo (persistent shallow clone of aesthetic-computer, 628 - // cloned on first boot or manually by the user) — PREFERRED so 629 - // Claude can actually `git commit` real project files 633 + // auto-cloned on WiFi connect or manually by the user) — PREFERRED 634 + // so Claude can actually `git commit` real project files 630 635 // 2. /tmp/ac (tmpfs, persists per-session only) — FALLBACK for 631 636 // when there's no repo yet. Claude can still edit scratch 632 637 // files here. A placeholder git init makes it look like a ··· 665 670 "- **No package manager** — binaries are baked into initramfs\n\n" 666 671 "## Filesystem\n" 667 672 "- `/mnt` — USB boot drive (FAT32, has config.json and logs)\n" 668 - "- `/mnt/ac-repo` — full aesthetic-computer git repo (persistent)\n" 673 + "- `/mnt/ac-repo` — persistent shallow repo clone (GitHub fetch mirror)\n" 669 674 "- `/mnt/tapes` — MP4 tapes recorded via PrintScreen\n" 670 675 "- `/tmp` — tmpfs (lost on reboot)\n" 671 - "- `/bin` — busybox + baked binaries (bash, git, rg, jq)\n\n" 676 + "- `/tmp/.ssh` — Tangled SSH key/config, when baked via `ac-os`\n" 677 + "- `/bin` — busybox + baked binaries (bash, git, rg, jq, ssh)\n\n" 672 678 "## Networking\n" 673 679 "- WiFi auto-connects to saved networks\n" 674 680 "- `curl` is available for HTTP requests\n" 675 - "- GitHub PAT is pre-configured for git push/pull\n\n" 681 + "- `/mnt/ac-repo` fetches from the GitHub mirror\n" 682 + "- If `/tmp/.ssh/tangled` exists, `git push origin` mirrors to knot + GitHub\n\n" 676 683 "## What you can do\n" 677 684 "- Write and run shell scripts (bash, not busybox)\n" 678 685 "- Use curl to interact with APIs\n" 679 686 "- Edit files in /mnt/ac-repo and `git commit` them\n" 680 - "- Use git (GitHub PAT is wired up for whistlegraph)\n" 687 + "- Use git (GitHub PAT is wired up; Tangled push works when the SSH key is baked)\n" 681 688 "- Read system logs at /mnt/ac-native.log\n", 682 689 handle, handle); 683 690 fclose(cm);
+138
reports/2026-04-11-notepat-ota-usb-flash-report.md
··· 1 + # 2026-04-11 — notepat OTA build and USB flash report 2 + 3 + Session focus: ship the async DJ USB mount check and Notepat live-percussion 4 + work as a fresh OTA build, then get that exact OTA image onto the USB with 5 + Jeffrey's inscribed credentials and preserved Wi-Fi seeds. 6 + 7 + Final release published in oven: 8 + 9 + - Release: `dynamic-milksnake` 10 + - Commit: `848d3231a` 11 + - Build stamp: `848d3231a-2026-04-11T21:30` 12 + - Oven job: `fff0bd10-0` 13 + 14 + --- 15 + 16 + ## 1. What shipped 17 + 18 + This build includes the native change that makes the DJ music-USB probe 19 + non-blocking: 20 + 21 + - `system.mountMusic()` now schedules its probe off-thread instead of blocking 22 + the main JS/render path 23 + - JS callers now consume cached mount state via 24 + `system.mountMusicMounted` / `system.mountMusicPending` 25 + - This closes the periodic crawl caused by synchronous USB polling during play 26 + 27 + Work was committed and pushed as: 28 + 29 + - Commit: `848d3231a5c5126cf2df1f731fd0d293d7a62350` 30 + - Message: `native: make dj usb mount check async` 31 + 32 + --- 33 + 34 + ## 2. OTA build result 35 + 36 + Manual oven trigger was required because the native poller was not picking the 37 + build up automatically at the time of the session. 38 + 39 + Published artifacts included: 40 + 41 + - `os/releases.json` 42 + - `os/native-notepat-latest.vmlinuz` 43 + - `os/native-notepat-latest.vmlinuz-slim` 44 + - `os/native-notepat-latest.initramfs.cpio.gz` 45 + 46 + OTA publication succeeded and `ac-os pull` was able to download and verify the 47 + new release payload. 48 + 49 + --- 50 + 51 + ## 3. Standard flash-path failure 52 + 53 + The normal `./fedac/native/ac-os pull` flash path did not complete on this host. 54 + 55 + Observed failure: 56 + 57 + - partitioning completed, but kernel partition-table reread stayed busy 58 + - `mkfs.vfat` on `/dev/sda1` retried repeatedly and failed with 59 + `Device or resource busy` 60 + - direct host-side reread attempts (`blockdev --rereadpt`, `partx -u`) were 61 + also blocked 62 + 63 + This meant the OTA payload was downloaded correctly, but the standard helper 64 + could not safely move from partitioning into filesystem creation on the live 65 + USB device. 66 + 67 + --- 68 + 69 + ## 4. Raw-image fallback 70 + 71 + To bypass the host's live partition-reread problem, the USB was flashed through 72 + a raw-disk fallback: 73 + 74 + 1. Download OTA artifacts with `ac-os pull` 75 + 2. Stage `config.json`, `wifi_creds.json`, kernel, initramfs, and EFI payloads 76 + 3. Build full `ACBOOT`, `ACEFI`, and `AC-MAC` partition images offline 77 + 4. Assemble a complete GPT raw USB image 78 + 5. Write that raw image directly to `/dev/sda` 79 + 6. Read files back from the physical device by partition offset to verify the 80 + result 81 + 82 + The bulk raw write completed successfully: 83 + 84 + - bytes written: `15518924800` 85 + - elapsed: `742.457 s` 86 + - sustained rate at completion: `20.9 MB/s` 87 + 88 + --- 89 + 90 + ## 5. Device readback verification 91 + 92 + Readback from the physical USB confirmed: 93 + 94 + - `config.json` present on the device 95 + - `wifi_creds.json` present on the device 96 + - handle: `jeffrey` 97 + - AC token: present 98 + - Claude token: present 99 + - GitHub PAT: present 100 + - `claudeCreds`: present 101 + - `claudeState`: present 102 + - Wi-Fi networks preserved: `4` 103 + 104 + Readback SSIDs: 105 + 106 + - `aesthetic.computer` 107 + - `ATT2AWTpcr` 108 + - `GettyLink` 109 + - `Tondo_Guest` 110 + 111 + Partition content checks: 112 + 113 + - `ACBOOT` contained `EFI/BOOT/BOOTX64.EFI` and `BOOTIA32.EFI` 114 + - `ACEFI` contained `EFI/BOOT/BOOTX64.EFI`, `LOADER.EFI`, and `KERNEL.EFI` 115 + 116 + --- 117 + 118 + ## 6. Caveat 119 + 120 + Immediately after the raw write, host-side `lsblk` still showed `/dev/sda1-3` 121 + without refreshed filesystem metadata. That appears to be the same host/kernel 122 + partition-refresh limitation that broke the standard flash path. 123 + 124 + Important distinction: 125 + 126 + - host metadata refresh was stale 127 + - direct readback from the physical USB contents succeeded 128 + 129 + So the USB image itself verified cleanly even though the host did not fully 130 + refresh its partition view until replug. 131 + 132 + --- 133 + 134 + ## 7. Outcome 135 + 136 + The `dynamic-milksnake` OTA build for commit `848d3231a` was successfully 137 + published and successfully written to the USB, with credentials and Wi-Fi data 138 + inscribed and verified from the device itself.