Lifts the build off the JDK-21 ceiling — the old AGP 8.7.3 pin could not run
on JDK 25. Two AGP-9 migration edits were required:
- AGP 9 ships built-in Kotlin support, so applying org.jetbrains.kotlin.android
alongside it fails with "extension with name kotlin already registered";
the alias is dropped (kotlin.compose / kotlin.serialization stay, they are
separate compiler plugins).
- kotlinOptions { jvmTarget } came from that plugin and no longer resolves;
AGP derives jvmTarget from compileOptions instead.
compileSdk/targetSdk 35 -> 36, since Android Studio ships API 36.1 by default
and installing 35 was avoidable churn. versionCode 5 so the reports from this
toolchain are attributable (build 4 was already archived from the old one).
Also fixes a missing comma that left .claude/settings.json invalid JSON.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports table, host-IP/second-IP/canary-zone requirements, and the two
protocol properties (SPKI pinning, observed-source fidelity) that a
reverse proxy would break. Coexists with traefik by never touching its
ports.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Release tags are server-v1.2.3 but binaries are stamped v1.2.3 — the raw
comparison would re-download the current version on every check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docker was present on the echolot runner; the login failed because
Gitea's container registry does not accept the ephemeral Actions token.
Requires a PAT with package read/write scope as the REGISTRY_TOKEN repo
secret (REGISTRY_USER optional, defaults to the actor); fails with a
self-explanatory error when missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
compilesau-echolot (repo runner, label echolot) handles the docker image
build. Registry host defaults to git.rambossek.at (runner-internal
GITHUB_SERVER_URL is http://app:3000). Asset re-uploads on tag re-runs
warn instead of failing the completed release.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The job log showed uploads 404ing: sed's greedy .* matched the LAST "id"
in the release JSON (a nested user id), not the release's. First-match
grep now. Release creation falls back to GET-by-tag on re-runs, and the
image job strips any scheme from the internal server URL (http://app:3000)
with an ECHOLOT_REGISTRY_HOST override for the public registry host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
First tag run failed as one monolithic job. Binaries + Gitea release now
need only Go and curl (jq dropped too) and succeed on any runner; the
docker image build is a separate job that can fail without killing the
release self-update depends on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wireless-adb findings, report-collection workflow, build-number
convention, per-device Shizuku matrix, and the JDK constraint now live in
the repo so a session on any machine starts with them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A push trigger that declares tags: stops matching branch pushes, and the
paths filter doesn't apply to tag pushes — so the combined workflow could
effectively never run. test-server.yml now covers branch pushes touching
server/; build-server.yml is tags-only (server-v*).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
exec_path confirms the Lenovo/A15 ran the whole battery via the legacy
Shizuku.newProcess path: 6/7 commands with real data, incl. Lenovo's
IpClient dump format (distinct from OnePlus's — the per-vendor parser
sample the prober exists to collect). Design consequence recorded:
core-shizuku needs a dual-path executor. ip_monitor times out under
newProcess; bind-wait shortening noted as future tuning.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Build 3 settled it: the Lenovo TB330FU/A15 never spawns the UserService
(two 25s bind attempts, binder alive, permission granted). Build 4 falls
back to the legacy Shizuku.newProcess remote-process API via reflection
and records exec_path in the evidence — whether that path works per
device is itself the capability question core-shizuku needs answered.
Also archives both build-3 reports: phone 7/7 incl. provoked NEIGH
transitions in ip_monitor; traceroute.udp4 handled a silent hop ("*")
correctly on both devices.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Build-2 matrix (OnePlus 15/A16 + Lenovo TB330FU/A15) delivered the big
answer: traceroute.udp4 reads real hops via pure-Kotlin MSG_ERRQUEUE on
BOTH devices — the C-over-JNI shim is dead for Android 15+. Per-network
ICMP works as designed (phone ping6: cellular-only with topology
evidence). mDNS meta-query confirmed broken on both devices while
concrete types find real services — production must enumerate types.
Network finding: hudeWLAN RAs advertise a v6 default route but no global
prefix — the LAN's IPv6 is half-broken (nice dogfood case).
Tablet Shizuku bound 0/7 (binder alive, permission granted): first-spawn
dex extraction on slow storage suspected — bind window now 25s + one
retry. versionCode 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pure stdlib. Implements the spec's core: enrollment (single-use tokens),
profile (SPKI pin, only real capabilities advertised), sessions with the
§2.4 HKDF-SHA256 key schedule; UDP data plane with the 32-byte ELT1
header, 4-byte HMAC gate, 1024-wide anti-replay window, ECHO_RESP with
observation block, TIMESYNC, and the §3.4 anti-amplification cap. Wire
format has tests (roundtrip + silent-drop cases); enroll→profile→session
smoke-tested live.
Modes: container (autodetect /.dockerenv|/run/.containerenv|cgroup, or
--docker/ECHOLOT_DOCKER=1; config via ECHOLOT_* env; distroless image;
network_mode host required — Docker NAT would falsify observed sources)
and native (--install-systemd/--uninstall-systemd with a hardened unit,
opt-in --self-update from Gitea releases; refused in containers).
CI: tests on any server/ push; server-v* tags build+push the image to the
Gitea registry and attach linux amd64/arm64 binaries + SHA256SUMS to a
release — the artifact self-update consumes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ascending integer bumped per deployed change; shown in the header and as
proberBuild in the JSON report so a report is attributable to an exact
build. buildConfig feature enabled for BuildConfig.VERSION_CODE.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- IcmpProbe: attempt the echo on the default network AND each active
network (Network.bindSocket + per-network DNS). Run 1's ping6 EAGAIN was
topology (v6 only on cellular), not capability — now the report shows
which networks carry which family instead of a bare ERROR.
- New traceroute.udp4 (TracerouteProbe): actual UDP traceroute reading
ICMP time-exceeded via Os.recvmsg(MSG_ERRQUEUE) with cmsg parsing —
reflection per repo convention (API surface exists ~34+). If this
returns hops, the C-over-JNI shim is unnecessary on that device.
sock_extended_err layout constants documented in OsAbi.
- MulticastProbe: 10 s window; meta-query PLUS concrete types
(_http._tcp, _googlecast._tcp) — run 1 showed the meta-query alone
returning 0 on a network with live services; capture names + failure
codes as evidence.
- ShizukuProbe: ip monitor window 2s -> 5s, gateway ping in background to
provoke a NEIGH transition instead of hoping for ambient churn.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
multinetwork now SUPPORTED (wifi+cellular bound concurrently; the cellular
request surfaced a second dual-stack PDN), Shizuku battery 7/7 in 2.9s
(was 4/7 in 34s), rtt_ms locale-clean. Remaining probe improvements
(per-network ping6, longer mDNS/ip-monitor windows) noted in build-status.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- AndroidManifest: add CHANGE_NETWORK_STATE — requestNetwork threw
SecurityException, multinetwork.request_and_bind could never run.
- IcmpProbe: format rtt_ms with Locale.ROOT — Austrian locale produced
"38,1" in the JSON report.
- ShizukuRunner/ShizukuProbe: bind the UserService once per battery
(execBatch) instead of per command; the per-command bind/unbind raced
Shizuku and 3/7 commands died on SHIZUKU_BIND_TIMEOUT.
Archive the report at echolot-prober/reports/, record findings in
build-status.md. Notable: errqueue path fully reachable on Android 16 —
the native shim may be unnecessary on modern devices.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found on the OnePlus 15 (CPH2747): rotating after a run emptied the result
list — results lived in a MainActivity field and the run in lifecycleScope,
so a config change dropped collected results and cancelled remaining probes.
Both now live in ProberViewModel (viewModelScope + application context; no
probe needs an Activity). A lost run means a lost export, so this matters
for a collection tool.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Code: GPL-3.0-or-later (LICENSE, SPDX headers on all .kt/.aidl).
Specs in docs/: CC-BY-4.0 (docs/LICENSE). Rationale in build-status.md;
server decided GPL (not AGPL).
- Root README for the public repo.
- web/: Cloudflare Worker site for echo-lot.app. /apk resolves the newest
APK from the Gitea latest-release API at request time (edge-cached 5 min),
so tagging a release is the only publish step. /fdroid, /source, and a
manual DOWNLOAD_URL fallback are wrangler vars.
- .gitea/workflows/release.yml: tag-driven (v*) signed semver APK builds for
the future production app in echolot-app/. Parked; the prober is
deliberately not CI-built.
- Fix UserService.kt: drop the explicit secondary constructor that
conflicted with the implicit primary (never compiled before — first
local build caught it). Prober now builds: :app:assembleDebug OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Monorepo root for Echolot. Contains the no-root capability prober
(Kotlin/Compose, app.echo_lot.prober) and the four design docs that act
as the contract for the production app and the Go server.
LICENSE is deliberately absent — still undecided, see docs/build-status.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>