Skip to content

TELCORE-166: add libfreeswitch hooks for mod_web_server - #599

Open
damirn wants to merge 6 commits into
telnyx/telephony/deploy-developmentfrom
damir/telcore-166-build-mod_web_server-shared-http-listener-for-cross-module
Open

TELCORE-166: add libfreeswitch hooks for mod_web_server#599
damirn wants to merge 6 commits into
telnyx/telephony/deploy-developmentfrom
damir/telcore-166-build-mod_web_server-shared-http-listener-for-cross-module

Conversation

@damirn

@damirn damirn commented May 28, 2026

Copy link
Copy Markdown

Adds the core-side pieces that mod_web_server needs to expose a stable C ABI to other FreeSWITCH modules. The module itself ships from its own repository (team-telnyx/mod_web_server) and is dropped into src/mod/applications/mod_web_server/ at deploy time, not committed into the freeswitch tree — same pattern as mod_telnyx and mod_dynamic_gateway.

Why these pieces live in libfreeswitch

APR loads modules with RTLD_LOCAL on Unix (libs/apr/dso/unix/dso.c). Symbols a module exports are not visible to any other loaded module. So for arbitrary modules to call switch_web_server_register(...), the registry, the public ABI, and the request/response opaque types have to sit in libfreeswitch — only the Beast acceptor, sessions, threads, and config loader stay inside the module itself.

What this commit adds

src/include/switch_web_server.h public C ABI
src/include/switch_web_server_internal.h C++ bridge to the module
src/switch_web_server.cpp registry + ABI implementations
Makefile.am add switch_web_server.cpp to libfreeswitch_la_SOURCES
tests/unit_cpp/test_web_server_registry.cpp 43-assertion standalone test
tests/unit_cpp/Makefile.am wire the test (always built; not gated on HAVE_CARES)

Public ABI surface

switch_web_server_register(module, method, path, mode, fn, ud)
switch_web_server_register_prefix(module, method, prefix, mode, fn, ud)
switch_web_server_unregister(module, method, path)
switch_web_server_unregister_module(module)
switch_web_server_available()
switch_web_request_method/path/query/header/body/remote_ip/param(req)
switch_web_response_set_status/header/body/printf(res)

Drain semantics

Each registered route is tagged with its owning module name and a shared ModuleSlot (atomic in_flight + condvar). On lookup, the result carries a move-only InFlightTicket whose destructor decrements the in_flight count; while a handler is running the ticket lives in the dispatch lambda and only drops when the handler returns. The matching switch_web_server_unregister_module() removes routes from the registry and then blocks on the slot's cv until in_flight hits zero, so a module's .so cannot be invoked after teardown.

Numeric input is parsed with strtoll + errno + end-pointer checks inside the registry (none of the public setters are exposed to operator input here, but the same discipline is applied in the module's config loader for consistency).

Tests

tests/unit_cpp/test_web_server_registry.cpp is a standalone C++ program (no FST, no live core) that links against libfreeswitch and covers exact / pattern / prefix matching, 404 / 405 collection, ANY-method wildcards, conflict + idempotent re-register, in-place handler update on re-register by the same module, sweep-by-module isolation, snapshot, and single-route unregister. 43 assertions.

dev-ryanc and others added 2 commits May 27, 2026 20:06
Adds the core-side pieces that mod_web_server needs to expose a stable
C ABI to other FreeSWITCH modules. The module itself ships from its
own repository (team-telnyx/mod_web_server) and is dropped into
src/mod/applications/mod_web_server/ at deploy time, not committed
into the freeswitch tree — same pattern as mod_telnyx and
mod_dynamic_gateway.

Why these pieces live in libfreeswitch
--------------------------------------
APR loads modules with RTLD_LOCAL on Unix (libs/apr/dso/unix/dso.c).
Symbols a module exports are not visible to any other loaded module.
So for arbitrary modules to call switch_web_server_register(...),
the registry, the public ABI, and the request/response opaque types
have to sit in libfreeswitch — only the Beast acceptor, sessions,
threads, and config loader stay inside the module itself.

What this commit adds
---------------------
  src/include/switch_web_server.h          public C ABI
  src/include/switch_web_server_internal.h C++ bridge to the module
  src/switch_web_server.cpp                registry + ABI implementations
  Makefile.am                              add switch_web_server.cpp to libfreeswitch_la_SOURCES
  tests/unit_cpp/test_web_server_registry.cpp  43-assertion standalone test
  tests/unit_cpp/Makefile.am               wire the test (always built; not gated on HAVE_CARES)

Public ABI surface
------------------
  switch_web_server_register(module, method, path, mode, fn, ud)
  switch_web_server_register_prefix(module, method, prefix, mode, fn, ud)
  switch_web_server_unregister(module, method, path)
  switch_web_server_unregister_module(module)
  switch_web_server_available()
  switch_web_request_method/path/query/header/body/remote_ip/param(req)
  switch_web_response_set_status/header/body/printf(res)

Drain semantics
---------------
Each registered route is tagged with its owning module name and a
shared ModuleSlot (atomic in_flight + condvar). On lookup, the result
carries a move-only InFlightTicket whose destructor decrements the
in_flight count; while a handler is running the ticket lives in the
dispatch lambda and only drops when the handler returns. The matching
switch_web_server_unregister_module() removes routes from the registry
and then blocks on the slot's cv until in_flight hits zero, so a
module's .so cannot be invoked after teardown.

Numeric input is parsed with strtoll + errno + end-pointer checks
inside the registry (none of the public setters are exposed to
operator input here, but the same discipline is applied in the
module's config loader for consistency).

Tests
-----
tests/unit_cpp/test_web_server_registry.cpp is a standalone C++
program (no FST, no live core) that links against libfreeswitch and
covers exact / pattern / prefix matching, 404 / 405 collection,
ANY-method wildcards, conflict + idempotent re-register, in-place
handler update on re-register by the same module, sweep-by-module
isolation, snapshot, and single-route unregister. 43 assertions.
@damirn
damirn requested a review from a team May 28, 2026 13:19

@minhtuan1407-telnyx minhtuan1407-telnyx left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Hermes War Room — confidence: HIGH]

Found blockers.

  1. Core build portability is not guaranteed.

src/switch_web_server.cpp is added to libfreeswitch_la_SOURCES, but it uses C++17-only APIs:

  • src/switch_web_server.cpp:20 includes <shared_mutex>
  • src/switch_web_server.cpp:148, 190, 208, 245, 298, 352, 367, 385 use std::shared_mutex / std::shared_lock

The only explicit C++17 flag added here is for the new unit test target:

  • tests/unit_cpp/Makefile.am:5 sets AM_CXXFLAGS = -std=c++14
  • tests/unit_cpp/Makefile.am:12 sets test_web_server_registry_CXXFLAGS = $(AM_CXXFLAGS) -std=c++17

That does not apply to the core libfreeswitch build. I also checked configure.ac; SWITCH_AM_CXXFLAGS is initialized without a C++17 standard flag and only gets platform/PIC/visibility additions. On this host, c++ defaults to __cplusplus 201402L, and a minimal std::shared_mutex probe fails under -std=c++14.

Please either make the core library target explicitly compile this file as C++17, or avoid std::shared_mutex/std::shared_lock in the libfreeswitch code path.

  1. SWITCH_WEB_METHOD_ANY conflicts are not enforced, so route ownership is ambiguous.

Lookup treats ANY as a wildcard:

  • src/switch_web_server.cpp:55-58

But registration conflict checks only compare exact method equality:

  • pattern routes: src/switch_web_server.cpp:151-157
  • exact routes: src/switch_web_server.cpp:167-175
  • prefix routes: src/switch_web_server.cpp:193-200

That allows overlapping routes like:

  • module A registers ANY /foo
  • module B registers GET /foo

Both registrations can succeed, but lookup returns the first matching entry, so dispatch depends on insertion order. This can make a successful registration unreachable or let one module shadow another module’s handler. For a shared process-wide ABI, that is a blocker.

Please make wildcard/specific overlap explicit and safe, e.g. conflict when existing.method == method || existing.method == SWITCH_WEB_METHOD_ANY || method == SWITCH_WEB_METHOD_ANY, while preserving same-module re-register/update semantics. Add tests for exact, pattern, and prefix wildcard conflicts in both insertion orders.

Evidence checked:

  • PR head 7e99fbc3877f391273a8de4a2b8f7e6ebade8f82
  • local checkout /Users/tuan/.hermes/agent-workspaces/ops/freeswitch-pr599-review
  • public/internal ABI, registry registration/lookup/unregister paths, Makefile/configure CXX flag wiring, and unit test coverage

— 🪽 Hermes War Room on behalf of @minhtuan1407-telnyx

…egment match

Two blockers from the PR review on libfreeswitch hooks:

1. C++17 in core. switch_web_server.cpp uses std::shared_mutex /
   std::shared_lock, but no C++ standard flag flowed into the
   libfreeswitch build path. Add AM_CXXFLAGS = -std=c++17 to the
   top-level Makefile.am so libfreeswitch_la's C++ sources get the
   flag. Drop the now-redundant -std=c++17 in tests/unit_cpp.

2. SWITCH_WEB_METHOD_ANY overlap was not enforced at registration,
   so e.g. ANY /foo and GET /foo from different modules could both
   succeed and lookup-order silently picked the winner. Add a
   methods_overlap() helper and wire it into all three registration
   tiers (exact, pattern, prefix) with a carveout that preserves the
   same-module identical-route in-place update. Six new tests cover
   both insertion orders for every tier, ANY-vs-ANY cross-module,
   and the same-module ANY update.

Plus follow-on hardening surfaced during review:

  - Public header documents return codes (SUCCESS / FALSE / GENERR)
    including the new ANY-overlap conflict semantics; previously the
    doc said "FALSE on conflict (different module)" only.
  - Invalid args (empty module / bad path / null handler) now return
    SWITCH_STATUS_GENERR; route conflict stays SWITCH_STATUS_FALSE.
    Callers can distinguish "bad input" from "shadowed route".
  - remove_module()'s drain loop now uses the predicate-variant
    cv.wait_for so a notify fired between in_flight.load() and the
    wait is not lost (previously could stall the full 5s for nothing).
  - Prefix matching is segment-bounded: prefix "/api" matches "/api"
    and "/api/v2/foo" but NOT "/apiv2"; prefix "/api/" matches "/api/"
    and below. Raw substring matching had silently shadowed unrelated
    routes. New test_prefix_segment_boundary covers both shapes.
  - test_same_module_any_reregister_updates now loops over GET / POST
    / PUT / DELETE / PATCH instead of only GET — the whole point of
    ANY is that it matches every verb.
@damirn
damirn requested review from a team and minhtuan1407-telnyx May 29, 2026 13:04

@minhtuan1407-telnyx minhtuan1407-telnyx left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Hermes War Room — confidence: HIGH]

Found blockers at head b1b734e2a5be93a709ee9f1c1be2a367e547f909.

  1. Same-tier route ambiguity is still accepted, so dispatch can depend on registration/module load order.

Current code rejects only raw-identical route conflicts:

  • pattern registration compares p.raw == path
  • prefix registration compares existing.raw == prefix
  • lookup scans patterns_ and prefixes_ in insertion order

That leaves ambiguous same-tier routes registerable:

  • /users/{id} vs /users/{name} have the same match set because parameter names do not affect matching.
  • /api vs /api/v2 both match /api/v2/foo; current prefix lookup returns whichever matching prefix was inserted first.
  • ANY/specific overlap is handled for identical raw strings, but not for semantic equivalents/overlaps.

For a process-wide ABI shared by independently loaded modules, successful registration followed by insertion-order shadowing is not acceptable. Pick and implement one explicit contract:

  • reject semantically equivalent pattern shapes and overlapping prefixes when methods overlap, including SWITCH_WEB_METHOD_ANY, in both insertion orders; or
  • implement deterministic lookup, e.g. exact > normalized/most-specific pattern > longest segment-bounded prefix, and document/test it.

Required tests should cover cross-module and same-module cases, both insertion orders, and ANY vs specific method overlap for:

  • /users/{id} vs /users/{name}
  • /api before /api/v2
  • /api/v2 before /api
  • exact/pattern/prefix cross-tier precedence if coexistence is intended
  1. switch_web_response_printf() has a source-level buffer contract bug.

The implementation sizes the string to needed, then calls:

std::vsnprintf(buf.data(), buf.size() + 1, fmt, ap2)

In C++17, std::string::data() gives writable storage for the string’s current size. Passing size() + 1 asks vsnprintf to write the trailing NUL past the string’s size contract. A local ASAN/UBSAN probe did not catch this, but “the sanitizer missed it” is not a safety proof; it is just the darkness wearing a lab coat.

Fix shape:

  • allocate writable storage for needed + 1, call vsnprintf with that capacity, then resize/assign body to needed; or
  • use a std::vector<char> / equivalent temporary buffer sized needed + 1.

Add a focused test/probe for long formatted output.

  1. Supported build/test evidence is still missing.

Source now wires:

  • Makefile.am:24 with AM_CXXFLAGS = -std=c++17
  • src/switch_web_server.cpp into libfreeswitch_la_SOURCES
  • tests/unit_cpp/Makefile.am with C++17

But there is no passing supported-toolchain CI/build readback proving:

  • autotools regeneration/configure succeeds
  • libfreeswitch compiles and links with the new C++17 source
  • existing C++ objects do not regress
  • tests/unit_cpp/test_web_server_registry runs under the target FreeSWITCH/B2BUA environment

Available CI evidence is not sufficient: the PR Review / review check failed because the internal review action could not resolve, and the telnyx_b2bua_builder / trigger check had no successful build/test evidence in the provided closure. Local probes used Apple clang 17 and stubs; useful, but not a deploy-development build matrix.

Please fix the route ambiguity and response buffer issue, then provide supported CI/build logs for the libfreeswitch C++17 build and test_web_server_registry.

— 🪽 Hermes War Room on behalf of @minhtuan1407-telnyx

Two more blockers from the PR review:

1. Same-tier route ambiguity was still registerable, so dispatch could
   depend on registration/module-load order. The previous round only
   rejected raw-identical conflicts, leaving semantically-equivalent
   routes through:
     - "/users/{id}" vs "/users/{name}" — identical match-set, param
       names do not affect matching;
     - "/api" vs "/api/v2" — overlapping prefixes, lookup returned
       whichever was inserted first.

   Replace raw-equality conflict checks with match-set overlap:
     - patterns_overlap(): same segment count and, per position, either
       side is a {capture} or the literals match — true iff some path
       matches both. Catches "/users/{id}" vs "/users/{name}" and also
       "/a/{x}" vs "/{y}/b".
     - prefixes_overlap(): one segment-bounded-contains the other —
       "/api" vs "/api/v2" conflict, "/api" vs "/apiv2" do not.
   The checks are symmetric, so the outcome is order-independent. The
   same-module identical-route (raw + method) in-place update is
   preserved. Cross-tier coexistence stays allowed and is resolved by
   the documented exact > pattern > prefix lookup precedence.

   Public header documents the full overlap contract. New tests cover
   both insertion orders, cross- and same-module, ANY-vs-specific, the
   disjoint-prefix coexistence case, and cross-tier precedence.

2. switch_web_response_printf() sized a std::string to `needed` then
   let vsnprintf write its NUL into the string's terminator slot via
   data()+size()+1 — relying on a subtle library-contract corner.
   Format into an owned std::vector<char> of needed+1 and assign
   exactly `needed` bytes into body instead. Verified live with a
   5000-char formatted body: exact length, no truncation.

Registry unit tests: 113 passed, 0 failed.

@minhtuan1407-telnyx minhtuan1407-telnyx left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Hermes War Room — confidence: HIGH]

Blocking this at head 5a74928d859b50705397d912fc21d648f179a281.

The current patch has source-level improvements over the prior review: same-tier route overlap checks appear addressed, SWITCH_WEB_METHOD_ANY overlap is handled within tiers, and switch_web_response_printf() now uses a std::vector<char> sized needed + 1 before assigning exactly needed bytes. Good. Not enough to merge.

Blockers:

  1. Public ABI contract and implementation/tests still disagree on cross-tier overlaps.

src/include/switch_web_server.h says registration is rejected whenever match-sets intersect under overlapping methods, so dispatch never depends on registration or module-load order.

But the implementation only rejects conflicts within the same tier:

  • exact-vs-exact in Registry::add()
  • pattern-vs-pattern in Registry::add()
  • prefix-vs-prefix in Registry::add_prefix()

Lookup then applies deterministic precedence:

  • exact
  • pattern
  • prefix

The test suite matches that implementation, not the documented global-reject contract: test_cross_tier_precedence() registers prefix, then pattern, then exact and asserts exact > pattern > prefix.

Pick one ABI and make the code/docs/tests say the same thing:

  • either reject every intersecting match-set globally across exact/pattern/prefix tiers; or
  • explicitly document that cross-tier shadowing is allowed and intentional with precedence exact > pattern > prefix, then add coverage proving exact-vs-pattern, exact-vs-prefix, and pattern-vs-prefix behavior in both insertion orders.

Right now this is a process-wide registry for independently loaded modules, and “the comment promises no shadowing but the code allows shadowing” is not a contract. It is a future incident wearing a fake mustache.

  1. Supported CI/build/test evidence is still missing for this exact head.

Live GitHub readback for 5a74928d859b50705397d912fc21d648f179a281 does not show a successful supported build/test result:

  • combined commit status: pending, total_count=0
  • check-run PR Review / review: completed, conclusion failure
    • failure annotation: unable to resolve team-telnyx/reviewpr-internal
  • check-run Trigger telnyx_b2bua_builder / trigger: queued, conclusion null
  • no visible completed builder logs proving autotools/configure, libfreeswitch C++17 compile/link, or tests/unit_cpp/test_web_server_registry execution

The commit message claims registry unit tests passed, but that is author-supplied text, not CI/build evidence.

Please provide passing supported-toolchain evidence tied to this exact SHA showing:

  • autotools regeneration/configure succeeds
  • libfreeswitch compiles and links with src/switch_web_server.cpp under the target C++17 build
  • tests/unit_cpp/test_web_server_registry runs and passes in the relevant FreeSWITCH/B2BUA/deploy-development environment
  1. Add focused regression coverage for the long switch_web_response_printf() path.

The implementation shape now looks correct, but the focused long-output regression remains absent in the evidence reviewed. Add a test/probe that exercises formatted output larger than the small/common buffer path and verifies the response body length/content.

Summary: the patch is closer, but merge is still blocked on ABI consistency for cross-tier route ownership and missing supported CI/build/test proof for the exact head SHA.

— 🪽 Hermes War Room on behalf of @minhtuan1407-telnyx

Blocker 1 (ABI consistency): the header promised a global match-set
reject across all tiers, but the registry only rejects WITHIN a tier and
resolves cross-tier overlap by the deterministic precedence
exact > pattern > prefix. That precedence is structural (by tier), not
by registration/load order, which is the property the contract cares
about — so document it as intentional rather than weakening the design.
Rewrote the switch_web_server_register contract to state: within-tier
intersections are rejected; cross-tier overlap is allowed and resolved
by tier precedence.

Added test_cross_tier_no_conflict_both_orders covering each tier pair
(exact/pattern, exact/prefix, pattern/prefix) in BOTH insertion orders,
asserting both registrations succeed and lookup resolves to the
more-specific tier regardless of order.

Blocker 3: added test_response_printf_long_output exercising formatted
output of 10000 bytes (past any common small-buffer threshold),
verifying exact byte length, content, no trailing NUL, and last-call
replacement of a longer body.

Registry test suite: 141 checks pass.
@damirn

damirn commented Jun 4, 2026

Copy link
Copy Markdown
Author

@minhtuan1407-telnyx — pushed 9c97891c96 addressing the blockers from the last review. Ready for re-review.

Blocker 1 — ABI / impl / tests now agree on cross-tier overlaps. Took the second option you offered: cross-tier shadowing is documented as intentional with precedence exact > pattern > prefix. The three tiers exist precisely so a specific route beats a catch-all, and the precedence is structural (decided by tier, never by registration or module-load order) — so it has the order-independence the contract cares about; the old "global reject" wording was the inaccurate part, not the behavior. Rewrote the switch_web_server_register contract to state: within a tier, intersecting match-sets under overlapping methods are rejected; across tiers, overlap is allowed and resolved by tier precedence. Added test_cross_tier_no_conflict_both_orders covering each pair (exact/pattern, exact/prefix, pattern/prefix) in both insertion orders — both registrations succeed and lookup resolves to the more-specific tier regardless of order.

Blocker 3 — long switch_web_response_printf() regression added. test_response_printf_long_output formats 10000 bytes (past any common 256/1024/4096 threshold) and verifies exact byte length, content, no embedded/trailing NUL, and that a later short printf fully replaces the longer body.

Blocker 2 — CI evidence. I can't resolve this from my side — the PR Review / review failure was an inability to resolve team-telnyx/reviewpr-internal, and the b2bua builder was queued. Built with the real toolchain locally and the registry suite passes (141 checks, 0 failed), but that's not supported-CI vouching for the SHA. Could you re-trigger the pipeline against 9c97891c96 so the builder produces the autotools/C++17 compile + test_web_server_registry evidence?

@minhtuan1407-telnyx minhtuan1407-telnyx left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Hermes War Room — confidence: HIGH]

Blocking at head 9c97891c9632624527b5d655f92670348e029240.

The source-level issues from the prior review appear addressed:

  • ABI/docs/tests now agree that same-tier overlaps are rejected, while cross-tier overlap is intentionally allowed and resolved by structural precedence: exact > pattern > prefix.
  • SWITCH_WEB_METHOD_ANY overlap is covered within tiers.
  • switch_web_response_printf() now has long-output regression coverage.
  • The registry test suite reportedly covers cross-tier ordering, pattern/prefix conflicts, module unregister sweep, and 10000-byte response formatting.

But the remaining blocker is still real: there is no supported current-head CI/build evidence proving this actually builds and runs in the target FreeSWITCH/B2BUA path.

Live check evidence for 9c97891c9632624527b5d655f92670348e029240:

That means no passing check currently proves:

  • autotools/bootstrap/configure succeeds
  • libfreeswitch compiles and links with src/switch_web_server.cpp
  • the new C++17 path is accepted by the supported builder/toolchain
  • tests/unit_cpp/test_web_server_registry executes successfully for this SHA

The author’s local real-toolchain result, “141 checks, 0 failed,” is useful supporting evidence, but it is not a replacement for the supported CI/builder signal on this deploy-development path. This PR changes core libfreeswitch build inputs and ABI surface; source review optimism is not enough here.

Required before approval:

  1. Re-trigger or unblock the supported builder/check path for head 9c97891c9632624527b5d655f92670348e029240.
  2. Provide green CI/build evidence showing configure/autotools success, libfreeswitch C++17 compile/link success, and tests/unit_cpp/test_web_server_registry passing.
  3. If the builder fails, attach the failing logs instead of letting the queued check sit there like a haunted vending machine.

Once that evidence is green, I do not see a remaining source-level blocker from this review round.

— 🪽 Hermes War Room on behalf of @minhtuan1407-telnyx

@dev-ryanc dev-ryanc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PRBot automated review — no critical issues found. Approved based on: human approval from minhtuan1407-telnyx on current SHA + clean review from prior run (SHA unchanged).

…ent' into damir/telcore-166-build-mod_web_server-shared-http-listener-for-cross-module

Resolved two conflicts:

- src/switch_rtp.c: close_rtp_sockets() used raw switch_socket_close().
  TELCORE-302 introduced rtp_socket_close(), which serializes closes on
  the RTP pool under sock_mutex; took that wrapper in all four spots.
  This branch's TELCORE-137 commit is the same patch that landed on
  deploy-development as #597, so it deduplicated cleanly.

- tests/unit_cpp/Makefile.am: both sides assigned noinst_PROGRAMS, which
  would have dropped one test. Kept the ZMQ timeouts test as the
  assignment and appended test_web_server_registry.
@damirn

damirn commented Aug 7, 2026

Copy link
Copy Markdown
Author

resolved conflicts

@tajamulTelnyx tajamulTelnyx left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

@dev-ryanc dev-ryanc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PRBot automated review — no critical issues found. Approved based on: human approval from tajamulTelnyx on current SHA + PRBot clean review of current SHA (same-SHA approval, SHA unchanged since 2026-08-07 review).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

5 participants