Skip to content

fix(cli): pin website capture connections to public peers - #5668

Merged
miguel-heygen merged 7 commits into
mainfrom
fix/capture-public-transport
Oct 16, 2026
Merged

miguel-heygen merged 7 commits into
mainfrom
fix/capture-public-transport

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

What changed

Website capture now connects only to checked public IPv4 peers. Browser navigation, redirects, subresources and secondary pages use a per-capture proxy that dials the validated numeric address. Asset, font, CSS, GIF, Lottie and standalone download requests use the same address policy with verified HTTPS and manual redirects.

The browser uses a temporary profile that disables direct WebRTC UDP, removes the implicit loopback proxy bypass and disables QUIC. Capture retains the Chromium sandbox on macOS and Windows; Linux keeps its documented compatibility exception. Capture ancestor dotenv files can supply only supported vision settings; inherited HYPERFRAMES_NO_CAPTURE_DOTENV=1 skips loading them entirely.

What I measured

  • 47 capture/dotenv test files: 465 tests pass, including four real Chromium fixture cases.
  • Eight real HTTP/TLS socket integration cases verify numeric dialing, Host/SNI, certificate rejection, private DNS refusal and cancellation.
  • Controlled browser redirect to loopback: two requests before the fix, zero afterward. Private iframe/image, later DNS changes, STUN UDP and TURN TCP also reach zero prohibited fixture requests.
  • The command-line WebRTC flag alone still emitted four local UDP packets; the profile preference reduces this to zero.
  • Live public HTTPS smoke: the browser and Node transport both fetch example.com successfully with normal certificate verification.
  • The full workspace build, CLI/core/Studio/scripts TypeScript, changed-file lint/format, normal commit hooks and the new-only Fallow gate pass. Local checks reuse cached dependency packages with task-local links; the cached test runner is Vitest 3.2.4 rather than the declared 4.1.11, so hosted CI remains required.

fails without the fix: the baseline browser reaches the loopback redirect fixture; dotenv/sandbox tests fail against the prior policy; removing the WebRTC profile preference reproduces local UDP packets.

What I did NOT exercise

No production/private services, paid providers, or live credentials. Browser fixture coverage uses the available Chrome 147 on Linux; it does not establish macOS/Windows execution or support for arbitrary custom browser binaries. Controlled socket tests substitute the final public peer with an owned local fixture and assert the numeric dial argument.

Compatibility

Capture is deliberately IPv4-only, rejects credentialed/non-HTTP(S) initial URLs, refuses plaintext WebSocket upgrades and rejects asset responses that ignore identity content encoding. Unsupported resources may be absent from a capture. The proxy is a request-routing boundary, not an operating-system sandbox against a compromised browser binary.

Windows test correction

The first Windows run passed 4,926 CLI tests and failed one new assertion that treated uppercase and lowercase environment names as separate keys. Windows aliases those names. The successor separates the wrong-case dotenv rejection test from the uppercase allowed-setting control; production behavior is unchanged. All 35 focused scaffolding/dotenv tests, changed-file lint/format and normal commit hooks pass locally. Fresh hosted Windows CI remains required.

Review follow-up

Current head 1679bd8e6 addresses both findings from review at 5f35f025:

  • Explicit transfer deadlines: publicRequest now uses the caller signal when supplied and creates its 30-second default only without one. A real HTTPS fixture streams 35 MiB over 35 seconds under a 120-second caller deadline: reviewed source aborts at 30.01 seconds; the fix completes at 35.09 seconds. Additional tests exercise default expiry and caller cancellation independently.
  • Shipped CLI dotenv boundary: the early CWD loader is now capture-aware and shares the same vision allowlist as output-ancestor loading. Inherited HYPERFRAMES_NO_CAPTURE_DOTENV=1 skips that early file too; inherited browser settings are preserved. Actual CLI dispatch tests cover capture with/without opt-out, inherited browser settings and unchanged non-capture behavior.
  • The CLI was rebuilt and its actual bin/hyperframes.mjs entrypoint exercised from a temporary project with an existing Chrome path, a runtime setting and a synthetic provider key in .env: capture help ignores browser/runtime values, opt-out ignores the provider key too, and non-capture help retains its existing loading behavior. These are startup/help probes, not a capture or provider call.
  • 85 focused transport/bootstrap/scaffolding tests passed before adding the inherited-browser control; all 32 CLI dispatch/lifecycle/registration checks pass with that control. CLI build and CLI TypeScript checks pass; normal commit hooks pass. The complete capture/bootstrap rerun passes 469 tests in 47 files; a separate actual Chrome run passes all four browser cases (473 unique passes total). The final comment-only follow-up passes the comment-share gate and normal commit hooks. Fresh hosted CI is running for this head.

No new Mac/Windows runtime, paid provider, customer content or private-service verification is claimed. The previously documented browser shutdown/profile cleanup limit remains unverified; no new cleanup guarantee is added.

Latest review follow-up (b0c90870a)

This head removes one redundant comment after tested implementation c7ca51969; runtime and test sources are otherwise identical.

  • Leading-option dotenv boundary: The early loader selects capture using the same first non-option command / -- boundary as the current root Citty dispatch (the root declares no value-taking options). Both --json capture and --json --verbose capture now apply the allowlist and inherited opt-out before command loading. Existing inherited browser settings and non-capture dotenv behavior remain covered. The real CLI-dispatch matrix has fifteen cases; six added cases fail on 1679bd8e and pass with this fix. Three rebuilt bin/hyperframes.mjs startup probes also pass, including leading --json with opt-out.
  • Signal cleanup ownership: Capture registers its idempotent session cleanup with the shared browser signal owner before launching. Signal shutdown settles pending launches and raw-browser closure, then joins registered session cleanup before requesting root exit. Manual close and failure paths use the same cleanup and unregister it afterward. Two regressions drive the actual signal owner without calling session.close() first: SIGINT and SIGTERM both fail on 1679bd8e because the explicit profile still exists at the root-exit request, and pass with the fix. Repeated manual close remains idempotent.
  • Executed: 483 tests across 49 passing files, plus four actual-Chrome proxy/WebRTC tests run separately: 487 unique passing tests. The retained real HTTPS test still transfers 35 MiB over 35 seconds under the caller's 120-second deadline. Two actual Chrome child-process signal probes close a page, leave non-browser work pending, and send SIGINT/SIGTERM: the explicit profile is absent and the proxy refuses connections at the root-exit request; Chrome is gone after exit with codes 130/143. CLI build, CLI typecheck, changed-file lint/format, process-ownership gate and normal signed-commit hooks passed. Cached Vitest is 3.2.4, distinct from declared 4.1.11.
  • Limits: Signal execution was on Linux Chrome, not macOS/Windows. The signal probes use a controlled pending promise rather than a provider call. No paid provider or credential request was made. SIGKILL cannot run asynchronous cleanup. Fresh hosted CI for this successor remains a separate gate; no prior approval is transferred.

@mintlify

mintlify Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 15, 2026, 10:13 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

Comment thread packages/cli/src/capture/publicNetwork.ts Dismissed
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 173 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (3)

  • crop-none-px-r0-nested-z100: tracking 0.04, pressJump 0, drop 40.07, reload 40.07, render 40.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.03, renderKey -, undo true, teleport true
  • crop-tween-xpercent-r0-nested-z100: tracking 0.04, pressJump 0, drop 40.03, reload 40.03, render 39.99, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.03, reload 0.03, render 0.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.03, reload 0.03, render 0.03, renderKey -, undo true, teleport true
  • seqplainkeys-none-pct-r0-root-z100: tracking 248.42, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport false / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source review at exact HEAD 5f35f0250af835bb647aa7cef1da08a3edff01c0, API/effective base 0d6bdd453bbcfae2280cf2b66f0b6b5afccc10ba.

[P2] Preserve callers' explicit transfer deadlines

packages/cli/src/capture/publicNetwork.ts:86–89

The unconditional AbortSignal.timeout(30_000) is combined with, rather than replaced by, the caller's signal, so the new transport terminates healthy response bodies at 30 seconds even when their real consumer explicitly allows 120 seconds. commands/capture/video.ts:13–18 passes that 120-second deadline through safeFetch; its actual CLI route is capture --video … --index … → runVideoMode → streamToFile. Before this change, safeFetch passed that signal directly to fetch; now it calls publicRequest and inherits the shorter deadline.

Concrete source-valid witness: an existing, uniquely named manifest video on public HTTPS, valid TLS, prompt 200/video headers, and a 40 MiB MP4 streaming steadily at 1 MiB/s. This is below the standalone 250 MiB cap and completes at 40 seconds, well within the caller's 120 seconds. BASE can save it; HEAD aborts the body at 30 seconds, streamToFile deletes the partial output, and runVideoMode reports download failure/exit 1. The main capture's mediaCapture.ts:225–287,547–572 is also affected when its remaining budget exceeds 30 seconds: it returns no saved body/localPath. This is a static before/after source witness, not a claimed executed network reproduction.

Honor an explicit caller signal/deadline and apply a default only when needed, or separate bounded connection/header establishment from the caller's body-transfer budget. Add a regression that exercises the real transport with a longer caller deadline; existing consumer mocks do not cover the new timeout.

[P2] Make the new dotenv isolation/opt-out promise cover the shipped entrypoint

docs/packages/cli.mdx:223–227

The new documentation promises that ancestor dotenv cannot change the browser path/runtime settings and that inherited HYPERFRAMES_NO_CAPTURE_DOTENV=1 skips those files. That is not true for normal CLI capture. The actual packaged route (package.json bin → bin/hyperframes.mjs → the tsup src/cli.ts entry) first runs cli.ts:63–74: it loads CWD .env with unrestricted applyDotEnv, before the lazy capture command and before the new allowlist/opt-out in capture/scaffolding.ts:35–41.

With the default ./capture output, CWD is an output ancestor. Start with the opt-out inherited and no inherited browser override, and let that CWD .env name an existing browser using HYPERFRAMES_BROWSER_PATH. The early loader still imports it; browser/manager.ts:337–343 selects it, and captureAttempt.ts:121–124 passes that exact executable path to the launcher. No malicious browser behavior is needed to demonstrate the configuration-selection mismatch. The newly restricted helper is correct in isolation, but the earlier entrypoint bypasses the advertised boundary. Make startup capture-aware or explicitly narrow the documentation so callers are not promised an isolation/skip guarantee they do not receive.

Baseline distinction: the broad CLI loader and browser selection are unchanged from BASE. I am flagging the newly introduced false end-to-end security/opt-out contract, not calling the preexisting loader a newly introduced arbitrary-execution vulnerability.

Public concern and unresolved question

The public CodeQL inline reports file data reaching an outbound request. I traced the real sink: hostname is the checked numeric IPv4 returned by resolvePublicAddress, while Host/SNI and TLS certificate verification retain the original identity; redirect targets are revalidated. I did not establish a concrete introduced private-peer bypass from that public taint report. This is not a runtime security guarantee or a claim to have inspected the private alert trace.

The raw-browser signal shutdown does not join the capture wrapper's profile-removal cleanup while postprocessing is pending. Whether the supported Puppeteer/browser itself removes an explicitly supplied profile remains unverified, so I have not promoted actual profile retention to a confirmed blocker. Normal finally/watchdog paths do close the wrapper.

Audited: all 25 changed files across four completed bounded source lanes (11 transport, 5 consumers, 8 configuration/lifecycle, 1 documentation), each BASE-first within its assigned scope; complete HEAD semantics include exact unchanged-byte maps plus all changed blocks where applicable. Real CLI/bootstrap/browser-selection, download consumers, redirects, proxy/session ownership, retry/watchdog/cleanup and cross-scope fix interactions were traced. Parent independently reconstructed both immutable trees, rebound all 41 changed source versions, checked the 25-path coverage union and rebound 10 unchanged real entry/consumer dependencies. This is scoped aggregate coverage, not a claim of two complete whole-PR passes or whole-repository semantic completeness.

Trusting/not exercised: repository tests/compiler/build/CI, browser/socket/native execution, external Puppeteer/Chromium/platform semantics, providers, rendered/packet evidence and unreviewed transitive dependency ranges. The author's local/fixture/CI statements are attributed reports, not my executions. No live network/provider/credential access, source edits or workflow triggers were performed.

Verdict: REQUEST_CHANGES
Reasoning: the new transport shortens existing supported video transfer deadlines, and the new dotenv isolation/opt-out promise is false at the actual shipped CLI entrypoint.

— tai

@miguel-heygen

miguel-heygen commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Current successor: b0c90870a8d69e486de301c7d99955ac5dc8ff2d.

This head removes one redundant comment to satisfy the comment-share gate; runtime and test sources are identical to tested implementation c7ca51969.

Finding-by-finding reconciliation:

  1. 120-second caller deadline — retained resolved. publicRequest keeps the explicit caller signal instead of replacing it with the default 30-second deadline. The real 35 MiB / 35-second HTTPS fixture passes under the 120-second caller deadline; default-timeout and caller-abort controls remain.
  2. Early CLI dotenv — leading-option gap fixed. Capture policy now follows the current root's first non-option command and -- separator behavior before any dotenv import. Fifteen actual-dispatch cases cover direct, --json, and multiple-leading-option routes, allowlist/opt-out, inherited browser settings and non-capture behavior. Six new cases fail on 1679bd8e; all pass at this successor. Three rebuilt packaged-entry startup probes pass, including --json capture with inherited opt-out.
  3. Explicit profile signal ownership — fixed. The shared signal owner joins registered capture-session cleanup before root exit. Capture manual close, launch failure and signal shutdown share idempotent cleanup. SIGINT/SIGTERM regressions invoke the real owner without first closing the wrapper: both expose retained profiles on 1679bd8e and pass here. Actual Linux Chrome child-process probes also verify profile absence and proxy connection refusal at the root-exit request, native Chrome exit and 130/143 status while non-browser work remains pending.

Validation: 487 unique passing tests (483 capture/bootstrap tests plus four separately executed actual-Chrome proxy/WebRTC cases), CLI build/typecheck, process-ownership audit, lint/format and normal signed-commit hooks. Cached Vitest 3.2.4 differs from declared 4.1.11. No dependency installation or bypass. Linux signal probes do not certify macOS/Windows or SIGKILL cleanup; no provider/credential calls. Current-head hosted CI and eligible review remain required. The public CodeQL taint path is not dismissed; no introduced private-peer bypass has been established by the source audit.

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact HEAD 1679bd8e68e32b728284ccd4f5dc3b8e39e588c9 against API/effective merge-base 0d6bdd453bbcfae2280cf2b66f0b6b5afccc10ba.

Verdict: REQUEST_CHANGES — the explicit deadline fix is resolved, but capture dotenv isolation still misses supported dispatch, and signal shutdown bypasses explicit-profile cleanup.

[P2] Classify capture using the same command selection as real dispatch

packages/cli/src/cli.ts:64–75

captureEnv checks only argv[0], whereas the bundled Citty 0.2.2 dispatch selects the first non-option subcommand (src/command.ts, findSubCommandIndex). Consequently, a supported invocation such as hyperframes --json capture https://www.wikipedia.org --skip-vision takes the unrestricted early dotenv loader and then actually dispatches capture.

Concrete source-valid schedule: inherit HYPERFRAMES_NO_CAPTURE_DOTENV=1, leave the inherited browser override unset, and run that invocation from a project whose .env supplies an existing legitimate Chrome path through HYPERFRAMES_BROWSER_PATH. The bootstrap sees --json, sets captureEnv=false, and imports the file without an allowlist despite the opt-out. The real browser/manager.ts:337–343 then selects the imported path, which captureAttempt passes to the launcher. The later output-ancestor loader's opt-out cannot undo that environment mutation. This also bypasses the allowlist without the opt-out.

The ordinary capture-first case was fixed in f0d32ac6768c5a9fa9b8f72b83e7989c65a45da2; the retained cli.dotEnv.test.ts cases all place the command first, so they do not cover this real dispatch shape. Share the actual subcommand selection, or otherwise enforce the capture policy before any unrestricted import for every supported capture route, and add a leading-option dispatch regression.

Baseline distinction: BASE already loaded CWD dotenv broadly. This is an incomplete implementation of this PR's new isolation/opt-out contract, not a newly introduced arbitrary-execution vulnerability or a claim that an existing Chrome binary is malicious.

[P2] Join explicit capture-profile cleanup into the signal owner

packages/cli/src/capture/launchCaptureBrowser.ts:30–53

The new launcher creates hf-capture-profile-* and passes it as explicit userDataDir. Its disconnected callback closes only the proxy; profile removal exists in the wrapper's close() and launch-error catch. However, the actual unchanged signal owner in browser/launch.ts:21–31,73–105 registers and closes the raw Puppeteer Browser, not this wrapper. After raw-browser shutdown it calls requestCliExit; cli.ts:346–351 finalizes telemetry and calls process.exit without joining the running capture's wrapper cleanup.

A valid user schedule is Ctrl-C after coreExtractionPhase.ts:428 closes its page while postExtractionPhase.ts:307–336 is awaiting optional vision work. Closing the raw browser need not settle that non-browser promise. The root can therefore exit before captureAttempt reaches its finally, leaving the new profile behind. This is not an impossible within-one-turn race and does not require a browser crash, malicious input, or uncatchable SIGKILL.

The uncertainty recorded in my earlier review is now resolved by the pinned dependency source: the lock selects puppeteer-core@25.11.0 / @puppeteer/browsers@3.2.2; at upstream commit 043cd46e594b39ea77343e221a59724183448059, ChromeLauncher sets isTempUserDataDir only when it creates the profile itself, and cleanUserDataDir removes only that temporary case. BASE omits userDataDir and uses this dependency-owned cleanup; HEAD's explicit directory is not covered by it. Register an idempotent capture-session cleanup with the same signal shutdown owner and await profile removal before root exit. The new disconnected test explicitly calls session.close() afterward, so it does not cover this production shutdown path.

This is a source-proven ownership/retention defect, not a claim that I executed a signal or inspected any profile contents.

Prior review reconciliation

  • Explicit transfer deadline: resolved by f0d32ac6768c5a9fa9b8f72b83e7989c65a45da2. Current publicRequest uses init.signal ?? AbortSignal.timeout(30_000) through DNS/request/body, preserving the real standalone video's 120-second caller deadline. The retained HTTPS fixture streams 35 MiB over 35 seconds; separate cases distinguish default expiry from caller cancellation. Regression sources read, not run.
  • Early dotenv boundary: partially resolved, with the supported leading-option bypass above remaining.
  • Public CodeQL concern: numeric IPv4 dialing, original Host/SNI, TLS verification and redirect revalidation were traced. No concrete introduced private-peer bypass was established from the public taint report. I did not inspect or dismiss the private scanning alert.

Audited: Two independent complete changed-source passes cover all 27 changed paths, 44 present BASE/HEAD versions, and tree-proven absences, BASE-first with explicit byte-equality transfers where applicable. Actual CLI packaging/bootstrap/dispatch/browser selection, transport/download consumers, proxy/session ownership, retry/watchdog/postprocessing/cleanup and bundled cross-feature pairs were traced. Parent separately reconstructed both current trees, rehashed all 44 changed versions, rebound real dependencies and the pinned Citty/Puppeteer sources, and independently checked the two findings and historical fixes. This is not a third full semantic pass or a whole-repository audit.

Trusting / not exercised: Repository tests, compiler/build/CI, browser/socket/native execution, rendered/packet evidence, providers and unreviewed transitive source ranges. The dependency source contract above is pinned; arbitrary custom browser binaries and platform runtime behavior are not certified. Author test/CI/help-probe receipts remain attributed reports. No live service/credential access, process signals, source edits, or workflow triggers were performed.

— tai

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact HEAD b0c90870a8d69e486de301c7d99955ac5dc8ff2d against API/effective merge-base 0d6bdd453bbcfae2280cf2b66f0b6b5afccc10ba.

My earlier change requests are resolved in this revision:

  • Explicit transfer deadlines: f0d32ac6768c5a9fa9b8f72b83e7989c65a45da2 preserves the caller signal instead of imposing the default 30-second limit on it. The real standalone video route supplies 120 seconds. Retained HTTP/TLS regression sources cover a 35 MiB/35-second response, default versus explicit expiry, and cancellation after headers.
  • Leading-option dotenv isolation: c7ca51969b6c75addc4ae355cf5bd5756483a9ca selects the first non-option command before --, matching the pinned Citty 0.2.2 dispatcher for the actual root, which declares no value-taking options. The early CWD and ancestor loaders share the allowlist and inherited opt-out. Actual CLI-dispatch test sources cover direct, one-leading-option and multiple-leading-option capture, inherited browser settings, opt-out and non-capture compatibility.
  • Explicit-profile signal cleanup: the same fixing commit registers the capture-session cleanup with the shared signal owner before native launch. Shutdown joins pending launches, raw-browser closure and registered session cleanup before requesting root exit; the wrapper removes its explicitly owned profile. The SIGINT/SIGTERM regression sources invoke the real signal owner without first calling session.close() and inspect profile absence at the root-exit request. Pinned Puppeteer source confirms why explicit profiles require this ownership.

I checked these fixes together with real bootstrap/browser selection, download consumers, redirects, stream cancellation/backpressure, proxy teardown, watchdog/retry and pending postprocessing. The current final commit is a redundant-comment removal after that implementation, not a substitute for current integration review.

Nonblocking compatibility note: capture/publicNetwork.ts:18 intentionally excludes all 192.0.0.0/16, broader than the old shared classifier's special-use /24 exclusions. Consequently some otherwise global IPv4 peers are now refused by both transports. This is a deliberate conservative policy narrowing, not evidence of a private-peer bypass; keep it visible as a compatibility limit.

Public CodeQL concern: the checked numeric IPv4 is the actual dial target; original Host/SNI, certificate verification and redirect revalidation remain in place. No concrete introduced private-peer bypass was established from the public taint report. I did not inspect or dismiss the private scanning alert.

Audited: the aggregate source review covers all 29 changed leaves and 47 present BASE/HEAD versions, including 11 tree-proven additions. The independent adversarial pass covered all changed-source semantics BASE-first, with exact unchanged-span transfers where applicable. The primary pass fully read production and current tests but was partial for unchanged documentation and separately reread BASE-test ranges. Parent independently reconstructed both trees, rehashed every changed version, read the complete generated diff, and checked the key dispatch, deadline and cleanup integration. This is scoped aggregate coverage, not two unqualified full passes or a whole-repository audit.

Trusting / not exercised: repository tests, compiler/build/CI, browser/socket/native execution, platform/packet evidence, providers and unreviewed transitive internals. Regression sources were read, not run. Author execution receipts remain attributed reports; macOS/Windows, arbitrary custom browsers and SIGKILL cleanup are not certified. No credentials/live services, process signals, source edits or workflow triggers were used.

Verdict: APPROVE
Reasoning: the historical deadline, leading-option isolation and profile-cleanup defects are repaired at this exact head; the current source audit established no remaining introduced blocker.

— tai

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 8be76fc Oct 16, 2026
74 of 75 checks passed
@miguel-heygen
miguel-heygen deleted the fix/capture-public-transport branch October 16, 2026 02:51
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.

3 participants