← AutonomCONTENT HISTORY

Update to Autonom

Snapshot Oct 24, 2026 · 13:54 UTC · version 0.30.0

Collection source: downloaded plugin package.

WHAT CHANGED

First saved snapshot

No earlier snapshot is available to establish a change.

Save Autonom and check future changes in Following.

Up to 10 plugins free · no card required.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "Inspect and mock a mobile app's HTTP(S) traffic for AI agents — start a localhost MITM proxy, attach a device with explicit consent, filter recorded requests, force responses, and export HAR without leaking credentials.",
  "included_files": [
    {
      "relative_path": "agents/openai.yaml",
      "size_in_bytes": 368
    }
  ],
  "name": "mobile-network",
  "skill_md_contents": "---\nname: mobile-network\ndescription: Inspect and mock a mobile app's HTTP(S) traffic for AI agents — start a localhost MITM proxy, attach a device with explicit consent, filter recorded requests, force responses, and export HAR without leaking credentials.\n---\n\n# Mobile Network (inspect and mock)\n\n## Purpose\n\nAnswer **what did the app actually send and receive**, and **what does it do when\nthe server misbehaves**. Backed by mitmproxy, bound to localhost, scoped to one\nAutonom session.\n\nUse it when the task mentions an API, a request, a response, a status code, a\nmock, a HAR, a proxy, or \"why is this call failing\".\n\n## Before you start: this is privileged\n\nStarting the proxy decrypts and records traffic. Attaching a device changes that\ndevice's network configuration, and `--install-ca` writes to its trust store.\nNone of that happens without **explicit consent for that exact action**:\n\n- every privileged verb requires its flag (`--i-understand-mitm`, plus\n  `--install-ca` for a trust-store write);\n- on an interactive terminal you must additionally type a confirmation phrase;\n- consent is **never** cached — a grant earlier in the session does not carry to\n  the next command;\n- there is no environment variable or config file that grants it.\n\nOnly use this against devices and apps you own or are authorized to test.\n\n## Workflow\n\n```bash\n# 1. Own a session first\npython3 <autonom-root>/scripts/autonom.py session start --serial emulator-5554 --app-id com.example.app\n\n# 2. Start the proxy (127.0.0.1 only; port is auto-chosen when omitted)\npython3 <autonom-root>/scripts/autonom.py network start --i-understand-mitm\n\n# 3. Point the device at it\npython3 <autonom-root>/scripts/autonom.py network attach --i-understand-mitm\n\n# 4. Exercise the app, then look\npython3 <autonom-root>/scripts/autonom.py network requests list --path '*/login'\npython3 <autonom-root>/scripts/autonom.py network requests list --host api.example.com --status 401\npython3 <autonom-root>/scripts/autonom.py network requests show f_0003\n\n# 5. Force a failure and check the UI reacts\npython3 <autonom-root>/scripts/autonom.py network mock add \\\n  --match '*/v1/login' --method POST --status 500 \\\n  --body-file fixtures/login_error.json --header 'Content-Type: application/json'\npython3 <autonom-root>/scripts/autonom.py ui tap --text \"Log In\"\npython3 <autonom-root>/scripts/autonom.py ui find --text \"Something went wrong\"\n\n# 6. Keep the evidence, then clean up\npython3 <autonom-root>/scripts/autonom.py network export --har network/session.har\npython3 <autonom-root>/scripts/autonom.py network mock disable --all\npython3 <autonom-root>/scripts/autonom.py network detach\npython3 <autonom-root>/scripts/autonom.py network stop\n```\n\nStep 6 uses `disable --all`, not `clear`: the registry is persistent, so `clear`\nthrows away rules you may want tomorrow. Use `clear` only to actually delete them.\n\n`session stop` performs detach and stop best-effort anyway, in that order, so the\ndevice is never left pointing at a dead proxy.\n\n## Transparent capture on Android (no app change)\n\nThe workflow above sets the device's **global HTTP proxy** and trusts the CA in\nthe **user** store. That is enough for a native app with a debug\n`network_security_config`, but it captures **nothing** from a Flutter app:\n`dart:io` ignores the Android global proxy, and apps targeting API 24+ do not\ntrust user CAs. The fix routes the whole emulator through the proxy at launch and\nputs the CA in the **system** store — with **zero changes to the app**:\n\n`-http-proxy` is a **launch-time** flag, so pick the proxy port up front and pass\nthe *same* port to the boot and to `network start`. The emulator tolerates the\nproxy being down at boot and connects once `network start` brings it up on that\nfixed port. This is the exact order verified live on a rooted API 37 emulator:\n\n```bash\n# 1. Boot a dedicated, rootable AVD ROUTED through a chosen port (here 18091).\n#    `-http-proxy` cannot be injected into an already-running emulator.\npython3 <autonom-root>/scripts/autonom.py devices boot --avd Autonom_Mitm_API37 --http-proxy 127.0.0.1:18091\n# -> note the serial it reports (e.g. emulator-5554); use it as <serial> below\n\n# 2. Own a session on that emulator\npython3 <autonom-root>/scripts/autonom.py session start --serial <serial> --app-id com.example.app\n\n# 3. Start the proxy on the SAME port the emulator was routed to\npython3 <autonom-root>/scripts/autonom.py network start --port 18091 --i-understand-mitm\n\n# 4. Install the MITM CA into the SYSTEM trust store (reversible; needs root)\npython3 <autonom-root>/scripts/autonom.py network attach --system-ca --i-understand-mitm --serial <serial>\n\n# 5. Launch/drive the app, then look — Flutter/Dio requests now decrypt\npython3 <autonom-root>/scripts/autonom.py session launch com.example.app\npython3 <autonom-root>/scripts/autonom.py network requests list\n```\n\n`network start` is session-bound, which is why the session is owned first (step\n2) and the port is fixed by hand rather than auto-chosen. `network attach\n--system-ca` verifies the install from a zygote mount namespace and reports\n`system_ca.verified`; a failed verify is a `system_ca_unverified` warning, not a\nhard failure (HTTP is still captured, and the change is reversible).\n\n**Hard constraint — the image must be rootable.** Use a `google_apis` system\nimage (userdebug: `adb root` succeeds or adbd already runs as uid 0), **not** a\n`google_apis_playstore` image and not a production build — those block root, and\n`network attach --system-ca` then refuses with `unsupported_capability`\n(`capability: network.system_ca`). Use a **dedicated test AVD**; the change is a\ntmpfs plus a bind into the zygote mount namespaces, and a **reboot clears all of\nit** — never the host, never a real device. **Verified live on API >= 34** (the\nAPEX conscrypt store, `apex_conscrypt`); the API < 34 path uses the\n`/system/etc/security/cacerts` remount instead and is **not yet verified on a\ndevice**.\n\nIf the emulator was not booted routed to this session's proxy, `network attach\n--system-ca` refuses with `unsupported_capability`\n(`capability: network.transparent_capture`) and tells you to reboot with\n`devices boot --avd X --http-proxy <session proxy>` — you cannot inject\n`-http-proxy` into a running emulator. It also refuses (`reason:\napp_proxy_attached`) while an app-proxy `network attach` is active: run `network\ndetach` first, so the device's previous global proxy is restored.\n\nIn transparent mode every flow on the proxy is **counted as the device's**, so\n`network status` answers `attached: true` with `evidence: \"transparent_proxy\"`,\n`target_flow_count == recent_flow_count`, and `unattributed_flow_count: 0`. The\nflows all arrive from `127.0.0.1` by design, and any host process can reach the\nproxy on loopback too — a `curl` through its port would be counted as well. That\nis a reasonable default on a **dedicated test host**, where nothing else uses\nthe port; keep it that way while you gather evidence. The count holds only while\nthe process registry still shows this emulator booted routed through the proxy:\nafter `devices shutdown`, or a reboot without `--http-proxy`, `network status`\nanswers `attached: false` with `evidence: \"transparent_route_gone\"` and a\n`reason` (unless a guest-network `10.0.2.x` flow still proves attachment),\nand loopback flows are unattributed again. `network requests` lists\n**all** captured flows. `network status` also reports `capture_mode` and the\ninstalled `system_ca` (`verified`, `checked_via`). A fresh `adb shell ls\n/apex/com.android.conscrypt/cacerts` will **not** show the cert (a different\nmount namespace); Autonom verifies from a zygote namespace with `nsenter`.\n\nThe emulator's `-http-proxy` opens every tunnel as `CONNECT <ip>:443`, so the\nconnection target is an IP. Each flow's `host` is still the **name** the app\nasked for (the Host header), and the IP is kept in `server_ip`. So `network\nrequests list --host backend.example.com` and `network mock add --host\nbackend.example.com` work as usual in transparent mode.\n\n## Attaching, per platform\n\n| Platform | How | Coverage |\n| --- | --- | --- |\n| Android emulator (transparent) | `--system-ca` on an emulator booted with `devices boot --http-proxy`: routes at launch + system-store CA | full, incl. Flutter/`dart:io` (see the section above); needs a rootable `google_apis` image |\n| Android emulator (app-proxy) | sets the device's global HTTP proxy to `10.0.2.2:<port>` + user-store CA (`--install-ca`) | native apps with a debug `network_security_config`; **not** Flutter/`dart:io`, **not** pinned traffic |\n| Android physical | **refused** — the proxy is loopback-only and a physical device cannot reach it; widening the bind would expose an open proxy | none |\n| iOS Simulator | injects proxy environment variables into apps launched by `session launch` | clients honouring proxy env vars |\n\n**iOS limitation, state it in findings:** the per-process mechanism covers\nclients that read the proxy environment — curl and many SDKs. Native\n`URLSession` reads the *system* proxy configuration and is **not** captured this\nway. The Simulator has no proxy pane of its own — it uses the host Mac's\nnetwork stack — so the manual steps `network attach` prints set the **host's**\nsystem proxy (and switch it off again afterwards). Ask the operator before\ndoing that, then confirm with `network status`. On iOS `network attach`\nanswers `attached: \"unknown\"` with `mode: manual` and `attach_state: manual`:\nnothing has been observed yet, and the attach is not automatable the way\nthe emulator's is.\n\n**Flutter needs an in-app hook, on both platforms.** `dart:io`'s `HttpClient`\nignores the proxy environment *and* Android's global proxy setting unless the\napp itself sets `findProxy`. Add a hook to the debug build — for example\n`HttpOverrides.global` with a `createHttpClient` that sets\n`client.findProxy = HttpClient.findProxyFromEnvironment` (iOS), or returns\n`'PROXY 10.0.2.2:<port>'` (Android emulator) — then relaunch. iOS\n`network attach` says so in a `flutter_proxy_hook_required` warning. Without\nthe hook a Flutter app's requests never reach the proxy, whatever\n`network status` says about the device.\n\nAutonom never changes macOS network-service settings itself: that is a\nsystem-wide change whose blast radius is the operator's whole machine.\n\n## HTTPS and certificates\n\nDecrypting TLS needs the app to trust the MITM CA.\n\n- **Android, no app change at all:** transparent capture (see \"Transparent\n  capture on Android\" above) — `network attach --system-ca` on a rooted emulator\n  booted with `devices boot --http-proxy`. This is the only path that captures a\n  release-config Flutter app without rebuilding it.\n- **Preferred when you can rebuild:** point a **debug build** at a\n  `network_security_config` (Android) that trusts user CAs, or use a debug trust\n  configuration on iOS.\n- **iOS Simulator:** `--install-ca` runs `simctl keychain add-root-cert`, scoped to\n  that one simulator.\n- **Android emulator:** `--install-ca` (with `--i-understand-mitm`) runs\n  `adb root` and copies the CA into the user trust store. It works only on a\n  rootable `google_apis` image: a `google_apis_playstore` image blocks\n  `adb root`, and the attach fails with `backend_failed` saying so. A\n  physical device is refused. The same steps by hand (`/system` needs no\n  remount):\n\n  ```bash\n  CA=~/.local/state/autonom/ca/mitmproxy-ca-cert.pem\n  HASH=$(openssl x509 -inform PEM -subject_hash_old -in \"$CA\" | head -1)\n  adb -s <serial> root\n  adb -s <serial> push \"$CA\" /data/local/tmp/$HASH.0\n  adb -s <serial> shell \"cp /data/local/tmp/$HASH.0 /data/misc/user/0/cacerts-added/$HASH.0 \\\n    && chown system:system /data/misc/user/0/cacerts-added/$HASH.0 \\\n    && chmod 644 /data/misc/user/0/cacerts-added/$HASH.0\"\n  ```\n\n  Even then the app must trust **user** CAs through its `network_security_config`.\n\n**Certificate pinning defeats all of this by design.** If the app pins, requests\nfail and there is nothing to inspect. Say so plainly and use a debug build with\npinning disabled. Do not attempt to bypass pinning in a production build.\n\n## Reading the results\n\n```json\n{\n  \"id\": \"f_0003\", \"method\": \"POST\", \"url\": \"https://api.example.com/v1/login\",\n  \"host\": \"api.example.com\", \"path\": \"/v1/login\", \"status\": 401, \"duration_ms\": 42,\n  \"request_headers_preview\": {\"authorization\": \"<redacted>\"},\n  \"request_body_preview\": \"{\\\"email\\\": \\\"a@b.c\\\", \\\"password\\\": \\\"<redacted>\\\"}\",\n  \"mocked\": false, \"mock_id\": null,\n  \"sizes\": {\"request_bytes\": 120, \"response_bytes\": 80}\n}\n```\n\n- **Credentials are masked before anything is written to disk** — sensitive headers\n  and credential-shaped body fields (`password`, `token`, `api_key`, …). Do not\n  work around this to \"see the real value\".\n- **URLs are scrubbed too.** Values of sensitive query and fragment keys\n  (`token`, `access_token`, `api_key`, `key`, `secret`, `password`, `auth`,\n  `session`, `signature`, `code`, AWS signature keys, and similar — decoded\n  and case-insensitive) and the password in `user:password@host` become\n  `<redacted>` in the stored `url` and `Referer`, again in `requests\n  list/show`, and in HAR export; other query parameters (`page=2`) stay.\n- Bodies are **2 KiB previews** by default. `--capture-bodies` persists full bodies\n  and is off deliberately: bodies are the densest source of secrets and personal\n  data. `requests show --full` needs it and warns when used.\n- Listing is capped (default 50) and reports `total_matched` and `truncated`, so a\n  partial view is never mistaken for the whole story.\n\n## Mock semantics\n\nRules live in a **persistent, machine-level registry**\n(`~/.local/state/autonom/mocks/registry.json`), not in the session. They survive\nproxy restarts, session restarts and reboots, and every session shares one set.\n\n```bash\n# One-liner: this endpoint returns this JSON.\nautonom network mock add --url 'https://api.devbackend.net/post/update/12341' \\\n  --json '{\"status\":\"ok\"}' --note 'ticket-123 repro'\n\n# Or a glob, when the id varies\nautonom network mock add --match '*/post/update/*' --method POST --status 500\n\nautonom network mock list [--all]        # --all includes disabled rules\nautonom network mock show m_1            # rule + a scrubbed body preview\nautonom network mock update m_1 --status 503\nautonom network mock disable m_1 | --all # keeps the rule, stops it firing\nautonom network mock enable  m_1 | --all\nautonom network mock remove m_1          # deletes the rule and its body\nautonom network mock clear               # deletes everything\n```\n\n- `--url` matches the endpoint **exactly** and ignores the query string, so\n  `…/12341` also matches `…/12341?ts=9` but never `…/123415`. `--match` takes a\n  glob and leaves query handling to you.\n- Match on URL, plus optional method and host. **First enabled rule wins.**\n- A matched request **never reaches the origin** — the response is manufactured,\n  not rewritten after the fact.\n- Rules reload without restarting the proxy, so they can be swapped mid-scenario;\n  a corrupt registry keeps the last good set rather than dropping everything.\n- Mock CRUD needs **no session and no device** — rules can be prepared in advance.\n- Rules are validated when added or updated: a target (`--url` or `--match`)\n  is required (`selector_required`), a body that looks like JSON must parse,\n  `--status` must be 100..599, and each `--header` must be `Name: value`.\n- The registry lives outside any repository on purpose: a mock body is often a\n  captured response, and a captured response often carries a token.\n\n### The stale-rule hazard\n\nPersistence has a price: a rule enabled last week fires again the moment a proxy\nstarts, and a fabricated response looks exactly like a real one. The defence is\nthat everything says so — `network start`, `network status` and `doctor` all\nreport `mocks.active` and raise `persistent_mocks_active`.\n\n**Before trusting any network evidence, check `mocks.active` is what you expect.**\nIf a response looks wrong or suspiciously convenient, run `network mock list`\nbefore concluding anything about the backend. `requests list --mocked true` shows\nexactly which flows were faked.\n\n## Honest reporting\n\n1. Report status codes and bodies as **measured facts**, on-screen text separately.\n2. `network status` reports `attached` as `true`, `false`, or **`unknown`**,\n   with the `evidence` behind it, and `attach_state` (`automated`, `manual`,\n   `not_attached`) for how far the attach got. Only traffic *from the target*\n   counts: a flow from the emulator's guest network (`target_flows`), else the\n   device's proxy setting read back (`device_setting`); a host `curl` through\n   the proxy never does. Loopback flows are unattributed\n   (`unattributed_flow_count`, with `recent_user_agents` so you can judge\n   them). On iOS the Simulator and the host share one network stack, so\n   traffic leaves it `unknown` (`host_traffic_indistinguishable`); an Android\n   device that cannot be read is `setting_unreadable`. Do not upgrade\n   `unknown` to \"working\" in a summary.\n3. A HAR exported without `--capture-bodies` carries previews; its `log.comment`\n   says so. Do not present a preview as a full payload.\n4. If nothing was captured, distinguish the causes: not attached, pinning, the app\n   using a client that ignores the proxy, or simply no traffic yet.\n\n## Troubleshooting\n\n| Symptom | Likely cause |\n| --- | --- |\n| `consent_required` | flag missing; on a TTY the phrase is also required |\n| `proxy_not_running` | `network start` first |\n| `mitmdump_required` | install mitmproxy; `autonom doctor` prints the command |\n| `physical_device_attach_unsupported` | use an emulator, or configure the Wi-Fi proxy by hand |\n| Requests list is empty | not attached, pinning, `URLSession` on iOS, or a Flutter app without an in-app proxy hook |\n| App shows network errors after a crash | the device may still point at a dead proxy — `autonom doctor` reports it; run `network detach` |\n\n## Related\n\n- `mobile-session` — sessions and teardown\n- `mobile-screen` — correlate a captured response with on-screen state\n- `android-debugger-agent`, `ios-debugger-agent`\n"
}

SHA-256 of public snapshot: e7309838bce13d244997e1a819c9e0c68c41f118e5ed43f0c5a10b945d013375