Skip to content

feat(limrun): capture live HTTP(S) traffic through Limrun tunnel inspection - #3941

Closed
thymikee wants to merge 8 commits into
mainfrom
t3/limrun-network-capture
Closed

thymikee wants to merge 8 commits into
mainfrom
t3/limrun-network-capture

Conversation

@thymikee

@thymikee thymikee commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Summary

Add live HTTP(S) capture for Limrun sessions through tunnel inspection. network start selects hosts; network dump reads the bounded capture, with sensitive headers and JSON fields redacted before buffering; network stop returns dumps to app-log capture. Invalid hosts and tunnel failures now have distinct typed reasons. Cleanup reports tunnel-close failures. This includes the shared command and runtime seam because #3930 was closed without merging.

67 files: 36 production, 29 test, 2 docs. Scope includes the shared daemon and CLI surfaces needed to route capture.

agent-device network start --hosts api.example.com
agent-device network dump 15 --include all
agent-device network stop

Validation

  • Tested head: 40bb56892273b046d769e7ff711accc895362aa6; base: 0f7e6680075acffffa248e723788abdc8d8109cb.
  • pnpm check:affected --run --head HEAD: all runnable checks passed, exactCommit=true, checkoutChanged=false. This includes unit and provider integration. GitHub owns coverage and device lanes.
  • Live Limrun Android on this head: an authenticated HTTP Basic request returned 200; network dump --include all reported source=provider, backend=limrun-tunnel, and authorization: [REDACTED]. Capture, session, and daemon were closed.
  • Cookie, API-key, JSON-password, embedded JSON, and truncated JSON redaction have regression tests. A synthetic live POST did not execute, so these fields lack live evidence.
  • Previous Apple smoke failed because its runner budget expired before the iOS step; the current-head Apple and Coverage jobs must pass before merge.

@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-3941/

Built to branch gh-pages at 2026-10-24 09:22 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.59 MB 5.60 MB +15.1 kB
Package (unpacked) 5.59 MB 5.60 MB +15.1 kB
Package (download) 1.70 MB 1.70 MB +5.9 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 38.8 ms 40.4 ms +1.7 ms
CLI --help 78.1 ms 78.3 ms +0.3 ms

@thymikee

Copy link
Copy Markdown
Member Author

Thanks for this. At d17fc2f, two problems need fixing before merge: captured headers and bodies are not redacted, and bad --hosts input is reported as an unsupported device.

mapHarEntry turns request and response headers into one JSON string, then passes that string and the raw bodies to redactDiagnosticData. That redactor masks by key only on object keys, and its text rules only catch Bearer values and unquoted key=value. So "cookie":"sid=...", "authorization":"Basic ...", "x-api-key":"...", "set-cookie":... and a body like {"password":"..."} or {"access_token":"..."} pass through unchanged. I reproduced this with node against kernel/src/redaction.ts. The result is that network dump --include headers|all|body on a Limrun session prints session cookies, Basic credentials, API keys and login credentials into CLI and MCP output, while the PR body and commands.md promise redacted output. The test at network-capture.test.ts:214 only uses Bearer super-secret-token, which the token rule masks under any header name, so it cannot fail. The rule should be: every captured header whose name matches the sensitive-key rule, and every JSON secret field in a captured payload, is masked before it enters the buffer. Redact headers as a name-to-value object first and stringify after, and run bodies through one JSON-aware payload redactor owned by kernel/redaction with its own size budget (the 400-character diagnostic bound also cuts typical header JSON mid-object). Please add failing-first tests with Cookie, Basic auth, x-api-key and a JSON password body. Then rerun live network start, an authenticated request, and network dump --include all, and quote the output showing [REDACTED] in those fields. The earlier live runs predate this change, so they do not cover it.

resolveHosts returns unsupported-device for input errors: all selectors blank, more than 64 selectors, or a selector rejected by validateDestinationTunnelSelectors. classifyStartFailure (line 254) also maps every tunnel failure except already_active (auth, network, a disconnected client) to the same reason. The daemon then replies COMMAND_FAILED with details.reason unsupported-device. So network start --hosts 'https://api.example.com' or 65 selectors says a supported Limrun device is unsupported, and callers that branch on the typed reason give up on capture instead of fixing the argument. A refusal reason should name its cause. Please return a distinct reason such as invalid-hosts for selector validation and map it to INVALID_ARGS in the daemon, give unknown tunnel start failures their own reason such as tunnel-start-failed, and keep unsupported-device for owners that cannot capture. A daemon test where the provider rejects an invalid selector and the reply is INVALID_ARGS would cover it.

Not blocking, take or leave: use one wording for --hosts everywhere ("replaces the source's default selectors", since resolveHosts replaces the defaults) and keep the 64-selector limit out of the shared NetworkOptions contract; handleNetworkCaptureControl binds networkCaptureStartUse/StopUse twice, so the first bind throws before the fact.available check and captureUnsupportedResponse never runs (use admitRuntimeUse or bind once); bindLimrunAppLogs now awaits getNetworkCapture on every operation, which loads the tunnel modules and creates a capture per lease, so a lazy getter resolved inside start/stop would avoid that; release() has no test with a started capture and await capture.stop() runs before terminateSession without try/finally, so a throwing tunnel.close() would leave the instance undeleted; networkCaptureCliOutput has no test; networkCaptureStart and Stop look like mutates rather than repeatable, since they open a tunnel that routes device traffic; and the Limrun iOS Metro guidance is unrelated to capture and could be its own docs PR, while notes() matches SDK error text with a regex, so "typed notes" in the PR body is better described as free text.

The contracts types, reasons and payload bound here differ from ADR 0037 and contracts/network-capture.ts on the shared React Native network-capture branch for #3930, which also refuses network start|stop in the daemon with network-capture-cli-required, and ADR 0037 is not on main or this head. Would it be simpler to land that shared seam once (one start/stop contract with a limrun-tunnel source, one redactNetworkPayload for header maps and JSON keys, daemon routing through admitRuntimeUse when the owner declares networkCaptureStart) and shrink this PR to the Limrun provider? That would also remove the redaction duplication in the first finding. The ADR's routing rule for provider-native versus Metro sources would need to merge first, replacing the blanket daemon refusal.

CI: the Coverage gate fails on networkCaptureCliOutput in src/commands/observability/output.ts (CRAP 72, 0% coverage). This PR adds that function without a test, so the failure comes from this change. Smoke Tests failed only in the iOS phase and the summary shows no failing step, so I cannot tell yet whether it relates to this PR; please read or rerun that phase. I did not run a Limrun instance or rerun checks locally, and I counted the live evidence in the PR body for this head because the later delta is race-only. Before merge: fix the redaction with failing-first tests, type the --hosts and tunnel-start refusals, add tests for networkCaptureCliOutput, clear the iOS smoke result, then quote the live redacted dump. No conflicts are known.

…ection

Adds a provider-owned live capture source for Limrun sessions behind the
network runtime seam: network start opens the instance destination tunnel
with inspection enabled, network dump reads the bounded HAR buffer while a
capture runs, and network stop closes the tunnel. Entries map into the
shared NetworkEntry shape and pass through the kernel redaction owner.
Bodies stay opt-in through --include. Other platform owners report the
capture operations unavailable with a typed fact.

Temporary CLI entry: the network command gains start/stop actions and a
repeatable --hosts selector flag; expect rebase against the shared
network start/dump/stop command work.
- memoize per-session capture creation and drain in-flight creates on shutdown
- make stop advance the capture window before awaiting an open, so a tunnel that
  finishes opening late is closed and never adopted or crossed into the next window
- build the raw line from the redacted url
- report the source's own per-field payload cap in the provider dump limits
- scope network flags by action and classify networkHosts for the progress gate
- drop the dead capture-already-started reason and appLogNetworkDump surface
- state the Limrun iOS Metro reality in CLI help, the refusal hint, and the guide
@thymikee
thymikee force-pushed the t3/limrun-network-capture branch from d17fc2f to 40bb568 Compare October 29, 2026 14:04
@vercel

vercel Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agent-device-website Ready Ready Preview Oct 29, 2026 2:05pm UTC

Request Review

@thymikee

Copy link
Copy Markdown
Member Author

Addressed at 40bb56892273b046d769e7ff711accc895362aa6, rebased onto 0f7e6680075acffffa248e723788abdc8d8109cb.

  • Sensitive header maps are redacted before serialization; JSON payload fields are redacted before buffering. Structured fragments in unparseable text and nested strings are withheld. Failing-first tests cover Cookie, Basic auth, API keys, JSON passwords, embedded JSON, and truncated JSON.
  • Invalid selectors return invalid-hosts / INVALID_ARGS; unclassified tunnel failures return tunnel-start-failed. Provider and daemon tests cover the distinction.
  • CLI output tests cover capture start and stop. The old Coverage failure was the uncovered formatter; the new head still needs GitHub's Coverage result.
  • A throwing tunnel close no longer prevents instance termination or lease release. A warning is returned and recorded for background recovery; shutdown surfaces the failure after cleanup.
  • The --hosts wording and payload limits are corrected. I left the existing operation classification: marking all returned starts as mutations causes refused starts to be reported as dispatched. The duplicate admission bind is also unchanged; its thrown unsupported error retains the typed reason.
  • The old Apple smoke log says the runner budget expired before the iOS step began. Current-head Apple CI is required.

pnpm check:affected --run --head HEAD passed all runnable checks at this head (exactCommit=true, checkoutChanged=false). On live Limrun Android, an authenticated request returned 200 and network dump --include all showed source=provider, backend=limrun-tunnel, and "authorization":"[REDACTED]". A separate synthetic POST did not execute; cookie, API-key and JSON-password fields are covered by tests but do not yet have the requested live proof. The capture and session were closed.

@thymikee thymikee closed this Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant