Skip to content

[BE-0283] feat(android): add network-capture assertions to the adb backend#1180

Merged
0x0c merged 15 commits into
mainfrom
claude/be-0283-implementation-11c2e8
Jul 21, 2026
Merged

[BE-0283] feat(android): add network-capture assertions to the adb backend#1180
0x0c merged 15 commits into
mainfrom
claude/be-0283-implementation-11c2e8

Conversation

@0x0c

@0x0c 0x0c commented Jul 17, 2026

Copy link
Copy Markdown
Member

Summary

Ports request / requestSequence network-capture assertions to the Android (adb) backend, closing a
gap since BE-0007: the runner already pre-starts a NetworkCollector per Android device and injects
BAJUTSU_COLLECTOR into the launch env, but nothing on Android ever read it, so a request assertion
silently degraded to "zero exchanges observed" instead of failing loudly. This merges BE-0283 to
Implemented.

BajutsuAndroid gains an OkHttp interceptor (BajutsuNet.kt) that reports each exchange to the same
in-process collector iOS uses, and the runner bridges the emulator's isolated loopback to that collector
with adb reverse — the reverse-direction twin of the resident UI Automator server's forward_cmd
(BE-0245). The scenario schema, assertion pipeline, and capability model are all unchanged: network
stays a deliberately ungated construct the app-side collector satisfies without a backend advertising it,
exactly the accommodation idb already relies on.

What changed

  • BajutsuAndroid/src/main/java/dev/bajutsu/android/BajutsuNet.kt (new) — the OkHttp interceptor: a
    no-op unless BAJUTSU_COLLECTOR is configured, POSTs each completed exchange as JSON matching
    NetworkExchange field-for-field over a separate reporting client. A report failure (a body-peek or
    JSON-build throw) is caught and logged rather than allowed to fail the app's real request.
  • bajutsu/adb.pyreverse_cmd / reverse_remove_cmd, siblings of forward_cmd /
    forward_remove_cmd, tunneling the same device and host port so the injected collector URL resolves
    on-device unchanged.
  • bajutsu/platform_lifecycle/protocols.py + environments/{android,ios,web}.py — a new
    RunEnvironment.bridge_collector(port) seam (a third "no-op implementation" decline idiom, documented
    alongside the existing two): AndroidEnvironment runs adb reverse and returns a real teardown thunk;
    iOS/web return a no-op, since the Simulator shares the host loopback and web observes via the driver.
  • bajutsu/runner/pool.py — wires bridge_collector into the lease right before launch (alongside
    the existing BAJUTSU_COLLECTOR env injection) and tears it down on both release() and a launch
    failure, so one failed lease never leaks a tunnel.
  • bajutsu/drivers/adb.py — a comment recording why AdbDriver.CAPABILITIES must not advertise
    network (that token means native driver observation), locked by a preflight test.
  • Showcase reconciliationcompose/views Net.kt migrate from HttpURLConnection to OkHttp
    with BajutsuNet.interceptor() installed (debug-gated configure() call in MainActivity, matching
    the clipboard receiver's precedent); a new network_android.yaml scenario and a network (adb) job in
    android-e2e.yml prove a request assertion observes real emulator traffic end to end, via a
    throwaway host stub (net_stub.py) the emulator reaches at its 10.0.2.2 host-loopback alias.
  • Docsdocs/network.md (+ ja), demos/showcase/SPEC.md (+ ja), and docs/architecture.md
    (+ ja) updated to describe Android's network capture; BajutsuAndroid/README.md (+ ja) gains a
    network-capture section.

Scope

Capture is bounded to OkHttp-originated HTTP(S) traffic — the same bounded shape as iOS's
URLSession-only scope. HttpURLConnection, other HTTP clients, and WebView traffic are out of scope
(a WebView needs its own follow-up, as it did on iOS, BE-0037). Deterministic mocks (stub
responses) are deliberately deferred to a follow-up item — this PR ships observation only, matching the
proposal's "Alternatives considered."

Prime-directive compliance

  • AI never judges — no LLM anywhere on this path; the interceptor, collector, and request
    assertion are all deterministic.
  • Determinism first — no fixed sleep; the on-device scenario waits on the mirrored *.status
    value and the collector via condition waits.
  • App-agnostic — the interceptor is a library every app embeds identically; no per-app config.

Verification

make check
  ruff format/lint: All checks passed!
  mypy: Success: no issues found in 310 source files
  4369 passed, 3 skipped, coverage 92.39% (floor 89%)

New fast-gate coverage: reverse_cmd/reverse_remove_cmd command shapes, AndroidEnvironment.bridge_collector
establishing/tearing down the tunnel (including a teardown-failure-is-swallowed-and-logged case), the
pool wiring the bridge before launch and releasing it on both success and launch failure, the interceptor's
JSON wire-shape parsing into NetworkExchange, and that AdbDriver.CAPABILITIES excludes network while
a request scenario still preflight-cleans.

Not covered by make check (no JVM in the fast gate): the real Kotlin interceptor. It's proven by
the new network (adb) job in android-e2e.yml, an on-device CI run against a real emulator — not yet
executed in this session since it requires a booted AVD.

Reviewed with the built-in simplify/code-review discipline plus the pr-review-toolkit
silent-failure-hunter, type-design-analyzer, and pr-test-analyzer agents; their findings (an
unguarded report() call that could fail the app's real request, a swallowed-without-logging adb
failure, the Protocol docstring's declining-idiom list missing the new method, and an ungated configure()
call in the showcase) are all fixed in this diff.

🤖 Generated with Claude Code

@0x0c
0x0c force-pushed the claude/be-0283-implementation-11c2e8 branch from be0fdab to 7f81921 Compare July 17, 2026 15:06
@0x0c
0x0c temporarily deployed to claude-review July 17, 2026 15:06 — with GitHub Actions Inactive
@github-actions github-actions Bot added topic:dev-infra CI / build infrastructure (roadmap topic) topic:dogfood Dogfood fixtures (demo apps) (roadmap topic) topic:platform Platform expansion (Android / Web / Flutter) (roadmap topic) labels Jul 17, 2026
Comment thread bajutsu/platform_lifecycle/protocols.py
Comment thread BajutsuAndroid/src/main/java/dev/bajutsu/android/BajutsuNet.kt Outdated
Comment thread demos/showcase/android/Makefile Outdated
Comment thread BajutsuAndroid/src/main/java/dev/bajutsu/android/BajutsuNet.kt
0x0c added a commit that referenced this pull request Jul 17, 2026
targetSdk 35 blocks plaintext HTTP by default, but the network e2e
(network_android.yaml) needs cleartext to two loopback aliases with no
certificate to serve: 10.0.2.2 (SHOWCASE_API_URL, the emulator's
host-loopback alias to net_stub.py) and 127.0.0.1 (BAJUTSU_COLLECTOR, the
on-device end of the adb-reverse tunnel BajutsuNet reports exchanges over).
Without this, Net.get()'s OkHttp call fails cleartext validation
immediately, status never reaches "done", and the scenario's wait times
out — the network (adb) CI failure on PR #1180.

A scoped network security config permits cleartext for just those two
aliases, leaving every other host on the HTTPS-only platform default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
0x0c added a commit that referenced this pull request Jul 17, 2026
Four non-blocking Claude Code review comments on PR #1180:
- BajutsuNet.kt: log a transport failure on the fire-and-forget collector POST instead of swallowing
  it silently, so a "request not observed" assertion failure has a trace to point at.
- BajutsuNet.kt: bound the request body copy by contentLength, matching the response body's
  BODY_PEEK_LIMIT, so a large/unknown-length body is skipped rather than buffered whole.
- protocols.py: add the new bridge_collector method to the RunEnvironment surface enumeration.
- Makefile: pin E2E_NET_STUB_PORT (was override-safe-looking `?=`) since it must match the hardcoded
  SHOWCASE_API_URL in network_android.yaml.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@0x0c
0x0c temporarily deployed to claude-review July 17, 2026 15:34 — with GitHub Actions Inactive
Comment thread demos/showcase/android/Makefile
0x0c added a commit that referenced this pull request Jul 17, 2026
Poll the stub's port with a bounded curl retry loop (matching the
demos/web/Makefile e2e/record targets) instead of assuming the app
launch + wait: stable.refresh step always outlasts the ThreadingHTTPServer
bind. A fixed sleep would violate determinism-first; this is a condition
wait with a clear failure message if the stub never comes up.

Addresses PR #1180 review comment 3604402880.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@0x0c
0x0c temporarily deployed to claude-review July 17, 2026 15:50 — with GitHub Actions Inactive
Comment thread BajutsuAndroid/README.md
Comment thread BajutsuAndroid/README.ja.md
Comment thread docs/network.md Outdated
Comment thread docs/ja/network.md Outdated
@0x0c
0x0c marked this pull request as ready for review July 18, 2026 04:16
0x0c added a commit that referenced this pull request Jul 18, 2026
targetSdk 35 blocks plaintext HTTP by default, but the network e2e
(network_android.yaml) needs cleartext to two loopback aliases with no
certificate to serve: 10.0.2.2 (SHOWCASE_API_URL, the emulator's
host-loopback alias to net_stub.py) and 127.0.0.1 (BAJUTSU_COLLECTOR, the
on-device end of the adb-reverse tunnel BajutsuNet reports exchanges over).
Without this, Net.get()'s OkHttp call fails cleartext validation
immediately, status never reaches "done", and the scenario's wait times
out — the network (adb) CI failure on PR #1180.

A scoped network security config permits cleartext for just those two
aliases, leaving every other host on the HTTPS-only platform default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
0x0c added a commit that referenced this pull request Jul 18, 2026
Four non-blocking Claude Code review comments on PR #1180:
- BajutsuNet.kt: log a transport failure on the fire-and-forget collector POST instead of swallowing
  it silently, so a "request not observed" assertion failure has a trace to point at.
- BajutsuNet.kt: bound the request body copy by contentLength, matching the response body's
  BODY_PEEK_LIMIT, so a large/unknown-length body is skipped rather than buffered whole.
- protocols.py: add the new bridge_collector method to the RunEnvironment surface enumeration.
- Makefile: pin E2E_NET_STUB_PORT (was override-safe-looking `?=`) since it must match the hardcoded
  SHOWCASE_API_URL in network_android.yaml.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
0x0c added a commit that referenced this pull request Jul 18, 2026
Poll the stub's port with a bounded curl retry loop (matching the
demos/web/Makefile e2e/record targets) instead of assuming the app
launch + wait: stable.refresh step always outlasts the ThreadingHTTPServer
bind. A fixed sleep would violate determinism-first; this is a condition
wait with a clear failure message if the stub never comes up.

Addresses PR #1180 review comment 3604402880.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@0x0c
0x0c force-pushed the claude/be-0283-implementation-11c2e8 branch from b0f9af9 to 0723099 Compare July 18, 2026 04:24
@0x0c
0x0c temporarily deployed to claude-review July 18, 2026 04:27 — with GitHub Actions Inactive
Comment thread .github/workflows/android-e2e.yml
Comment thread BajutsuAndroid/src/main/java/dev/bajutsu/android/BajutsuNet.kt
Comment thread bajutsu/platform_lifecycle/environments/android.py Outdated
Comment thread docs/architecture.md
Comment thread docs/ja/architecture.md
t added 2 commits July 21, 2026 08:08
…entation-11c2e8

# Conflicts:
#	tests/runner/test_pool.py
The roadmap-index merge driver silently dropped the "Device-cloud
execution" and "Authoring experience" sections when merging origin/main
into this branch, leaving the GENERATED marker pairs absent and
make roadmap-index unable to regenerate into them. Restore the
scaffolding from origin/main and regenerate, which also moves BE-0283
out of Proposals now that it is Implemented.
@0x0c
0x0c temporarily deployed to claude-review July 20, 2026 23:13 — with GitHub Actions Inactive
Comment thread demos/showcase/scenarios/network_android.yaml
Rename the scenario tag from `net` to `network` so it matches
demos/web/scenarios/network.yaml, keeping `--tags network` filtering
consistent across backends.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WRkGhvrpKGzPk5wu4SsE2
@0x0c
0x0c temporarily deployed to claude-review July 20, 2026 23:23 — with GitHub Actions Inactive
Comment thread .github/workflows/android-e2e.yml
Comment thread bajutsu/platform_lifecycle/environments/android.py
- android-e2e.yml: add the `network` job to the `E2E (android)` required
  aggregator's needs/results. Its pass/fail is a deterministic,
  host-independent fact (was the exchange observed?), the same shape as
  smoke/conformance, so a regression must fail the required check; it is
  path-gated identically, so a skip still passes. golden/visual stay
  excluded (host-specific baselines).
- android.py: update AndroidEnvironment's class docstring, stale since
  BE-0283 — Android now has app-side network capture (the app reports to
  the host collector bridged via `adb reverse`), while the adb driver
  still declares no native NETWORK capability.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WRkGhvrpKGzPk5wu4SsE2
@0x0c
0x0c temporarily deployed to claude-review July 20, 2026 23:35 — with GitHub Actions Inactive
Comment thread BajutsuAndroid/README.md
… need

BAJUTSU_COLLECTOR is a plain-HTTP 127.0.0.1 URL, and Android (API 28+)
blocks cleartext by default (iOS exempts loopback via ATS, so BajutsuKit
needs no equivalent note). Without a network_security_config cleartext
exception for 127.0.0.1, BajutsuNet's report POST fails with
"CLEARTEXT communication to 127.0.0.1 not permitted" — logged but
otherwise silent, so no exchange reaches the collector. The showcase
already carries this exception; document the same requirement for
third-party integrators in BajutsuAndroid/README{,.ja}.md and
docs/network.md (+ ja) "App contract".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WRkGhvrpKGzPk5wu4SsE2
@0x0c
0x0c temporarily deployed to claude-review July 20, 2026 23:51 — with GitHub Actions Inactive
Comment thread BajutsuAndroid/README.md Outdated
Comment thread BajutsuAndroid/README.ja.md Outdated
House doc style asks for an acronym's full name in parentheses on first
use. Expand ATS -> App Transport Security (ATS) in the cleartext note
across BajutsuAndroid/README{,.ja}.md and docs/network.md (+ ja).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WRkGhvrpKGzPk5wu4SsE2
@0x0c
0x0c temporarily deployed to claude-review July 21, 2026 00:01 — with GitHub Actions Inactive
Comment thread bajutsu/platform_lifecycle/environments/android.py
Comment thread docs/network.md Outdated
Comment thread docs/ja/network.md Outdated
…ocs wording

- test_adb_lifecycle.py: add two tests mirroring start()'s failure tests —
  a failing `adb reverse` at setup surfaces a clean DeviceError
  (CalledProcessError path), and a missing adb binary does too (OSError
  path). The bridge_collector error-translation branches were untested.
- docs/network.md (+ ja): the cleartext-exception note said the report POST
  "fails silently"; it is actually logged (Log.w), just invisible to the
  assertion/network.json trail — align the wording with the READMEs so a
  reader debugging a missing exchange knows to check logcat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WRkGhvrpKGzPk5wu4SsE2
@0x0c
0x0c temporarily deployed to claude-review July 21, 2026 00:13 — with GitHub Actions Inactive
Comment thread BajutsuAndroid/src/main/java/dev/bajutsu/android/BajutsuNet.kt
Comment thread BajutsuAndroid/src/main/java/dev/bajutsu/android/BajutsuNet.kt
…omically

- BajutsuNet.post: onResponse now logs a non-2xx collector reply (a 401 on
  a missing/mismatched token, a 5xx) instead of dropping it, matching the
  onFailure transport-failure log — so a rejected report leaves a trace
  rather than surfacing only as "exchange not observed".
- Bundle the collector URL and token into one @volatile CollectorConfig set
  with a single assignment. Two independent @volatile fields could be read
  mid-write by a request racing configure(), sending a report with no
  Authorization header that the collector then 401s silently; one snapshot
  closes that window. report/reportFailure/post all read the one config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WRkGhvrpKGzPk5wu4SsE2
@0x0c
0x0c temporarily deployed to claude-review July 21, 2026 00:25 — with GitHub Actions Inactive
@0x0c
0x0c merged commit 13b8085 into main Jul 21, 2026
57 of 64 checks passed
@0x0c
0x0c deleted the claude/be-0283-implementation-11c2e8 branch July 21, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

topic:dev-infra CI / build infrastructure (roadmap topic) topic:dogfood Dogfood fixtures (demo apps) (roadmap topic) topic:platform Platform expansion (Android / Web / Flutter) (roadmap topic)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants