From 1835130d0791ffc21636408535ea1946482040f6 Mon Sep 17 00:00:00 2001 From: mega123-art Date: Sat, 11 Jul 2026 16:29:29 +0530 Subject: [PATCH 1/3] Fix Android rootfs asset rollback for git clone --- .github/workflows/android-apk.yml | 12 +++++- .github/workflows/android-assets.yml | 3 +- .github/workflows/android-release.yml | 12 +++++- install-guide/android.md | 3 +- .../java/com/iqlabs/agentnet/Installer.kt | 40 +++++++++++++++++-- .../java/com/iqlabs/agentnet/ServerManager.kt | 12 ++---- surfaces/android/scripts/build-assets.sh | 16 +++++--- 7 files changed, 76 insertions(+), 22 deletions(-) diff --git a/.github/workflows/android-apk.yml b/.github/workflows/android-apk.yml index 155701a..6f77b1f 100644 --- a/.github/workflows/android-apk.yml +++ b/.github/workflows/android-apk.yml @@ -114,12 +114,22 @@ jobs: - name: Stage assets into the source tree run: | set -e + manifest=ci-assets/assets/agentnet-rootfs.env + if [ ! -f "$manifest" ]; then + echo "::error::android-assets artifact has no rootfs manifest. Re-run android-assets so APKs cannot silently reuse the known-bad Ubuntu 24.04 rootfs." + exit 1 + fi + . "$manifest" + if [ "${AGENTNET_ROOTFS_BASE_IMAGE:-}" != "ubuntu:22.04" ]; then + echo "::error::android-assets artifact was built from ${AGENTNET_ROOTFS_BASE_IMAGE:-unknown}; issue #112 requires ubuntu:22.04. Re-run android-assets." + exit 1 + fi # assets/ is gitignored (its tars never get committed), so it doesn't exist on a # fresh checkout — create it before copying. mkdir -p surfaces/android/app/src/main/assets rm -rf surfaces/android/app/src/main/jniLibs cp -R ci-assets/jniLibs surfaces/android/app/src/main/jniLibs - cp ci-assets/assets/rootfs-*.tar surfaces/android/app/src/main/assets/ + cp ci-assets/assets/rootfs-*.tar ci-assets/assets/agentnet-rootfs.env surfaces/android/app/src/main/assets/ # Repack the server bundle FRESH from this run's dist (don't reuse the artifact's # stale agentnet-server.tar) so the APK carries the current UI. STAGE="$(mktemp -d)/server-bundle" diff --git a/.github/workflows/android-assets.yml b/.github/workflows/android-assets.yml index 673e8fe..f91832a 100644 --- a/.github/workflows/android-assets.yml +++ b/.github/workflows/android-assets.yml @@ -53,7 +53,8 @@ jobs: docker run --rm --platform linux/arm64 \ -v "${{ github.workspace }}:/work" -w /work \ -e ABI=${{ github.event.inputs.abi }} -e ALLOW_CROSS=1 \ - ubuntu:24.04 \ + -e AGENTNET_ROOTFS_BASE_IMAGE=ubuntu:22.04 \ + ubuntu:22.04 \ bash -c ' set -e apt-get update diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 73578d4..bec7711 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -110,10 +110,20 @@ jobs: - name: Stage assets into the source tree run: | set -e + manifest=ci-assets/assets/agentnet-rootfs.env + if [ ! -f "$manifest" ]; then + echo "::error::android-assets artifact has no rootfs manifest. Re-run android-assets so releases cannot silently reuse the known-bad Ubuntu 24.04 rootfs." + exit 1 + fi + . "$manifest" + if [ "${AGENTNET_ROOTFS_BASE_IMAGE:-}" != "ubuntu:22.04" ]; then + echo "::error::android-assets artifact was built from ${AGENTNET_ROOTFS_BASE_IMAGE:-unknown}; issue #112 requires ubuntu:22.04. Re-run android-assets." + exit 1 + fi mkdir -p surfaces/android/app/src/main/assets rm -rf surfaces/android/app/src/main/jniLibs cp -R ci-assets/jniLibs surfaces/android/app/src/main/jniLibs - cp ci-assets/assets/rootfs-*.tar surfaces/android/app/src/main/assets/ + cp ci-assets/assets/rootfs-*.tar ci-assets/assets/agentnet-rootfs.env surfaces/android/app/src/main/assets/ STAGE="$(mktemp -d)/server-bundle" mkdir -p "$STAGE/webview" cp -R surfaces/localhost/dist/. "$STAGE/" diff --git a/install-guide/android.md b/install-guide/android.md index a829ab1..155b36f 100644 --- a/install-guide/android.md +++ b/install-guide/android.md @@ -188,7 +188,8 @@ pnpm --filter agentnet-webview build docker run --rm --platform linux/arm64 \ -v "$(pwd):/work" -w /work \ -e ABI=arm64 -e ALLOW_CROSS=1 \ - ubuntu:24.04 \ + -e AGENTNET_ROOTFS_BASE_IMAGE=ubuntu:22.04 \ + ubuntu:22.04 \ bash -c 'apt-get update -qq && apt-get install -y -qq curl ca-certificates xz-utils tar coreutils && bash surfaces/android/scripts/build-assets.sh' ``` This writes the tars into `assets/` and the proot `lib*.so` files into `jniLibs/arm64-v8a/`. diff --git a/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt b/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt index 991b908..ff5f1aa 100644 --- a/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt +++ b/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt @@ -24,13 +24,14 @@ import java.io.File class Installer(private val ctx: Context) { companion object { private const val TAG = "AgentNet/Installer" - // Bumped v2 -> v3 to force a one-time rootfs re-extraction on existing installs: - // the hardlink-extraction fix (guest-absolute symlinks) lives in how the rootfs is - // unpacked, so binaries like claude stay broken until the rootfs is laid down again. + // Bumped v3 -> v4 to force a one-time rootfs re-extraction on existing installs: + // issue #112 is fixed by replacing the Ubuntu 24.04 guest asset with the 22.04 + // rootfs, so server-bundle-only updates are not enough. The hardlink-extraction + // v3 notes still apply: marker bumps are how heavy rootfs fixes reach devices. // The MARKER only re-extracts on a fresh marker, so bumping it is the only way the // fix reaches devices that update in place. Re-extract is from the bundled tar (no // network download). - private const val MARKER = ".installed-v3" + private const val MARKER = ".installed-v4" // Server bundle is small and changes every app build; its marker holds the app's // versionCode so an APK update re-extracts ONLY the server bundle (the heavy // rootfs is left alone). Without this, the idempotent MARKER froze the server @@ -107,8 +108,11 @@ class Installer(private val ctx: Context) { // keep this dependency-free, we unpack with Android's own gzip/xz is not // available, so the rootfs ships as a plain tar (.tar) and we stream-untar it // in Kotlin before first proot use. + val preservedHome = preserveGuestHome(p) + File(p.rootfs).deleteRecursively() Paths.dir(p.rootfs) TarExtractor.extract(ctx.assets.open("rootfs-$abi.tar"), File(p.rootfs)) + restoreGuestHome(p, preservedHome) configureGuest(p) // DNS/hosts/tmp — Android doesn't provide these to the guest onProgress("Almost there") @@ -118,6 +122,34 @@ class Installer(private val ctx: Context) { Log.i(TAG, "install complete") } + private fun preserveGuestHome(p: Paths.Layout): File? { + val home = File(p.home) + if (!home.exists()) return null + val backup = File(ctx.filesDir, ".guest-home-before-v4") + backup.deleteRecursively() + if (home.renameTo(backup)) return backup + return runCatching { + home.copyRecursively(backup, overwrite = true) + backup + }.onFailure { + Log.w(TAG, "could not preserve guest /root before rootfs replacement", it) + }.getOrNull() + } + + private fun restoreGuestHome(p: Paths.Layout, preservedHome: File?) { + if (preservedHome == null || !preservedHome.exists()) return + File(p.home).deleteRecursively() + val home = File(p.home) + if (!preservedHome.renameTo(home)) { + runCatching { + preservedHome.copyRecursively(home, overwrite = true) + preservedHome.deleteRecursively() + }.onFailure { + Log.w(TAG, "could not restore preserved guest /root after rootfs replacement", it) + } + } + } + // Android exposes neither /etc/resolv.conf nor /etc/hosts to the proot guest, so // the agent CLIs' DNS lookups (to the model API) fail without these. proot-distro // writes the same files at install time. /tmp must exist + be world-writable since diff --git a/surfaces/android/app/src/main/java/com/iqlabs/agentnet/ServerManager.kt b/surfaces/android/app/src/main/java/com/iqlabs/agentnet/ServerManager.kt index 2fd1c05..5ef8980 100644 --- a/surfaces/android/app/src/main/java/com/iqlabs/agentnet/ServerManager.kt +++ b/surfaces/android/app/src/main/java/com/iqlabs/agentnet/ServerManager.kt @@ -44,15 +44,9 @@ class ServerManager(private val ctx: Context) { "TERM=xterm-256color", "LANG=C.UTF-8", "TMPDIR=/tmp", - // Ubuntu 24.04's glibc (2.39) registers rseq for every thread. proot does not - // translate the rseq syscall AT ALL (it is absent from termux/proot's syscall - // table), so kernel-side rseq areas coexist with proot's ptrace rewriting and - // corrupt state under a loaded tracer — measured on the Seeker as git clone - // failures ("remote did not send all necessary objects" / lost .git files) - // whenever git runs beside thread-heavy node under ONE proot (issue #112, - // A/B-verified on device). This tunable is glibc's official switch for rseq - // (no proot-distro precedent — they don't handle it as of 2026-07). Set here - // so node and every child it spawns (claude/codex/git) inherit it. + // Keep rseq disabled when the guest libc supports this tunable. issue #112 is + // fixed by shipping the 22.04 rootfs again, but this remains a low-cost guard + // if a future asset build returns to a newer glibc under proot. "GLIBC_TUNABLES=glibc.pthread.rseq=0", "AGENTNET_PORT=${Paths.PORT}", // the React SPA ships alongside the server bundle (build-assets.sh packs it at diff --git a/surfaces/android/scripts/build-assets.sh b/surfaces/android/scripts/build-assets.sh index 65dc73d..4ad1c9d 100755 --- a/surfaces/android/scripts/build-assets.sh +++ b/surfaces/android/scripts/build-assets.sh @@ -21,6 +21,7 @@ set -euo pipefail ABI="${ABI:-arm64}" # arm64 | x86_64 +AGENTNET_ROOTFS_BASE_IMAGE="${AGENTNET_ROOTFS_BASE_IMAGE:-unknown}" case "$ABI" in arm64) PROOT_ARCH="aarch64" ;; x86_64) PROOT_ARCH="x86_64" ;; @@ -35,6 +36,7 @@ WORK="${WORK:-$ANDROID_DIR/.assets-build}" mkdir -p "$ASSETS" "$WORK" echo "==> ABI=$ABI proot=$PROOT_ARCH" +echo "==> rootfs base image: $AGENTNET_ROOTFS_BASE_IMAGE" echo "==> assets -> $ASSETS" echo "==> work -> $WORK" @@ -167,14 +169,18 @@ apt-get clean && rm -rf /var/lib/apt/lists/* # ship the agent environment guidance into the guest cp "$ANDROID_DIR/guest/AGENTS.md" /root/AGENTS.md -# 24.04's glibc registers rseq per-thread; proot passes the rseq syscall through -# untranslated, which corrupts traced processes under load (issue #112, A/B-verified on -# device). The app covers node + children via guest env (ServerManager.buildGuestEnv); -# this profile.d covers any LOGIN shell entered another way (adb + manual proot, the -# future in-app terminal) — same pattern as proot-distro's inject_termux_profile. +# Keep rseq disabled for login shells when the guest libc supports this tunable. issue +# #112 is fixed by shipping the 22.04 rootfs again, but this remains a low-cost guard if +# a future asset build returns to a newer glibc under proot. The app covers node + +# children via guest env; this profile.d covers adb/manual proot entry too. cat > /etc/profile.d/00-agentnet-rseq.sh <<'RSEQ' export GLIBC_TUNABLES=glibc.pthread.rseq=0 RSEQ +cat > "$ASSETS/agentnet-rootfs.env" < Date: Sat, 11 Jul 2026 20:50:23 +0530 Subject: [PATCH 2/3] Fix #112: clone via dulwich shim under targetSdk-35 proot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native `git clone` fails in the proot guest on Android's targetSdk-35 untrusted_app SELinux domain: the multi-process smart-HTTP pack fetch (git -> git-remote-https -> index-pack) truncates the packfile ("remote did not send all necessary objects"), and freshly written objects are briefly invisible across sibling git subprocesses. curl, node, ls-remote and single git commands all work in-app; only git's multi-process object machinery breaks. targetSdk 28 (the looser untrusted_app_28 domain) works, but Play requires 35 for new apps, so a targetSdk downgrade is not viable. Ship a git-clone shim ahead of /usr/bin/git on the guest PATH: it routes `clone` through dulwich (pure-Python git, single process, plain file I/O) and passes everything else to the real git untouched. Verified on-device (Galaxy A35, targetSdk 35): clone completes, HEAD/pack/worktree correct, git fsck clean. - build-assets.sh: install python3-dulwich; drop the shim at /usr/local/bin/git - guest/git-clone-shim.sh, guest/agentnet-git-clone.py: the shim - Installer: bump marker v4 -> v5 to re-extract the rootfs with the fix Supersedes the earlier rootfs-rollback theory: verified on-device that the Ubuntu version (22.04 vs 24.04) is irrelevant — clone fails identically on both. The trigger is the targetSdk-35 domain, not glibc/rseq. Co-Authored-By: Claude Opus 4.8 --- surfaces/android/.gitignore | 1 + .../java/com/iqlabs/agentnet/Installer.kt | 15 ++++--- surfaces/android/guest/agentnet-git-clone.py | 42 +++++++++++++++++++ surfaces/android/guest/git-clone-shim.sh | 18 ++++++++ surfaces/android/scripts/build-assets.sh | 11 ++++- 5 files changed, 78 insertions(+), 9 deletions(-) create mode 100755 surfaces/android/guest/agentnet-git-clone.py create mode 100755 surfaces/android/guest/git-clone-shim.sh diff --git a/surfaces/android/.gitignore b/surfaces/android/.gitignore index 187a3c7..4bce2ba 100644 --- a/surfaces/android/.gitignore +++ b/surfaces/android/.gitignore @@ -18,4 +18,5 @@ app/src/main/jniLibs/ app/src/main/assets/proot-* app/src/main/assets/rootfs-* app/src/main/assets/agentnet-server.tar +app/src/main/assets/agentnet-rootfs.env .assets-build/ diff --git a/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt b/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt index ff5f1aa..80defb3 100644 --- a/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt +++ b/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt @@ -24,14 +24,13 @@ import java.io.File class Installer(private val ctx: Context) { companion object { private const val TAG = "AgentNet/Installer" - // Bumped v3 -> v4 to force a one-time rootfs re-extraction on existing installs: - // issue #112 is fixed by replacing the Ubuntu 24.04 guest asset with the 22.04 - // rootfs, so server-bundle-only updates are not enough. The hardlink-extraction - // v3 notes still apply: marker bumps are how heavy rootfs fixes reach devices. - // The MARKER only re-extracts on a fresh marker, so bumping it is the only way the - // fix reaches devices that update in place. Re-extract is from the bundled tar (no - // network download). - private const val MARKER = ".installed-v4" + // Bumped v4 -> v5 to force a one-time rootfs re-extraction on existing installs: + // issue #112's real fix ships IN the rootfs (python3-dulwich + the git-clone shim at + // /usr/local/bin/git — native git clone is corrupted by proot under targetSdk-35's + // untrusted_app domain), so a server-bundle-only update is not enough. Marker bumps + // are how heavy rootfs fixes reach devices: the MARKER only re-extracts on a fresh + // marker, and the re-extract is from the bundled tar (no network download). + private const val MARKER = ".installed-v5" // Server bundle is small and changes every app build; its marker holds the app's // versionCode so an APK update re-extracts ONLY the server bundle (the heavy // rootfs is left alone). Without this, the idempotent MARKER froze the server diff --git a/surfaces/android/guest/agentnet-git-clone.py b/surfaces/android/guest/agentnet-git-clone.py new file mode 100755 index 0000000..591455b --- /dev/null +++ b/surfaces/android/guest/agentnet-git-clone.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# issue #112 — single-process `git clone` for the proot guest (see git-clone-shim.sh). +# dulwich runs the entire clone in one Python process via plain file I/O, avoiding git's +# multi-process pack fetch and cross-subprocess file handoff, both of which proot corrupts +# under Android's targetSdk-35 untrusted_app domain. +import sys +from dulwich import porcelain + + +def main(): + args = [a for a in sys.argv[1:] if a != "clone"] + depth = None + positional = [] + it = iter(args) + for a in it: + if a == "--depth": + depth = int(next(it)) + elif a.startswith("--depth="): + depth = int(a.split("=", 1)[1]) + elif a.startswith("-"): + continue # ignore -q/--progress/--single-branch/etc. + else: + positional.append(a) + if not positional: + sys.stderr.write("git clone: missing repository URL\n") + sys.exit(2) + url = positional[0] + if len(positional) > 1: + dst = positional[1] + else: + tail = url.rstrip("/").split("/")[-1] + dst = tail[:-4] if tail.endswith(".git") else tail + sys.stderr.write("Cloning into '%s'...\n" % dst) + try: + porcelain.clone(url, dst, depth=depth, errstream=sys.stderr.buffer) + except Exception as e: # noqa: BLE001 — surface any dulwich failure like git would + sys.stderr.write("fatal: clone failed: %s\n" % e) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/surfaces/android/guest/git-clone-shim.sh b/surfaces/android/guest/git-clone-shim.sh new file mode 100755 index 0000000..11e0bbb --- /dev/null +++ b/surfaces/android/guest/git-clone-shim.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# issue #112 — `git clone` shim for the proot guest. +# +# Under Android's targetSdk-35 `untrusted_app` SELinux domain, proot corrupts git's native +# clone path: the multi-process smart-HTTP pack fetch (git → git-remote-https → index-pack, +# concurrent + piped) truncates the packfile ("remote did not send all necessary objects"), +# and freshly-written objects are briefly invisible across sibling git subprocesses. curl, +# node and single git commands are fine; only git's multi-process object machinery breaks. +# (targetSdk 28 = the looser `untrusted_app_28` domain works, but Play requires 35.) +# +# dulwich (pure-Python git) performs the whole clone in ONE process using plain file I/O — +# the path that works here. Route `clone` through it; everything else uses the real git. +for a in "$@"; do + case "$a" in + clone) exec python3 /usr/local/bin/agentnet-git-clone.py "$@" ;; + esac +done +exec /usr/bin/git "$@" diff --git a/surfaces/android/scripts/build-assets.sh b/surfaces/android/scripts/build-assets.sh index 4ad1c9d..d677ea4 100755 --- a/surfaces/android/scripts/build-assets.sh +++ b/surfaces/android/scripts/build-assets.sh @@ -139,7 +139,9 @@ if [ "$(id -u)" != "0" ]; then fi export DEBIAN_FRONTEND=noninteractive apt-get update -apt-get install -y curl ca-certificates git ripgrep xz-utils +# python3-dulwich backs the git-clone shim (issue #112): native git clone is corrupted +# under proot on Android's targetSdk-35 untrusted_app domain; dulwich clones in one process. +apt-get install -y curl ca-certificates git ripgrep xz-utils python3 python3-dulwich # node (NodeSource LTS) curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - apt-get install -y nodejs @@ -169,6 +171,13 @@ apt-get clean && rm -rf /var/lib/apt/lists/* # ship the agent environment guidance into the guest cp "$ANDROID_DIR/guest/AGENTS.md" /root/AGENTS.md +# issue #112: install the git-clone shim ahead of /usr/bin/git on PATH. It routes `git clone` +# through dulwich (single-process, works under targetSdk-35 proot) and passes everything else +# to the real git. /usr/local/bin is first in the guest PATH (see ServerManager.buildGuestEnv). +cp "$ANDROID_DIR/guest/agentnet-git-clone.py" /usr/local/bin/agentnet-git-clone.py +cp "$ANDROID_DIR/guest/git-clone-shim.sh" /usr/local/bin/git +chmod +x /usr/local/bin/agentnet-git-clone.py /usr/local/bin/git + # Keep rseq disabled for login shells when the guest libc supports this tunable. issue # #112 is fixed by shipping the 22.04 rootfs again, but this remains a low-cost guard if # a future asset build returns to a newer glibc under proot. The app covers node + From 0b6ea278c2aa482e6f279487fad0aed27c9ada76 Mon Sep 17 00:00:00 2001 From: sumin Date: Sun, 12 Jul 2026 16:45:38 +0900 Subject: [PATCH 3/3] Address review: fix shim routing/flags, drop 22.04 rollback vestige - git-clone-shim.sh: route to dulwich only when the FIRST non-option arg is 'clone', so 'git commit -m clone' / 'git checkout clone' pass through. - agentnet-git-clone.py: consume value-taking flags so their values are not mistaken for the URL; honor -b/--branch (dulwich 0.20.31 clone(branch=) expects bytes) alongside --depth. - Revert the Ubuntu 22.04 rollback, the rootfs manifest, and the CI hard-fail guards: this PR's own on-device finding is that the Ubuntu version is irrelevant (22.04 fails identically), and the guards would break every APK build until android-assets re-runs. The shim is the fix; the rollback was the disproven first theory. - Installer: marker comment corrected (main was at v3, not v4); guest-home backup renamed and a crashed-install orphaned backup is now adopted on the next attempt instead of being stranded. - ServerManager/build-assets comments: state accurately that rseq-off is a separately-measured guard, not the #112 fix (the untrusted_app transport corruption survives it). --- .github/workflows/android-apk.yml | 12 +------ .github/workflows/android-assets.yml | 3 +- .github/workflows/android-release.yml | 12 +------ install-guide/android.md | 3 +- surfaces/android/.gitignore | 1 - .../java/com/iqlabs/agentnet/Installer.kt | 13 ++++--- .../java/com/iqlabs/agentnet/ServerManager.kt | 8 +++-- surfaces/android/guest/agentnet-git-clone.py | 36 ++++++++++++++++--- surfaces/android/guest/git-clone-shim.sh | 7 ++++ surfaces/android/scripts/build-assets.sh | 15 +++----- 10 files changed, 60 insertions(+), 50 deletions(-) diff --git a/.github/workflows/android-apk.yml b/.github/workflows/android-apk.yml index 6f77b1f..155701a 100644 --- a/.github/workflows/android-apk.yml +++ b/.github/workflows/android-apk.yml @@ -114,22 +114,12 @@ jobs: - name: Stage assets into the source tree run: | set -e - manifest=ci-assets/assets/agentnet-rootfs.env - if [ ! -f "$manifest" ]; then - echo "::error::android-assets artifact has no rootfs manifest. Re-run android-assets so APKs cannot silently reuse the known-bad Ubuntu 24.04 rootfs." - exit 1 - fi - . "$manifest" - if [ "${AGENTNET_ROOTFS_BASE_IMAGE:-}" != "ubuntu:22.04" ]; then - echo "::error::android-assets artifact was built from ${AGENTNET_ROOTFS_BASE_IMAGE:-unknown}; issue #112 requires ubuntu:22.04. Re-run android-assets." - exit 1 - fi # assets/ is gitignored (its tars never get committed), so it doesn't exist on a # fresh checkout — create it before copying. mkdir -p surfaces/android/app/src/main/assets rm -rf surfaces/android/app/src/main/jniLibs cp -R ci-assets/jniLibs surfaces/android/app/src/main/jniLibs - cp ci-assets/assets/rootfs-*.tar ci-assets/assets/agentnet-rootfs.env surfaces/android/app/src/main/assets/ + cp ci-assets/assets/rootfs-*.tar surfaces/android/app/src/main/assets/ # Repack the server bundle FRESH from this run's dist (don't reuse the artifact's # stale agentnet-server.tar) so the APK carries the current UI. STAGE="$(mktemp -d)/server-bundle" diff --git a/.github/workflows/android-assets.yml b/.github/workflows/android-assets.yml index f91832a..673e8fe 100644 --- a/.github/workflows/android-assets.yml +++ b/.github/workflows/android-assets.yml @@ -53,8 +53,7 @@ jobs: docker run --rm --platform linux/arm64 \ -v "${{ github.workspace }}:/work" -w /work \ -e ABI=${{ github.event.inputs.abi }} -e ALLOW_CROSS=1 \ - -e AGENTNET_ROOTFS_BASE_IMAGE=ubuntu:22.04 \ - ubuntu:22.04 \ + ubuntu:24.04 \ bash -c ' set -e apt-get update diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index bec7711..73578d4 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -110,20 +110,10 @@ jobs: - name: Stage assets into the source tree run: | set -e - manifest=ci-assets/assets/agentnet-rootfs.env - if [ ! -f "$manifest" ]; then - echo "::error::android-assets artifact has no rootfs manifest. Re-run android-assets so releases cannot silently reuse the known-bad Ubuntu 24.04 rootfs." - exit 1 - fi - . "$manifest" - if [ "${AGENTNET_ROOTFS_BASE_IMAGE:-}" != "ubuntu:22.04" ]; then - echo "::error::android-assets artifact was built from ${AGENTNET_ROOTFS_BASE_IMAGE:-unknown}; issue #112 requires ubuntu:22.04. Re-run android-assets." - exit 1 - fi mkdir -p surfaces/android/app/src/main/assets rm -rf surfaces/android/app/src/main/jniLibs cp -R ci-assets/jniLibs surfaces/android/app/src/main/jniLibs - cp ci-assets/assets/rootfs-*.tar ci-assets/assets/agentnet-rootfs.env surfaces/android/app/src/main/assets/ + cp ci-assets/assets/rootfs-*.tar surfaces/android/app/src/main/assets/ STAGE="$(mktemp -d)/server-bundle" mkdir -p "$STAGE/webview" cp -R surfaces/localhost/dist/. "$STAGE/" diff --git a/install-guide/android.md b/install-guide/android.md index 155b36f..a829ab1 100644 --- a/install-guide/android.md +++ b/install-guide/android.md @@ -188,8 +188,7 @@ pnpm --filter agentnet-webview build docker run --rm --platform linux/arm64 \ -v "$(pwd):/work" -w /work \ -e ABI=arm64 -e ALLOW_CROSS=1 \ - -e AGENTNET_ROOTFS_BASE_IMAGE=ubuntu:22.04 \ - ubuntu:22.04 \ + ubuntu:24.04 \ bash -c 'apt-get update -qq && apt-get install -y -qq curl ca-certificates xz-utils tar coreutils && bash surfaces/android/scripts/build-assets.sh' ``` This writes the tars into `assets/` and the proot `lib*.so` files into `jniLibs/arm64-v8a/`. diff --git a/surfaces/android/.gitignore b/surfaces/android/.gitignore index 4bce2ba..187a3c7 100644 --- a/surfaces/android/.gitignore +++ b/surfaces/android/.gitignore @@ -18,5 +18,4 @@ app/src/main/jniLibs/ app/src/main/assets/proot-* app/src/main/assets/rootfs-* app/src/main/assets/agentnet-server.tar -app/src/main/assets/agentnet-rootfs.env .assets-build/ diff --git a/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt b/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt index 80defb3..68dd622 100644 --- a/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt +++ b/surfaces/android/app/src/main/java/com/iqlabs/agentnet/Installer.kt @@ -24,9 +24,9 @@ import java.io.File class Installer(private val ctx: Context) { companion object { private const val TAG = "AgentNet/Installer" - // Bumped v4 -> v5 to force a one-time rootfs re-extraction on existing installs: - // issue #112's real fix ships IN the rootfs (python3-dulwich + the git-clone shim at - // /usr/local/bin/git — native git clone is corrupted by proot under targetSdk-35's + // Bumped v3 -> v5 (v4 skipped) to force a one-time rootfs re-extraction on existing + // installs: issue #112's fix ships IN the rootfs (python3-dulwich + the git-clone shim + // at /usr/local/bin/git — native git clone is corrupted by proot under targetSdk-35's // untrusted_app domain), so a server-bundle-only update is not enough. Marker bumps // are how heavy rootfs fixes reach devices: the MARKER only re-extracts on a fresh // marker, and the re-extract is from the bundled tar (no network download). @@ -123,8 +123,11 @@ class Installer(private val ctx: Context) { private fun preserveGuestHome(p: Paths.Layout): File? { val home = File(p.home) - if (!home.exists()) return null - val backup = File(ctx.filesDir, ".guest-home-before-v4") + val backup = File(ctx.filesDir, ".guest-home-backup") + // If a previous install attempt died between the rootfs delete and restore, the + // backup still holds the user's guest /root while home is gone — adopt it instead + // of returning null, or that data would be orphaned forever. + if (!home.exists()) return backup.takeIf { it.exists() } backup.deleteRecursively() if (home.renameTo(backup)) return backup return runCatching { diff --git a/surfaces/android/app/src/main/java/com/iqlabs/agentnet/ServerManager.kt b/surfaces/android/app/src/main/java/com/iqlabs/agentnet/ServerManager.kt index 5ef8980..f5b5186 100644 --- a/surfaces/android/app/src/main/java/com/iqlabs/agentnet/ServerManager.kt +++ b/surfaces/android/app/src/main/java/com/iqlabs/agentnet/ServerManager.kt @@ -44,9 +44,11 @@ class ServerManager(private val ctx: Context) { "TERM=xterm-256color", "LANG=C.UTF-8", "TMPDIR=/tmp", - // Keep rseq disabled when the guest libc supports this tunable. issue #112 is - // fixed by shipping the 22.04 rootfs again, but this remains a low-cost guard - // if a future asset build returns to a newer glibc under proot. + // Keep rseq disabled in the guest. NOT the fix for #112 — the app-domain + // (untrusted_app) transport corruption survives rseq-off; the fix is the + // git-clone shim in the rootfs. But rseq-under-ptrace is a real, separately + // measured corruption vector (flaky-link clones in the runas_app domain fail + // ~33% with rseq on, 0% with it off), so this stays as a cheap guard. "GLIBC_TUNABLES=glibc.pthread.rseq=0", "AGENTNET_PORT=${Paths.PORT}", // the React SPA ships alongside the server bundle (build-assets.sh packs it at diff --git a/surfaces/android/guest/agentnet-git-clone.py b/surfaces/android/guest/agentnet-git-clone.py index 591455b..2bddad4 100755 --- a/surfaces/android/guest/agentnet-git-clone.py +++ b/surfaces/android/guest/agentnet-git-clone.py @@ -7,18 +7,41 @@ from dulwich import porcelain +# Flags whose VALUE arrives as the next argv element. They must be consumed even when +# ignored, or the value would be mistaken for the URL (`git clone -b dev ` would +# try to clone "dev"). --depth and -b/--branch are honored; the rest are known +# value-taking clone flags that dulwich has no equivalent for — consumed and dropped. +VALUE_FLAGS = { + "--depth", "-b", "--branch", "-o", "--origin", "-c", "--config", + "--reference", "--reference-if-able", "--separate-git-dir", "--template", + "-u", "--upload-pack", "--shallow-since", "--shallow-exclude", "-j", "--jobs", + "--filter", "--bundle-uri", "--server-option", +} + + def main(): args = [a for a in sys.argv[1:] if a != "clone"] depth = None + branch = None positional = [] it = iter(args) for a in it: - if a == "--depth": - depth = int(next(it)) + if a in VALUE_FLAGS: + try: + v = next(it) + except StopIteration: + sys.stderr.write("git clone: flag %s requires a value\n" % a) + sys.exit(2) + if a == "--depth": + depth = int(v) + elif a in ("-b", "--branch"): + branch = v elif a.startswith("--depth="): depth = int(a.split("=", 1)[1]) + elif a.startswith("--branch="): + branch = a.split("=", 1)[1] elif a.startswith("-"): - continue # ignore -q/--progress/--single-branch/etc. + continue # valueless flags: -q/--progress/--single-branch/etc. else: positional.append(a) if not positional: @@ -32,7 +55,12 @@ def main(): dst = tail[:-4] if tail.endswith(".git") else tail sys.stderr.write("Cloning into '%s'...\n" % dst) try: - porcelain.clone(url, dst, depth=depth, errstream=sys.stderr.buffer) + porcelain.clone( + url, dst, depth=depth, + # dulwich 0.20.31 builds refs as bytes (refs/remotes// + branch) + branch=branch.encode("utf-8") if branch is not None else None, + errstream=sys.stderr.buffer, + ) except Exception as e: # noqa: BLE001 — surface any dulwich failure like git would sys.stderr.write("fatal: clone failed: %s\n" % e) sys.exit(1) diff --git a/surfaces/android/guest/git-clone-shim.sh b/surfaces/android/guest/git-clone-shim.sh index 11e0bbb..94cc34c 100755 --- a/surfaces/android/guest/git-clone-shim.sh +++ b/surfaces/android/guest/git-clone-shim.sh @@ -10,9 +10,16 @@ # # dulwich (pure-Python git) performs the whole clone in ONE process using plain file I/O — # the path that works here. Route `clone` through it; everything else uses the real git. +# +# Only the FIRST non-option argument decides the subcommand, so `git commit -m clone` or +# `git checkout clone` pass through untouched. Global option forms that take a value +# (`git -C clone`, `git -c k=v clone`) also pass through to real git: the value is +# indistinguishable from a subcommand here, and passthrough is the pre-existing behavior. for a in "$@"; do case "$a" in + -*) continue ;; clone) exec python3 /usr/local/bin/agentnet-git-clone.py "$@" ;; + *) break ;; esac done exec /usr/bin/git "$@" diff --git a/surfaces/android/scripts/build-assets.sh b/surfaces/android/scripts/build-assets.sh index d677ea4..858abc3 100755 --- a/surfaces/android/scripts/build-assets.sh +++ b/surfaces/android/scripts/build-assets.sh @@ -21,7 +21,6 @@ set -euo pipefail ABI="${ABI:-arm64}" # arm64 | x86_64 -AGENTNET_ROOTFS_BASE_IMAGE="${AGENTNET_ROOTFS_BASE_IMAGE:-unknown}" case "$ABI" in arm64) PROOT_ARCH="aarch64" ;; x86_64) PROOT_ARCH="x86_64" ;; @@ -36,7 +35,6 @@ WORK="${WORK:-$ANDROID_DIR/.assets-build}" mkdir -p "$ASSETS" "$WORK" echo "==> ABI=$ABI proot=$PROOT_ARCH" -echo "==> rootfs base image: $AGENTNET_ROOTFS_BASE_IMAGE" echo "==> assets -> $ASSETS" echo "==> work -> $WORK" @@ -178,18 +176,13 @@ cp "$ANDROID_DIR/guest/agentnet-git-clone.py" /usr/local/bin/agentnet-git-clone. cp "$ANDROID_DIR/guest/git-clone-shim.sh" /usr/local/bin/git chmod +x /usr/local/bin/agentnet-git-clone.py /usr/local/bin/git -# Keep rseq disabled for login shells when the guest libc supports this tunable. issue -# #112 is fixed by shipping the 22.04 rootfs again, but this remains a low-cost guard if -# a future asset build returns to a newer glibc under proot. The app covers node + -# children via guest env; this profile.d covers adb/manual proot entry too. +# Keep rseq disabled for login shells. Not the fix for #112 (that's the clone shim above — +# the app-domain transport corruption survives rseq-off), but rseq-under-ptrace is a real, +# separately-measured corruption vector, so this stays as a low-cost guard. The app covers +# node + children via guest env; this profile.d covers adb/manual proot entry too. cat > /etc/profile.d/00-agentnet-rseq.sh <<'RSEQ' export GLIBC_TUNABLES=glibc.pthread.rseq=0 RSEQ -cat > "$ASSETS/agentnet-rootfs.env" <